From a499fe4cd49f673905189df3a3b7cb7d42872d04 Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Tue, 15 Jul 2025 17:27:40 +0200 Subject: [PATCH] frontend: enhance dialog component with force delete option and improved layout --- .../basic-dialog/dialog.component.scss | 126 +++++++++++++++--- .../dialogs/basic-dialog/dialog.component.ts | 66 +++++++-- .../src/lib/models/notification.model.ts | 5 + .../pages/console/rooms/rooms.component.ts | 41 +++++- .../src/lib/services/notification.service.ts | 3 +- 5 files changed, 210 insertions(+), 31 deletions(-) diff --git a/frontend/projects/shared-meet-components/src/lib/components/dialogs/basic-dialog/dialog.component.scss b/frontend/projects/shared-meet-components/src/lib/components/dialogs/basic-dialog/dialog.component.scss index 29953d0..ae61849 100644 --- a/frontend/projects/shared-meet-components/src/lib/components/dialogs/basic-dialog/dialog.component.scss +++ b/frontend/projects/shared-meet-components/src/lib/components/dialogs/basic-dialog/dialog.component.scss @@ -37,12 +37,19 @@ .dialog-title { @extend .ov-text-center; @include ov-theme-transition; + @include ov-flex-center; + gap: var(--ov-meet-spacing-sm); color: var(--ov-meet-text-primary); font-size: var(--ov-meet-font-size-xl) !important; font-weight: var(--ov-meet-font-weight-medium); margin-bottom: var(--ov-meet-spacing-md); padding: var(--ov-meet-spacing-lg) var(--ov-meet-spacing-lg) 0; line-height: var(--ov-meet-line-height-tight); + + .dialog-icon { + @include ov-icon(md); + color: var(--ov-meet-text-secondary); + } } // === DIALOG CONTENT === @@ -54,6 +61,53 @@ padding: var(--ov-meet-spacing-md) var(--ov-meet-spacing-lg); text-align: center; min-height: auto; + + .dialog-message { + margin-bottom: var(--ov-meet-spacing-md); + } + + .force-checkbox-container { + @include ov-theme-transition; + margin-top: var(--ov-meet-spacing-lg); + padding: var(--ov-meet-spacing-md); + border: 1px solid var(--ov-meet-border-color); + border-radius: var(--ov-meet-radius-md); + background: var(--ov-meet-surface-variant); + text-align: left; + + .force-checkbox { + margin-bottom: var(--ov-meet-spacing-sm); + + .checkbox-text { + font-size: var(--ov-meet-font-size-md); + font-weight: var(--ov-meet-font-weight-medium); + color: var(--ov-meet-text-primary); + } + } + + .checkbox-warning { + display: flex; + align-items: flex-start; + justify-content: flex-start; + gap: var(--ov-meet-spacing-xs); + margin-top: var(--ov-meet-spacing-xs); + + .warning-icon { + @include ov-icon(sm); + color: var(--ov-meet-color-warning); + flex-shrink: 0; + margin-top: 2px; // Align with first line of text + } + + .warning-text { + font-size: var(--ov-meet-font-size-xs); + color: var(--ov-meet-text-secondary); + font-style: italic; + line-height: var(--ov-meet-line-height-relaxed); + flex: 1; + } + } + } } // === DIALOG ACTIONS === @@ -62,52 +116,61 @@ gap: var(--ov-meet-spacing-sm); padding: var(--ov-meet-spacing-lg); margin: 0; + justify-content: center; // Button styling button { @include ov-button-base; @include ov-theme-transition; + @include ov-flex-center; + gap: var(--ov-meet-spacing-xs); min-width: 80px; margin: 0; // Reset default margin + mat-icon { + @include ov-icon(sm); + } + // Cancel/Secondary button - &[mat-button] { + &.cancel-button { color: var(--ov-meet-text-secondary); border: 1px solid var(--ov-meet-border-color); background: transparent; &:hover { @include ov-hover-lift(-1px); - - // background: var(--ov-meet-surface-hover); - // color: var(--ov-meet-text-primary); + background: var(--ov-meet-surface-hover); + color: var(--ov-meet-text-primary); } - - // &:focus { - // outline: 2px solid var(--ov-meet-color-primary); - // outline-offset: 2px; - // } } // Confirm/Primary button - &[mat-flat-button] { - // background: var(--ov-meet-color-primary); + &.confirm-button { color: var(--ov-meet-text-on-primary); &:hover { @include ov-hover-lift(-1px); - // background: var(--ov-meet-color-primary-dark); - // box-shadow: var(--ov-meet-shadow-hover); } - // &:focus { - // outline: 2px solid var(--ov-meet-color-primary-light); - // outline-offset: 2px; - // } - &:active { transform: translateY(0); } + + // Force delete state - changes to warning color + &.force-delete { + background: var(--ov-meet-color-warning) !important; + border-color: var(--ov-meet-color-warning) !important; + + &:hover { + background: var(--ov-meet-color-warning) !important; + filter: brightness(0.9); + } + + &:focus { + outline: 2px solid var(--ov-meet-color-warning); + outline-offset: 2px; + } + } } } } @@ -117,11 +180,35 @@ .dialog-title { font-size: var(--ov-meet-font-size-lg); padding: var(--ov-meet-spacing-md) var(--ov-meet-spacing-md) 0; + flex-direction: column; + gap: var(--ov-meet-spacing-xs); + + .dialog-icon { + @include ov-icon(lg); + } } - mat-dialog-content { + ::ng-deep mat-dialog-content { padding: var(--ov-meet-spacing-sm) var(--ov-meet-spacing-md); font-size: var(--ov-meet-font-size-sm); + + .force-checkbox-container { + margin-top: var(--ov-meet-spacing-md); + padding: var(--ov-meet-spacing-sm); + + .checkbox-warning { + margin-left: 0; // Remove extra margin on mobile + gap: var(--ov-meet-spacing-xs); + + .warning-icon { + margin-top: 1px; // Adjust for smaller text + } + + .warning-text { + font-size: var(--ov-meet-font-size-xs); + } + } + } } .dialog-action { @@ -132,6 +219,7 @@ button { width: 100%; min-width: auto; + justify-content: center; } } } diff --git a/frontend/projects/shared-meet-components/src/lib/components/dialogs/basic-dialog/dialog.component.ts b/frontend/projects/shared-meet-components/src/lib/components/dialogs/basic-dialog/dialog.component.ts index 855c8e1..0c0cc2f 100644 --- a/frontend/projects/shared-meet-components/src/lib/components/dialogs/basic-dialog/dialog.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/components/dialogs/basic-dialog/dialog.component.ts @@ -1,18 +1,50 @@ import { ChangeDetectionStrategy, Component, Inject, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; -import { MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogRef } from '@angular/material/dialog'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatIconModule } from '@angular/material/icon'; +import { MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle } from '@angular/material/dialog'; import type { DialogOptions } from '@lib/models'; @Component({ selector: 'ov-dialog', standalone: true, - imports: [MatButtonModule, MatDialogActions, MatDialogContent], + imports: [FormsModule, MatButtonModule, MatIconModule, MatCheckboxModule, MatDialogActions, MatDialogContent, MatDialogTitle], template: `
-

{{ data.title }}

- +

+ {{ getDialogIcon() }} + {{ data.title }} +

+ +
+ @if (data.showForceCheckbox) { +
+ + {{ data.forceCheckboxText }} + +
+ warning + {{ data.forceCheckboxDescription }} +
+
+ } +
- - + @@ -22,15 +54,33 @@ import type { DialogOptions } from '@lib/models'; }) export class DialogComponent { readonly dialogRef = inject(MatDialogRef); + forceDelete = false; constructor(@Inject(MAT_DIALOG_DATA) public data: DialogOptions) {} close(type: 'confirm' | 'cancel'): void { this.dialogRef.close(); - if (type === 'confirm' && this.data.confirmCallback) { - this.data.confirmCallback(); + if (type === 'confirm') { + if (this.forceDelete && this.data.forceConfirmCallback) { + this.data.forceConfirmCallback(); + } else if (this.data.confirmCallback) { + this.data.confirmCallback(); + } } else if (type === 'cancel' && this.data.cancelCallback) { this.data.cancelCallback(); } } + + getDialogIcon(): string { + if (this.data.title?.toLowerCase().includes('delete')) { + return 'delete_outline'; + } + if (this.data.title?.toLowerCase().includes('warning')) { + return 'warning'; + } + if (this.data.title?.toLowerCase().includes('error')) { + return 'error_outline'; + } + return 'help_outline'; + } } diff --git a/frontend/projects/shared-meet-components/src/lib/models/notification.model.ts b/frontend/projects/shared-meet-components/src/lib/models/notification.model.ts index 0289ee9..17e52a9 100644 --- a/frontend/projects/shared-meet-components/src/lib/models/notification.model.ts +++ b/frontend/projects/shared-meet-components/src/lib/models/notification.model.ts @@ -5,4 +5,9 @@ export interface DialogOptions { cancelText?: string; cancelCallback?: () => void; confirmCallback?: () => void; + // Force delete options + showForceCheckbox?: boolean; + forceCheckboxText?: string; + forceCheckboxDescription?: string; + forceConfirmCallback?: () => void; } diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/rooms.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/rooms.component.ts index 3297999..4317bbb 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/rooms.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/rooms.component.ts @@ -276,12 +276,30 @@ export class RoomsComponent implements OnInit { } }; + const forceDeleteCallback = async () => { + try { + const response = await this.roomService.deleteRoom(roomId, true); // force = true + + const currentRooms = this.rooms(); + this.rooms.set(currentRooms.filter((r) => r.roomId !== roomId)); + this.notificationService.showSnackbar('Room force deleted successfully'); + } catch (error) { + this.notificationService.showAlert('Failed to force delete room'); + this.log.e('Error force deleting room:', error); + } + }; + this.notificationService.showDialog({ confirmText: 'Delete', cancelText: 'Cancel', title: 'Delete Room', message: `Are you sure you want to delete the room ${roomId}?`, - confirmCallback: deleteCallback + confirmCallback: deleteCallback, + showForceCheckbox: true, + forceCheckboxText: 'Force delete', + forceCheckboxDescription: + 'This will immediately disconnect all active participants and delete the room without waiting for participants to leave', + forceConfirmCallback: forceDeleteCallback }); } @@ -339,13 +357,32 @@ export class RoomsComponent implements OnInit { } }; + const bulkForceDeleteCallback = async () => { + try { + const roomIds = rooms.map((r) => r.roomId); + const response = await this.roomService.bulkDeleteRooms(roomIds, true); // force = true + + const currentRooms = this.rooms(); + this.rooms.set(currentRooms.filter((r) => !roomIds.includes(r.roomId))); + this.notificationService.showSnackbar('All rooms force deleted successfully'); + } catch (error) { + this.notificationService.showAlert('Failed to force delete rooms'); + this.log.e('Error force deleting rooms:', error); + } + }; + const count = rooms.length; this.notificationService.showDialog({ confirmText: 'Delete all', cancelText: 'Cancel', title: 'Delete Rooms', message: `Are you sure you want to delete ${count} rooms?`, - confirmCallback: bulkDeleteCallback + confirmCallback: bulkDeleteCallback, + showForceCheckbox: true, + forceCheckboxText: 'Force delete', + forceCheckboxDescription: + 'This will immediately disconnect all active participants and delete the room without waiting for participants to leave', + forceConfirmCallback: bulkForceDeleteCallback }); } } diff --git a/frontend/projects/shared-meet-components/src/lib/services/notification.service.ts b/frontend/projects/shared-meet-components/src/lib/services/notification.service.ts index e050756..40b7667 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/notification.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/notification.service.ts @@ -44,11 +44,10 @@ export class NotificationService { panelClass: 'custom-snackbar' }); } - showDialog(options: DialogOptions): void { this.dialog.open(DialogComponent, { data: options, - width: '400px', + width: '450px', disableClose: true }); }