frontend: enhance dialog component with force delete option and improved layout

This commit is contained in:
Carlos Santos 2025-07-15 17:27:40 +02:00
parent 0e4cfa5bb5
commit a499fe4cd4
5 changed files with 210 additions and 31 deletions

View File

@ -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;
}
}
}

View File

@ -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: ` <div class="dialog-container">
<h2 mat-dialog-title class="dialog-title ov-text-center">{{ data.title }}</h2>
<mat-dialog-content [innerHTML]="data.message"></mat-dialog-content>
<h2 mat-dialog-title class="dialog-title ov-text-center">
<mat-icon class="dialog-icon">{{ getDialogIcon() }}</mat-icon>
{{ data.title }}
</h2>
<mat-dialog-content>
<div class="dialog-message" [innerHTML]="data.message"></div>
@if (data.showForceCheckbox) {
<div class="force-checkbox-container">
<mat-checkbox
[(ngModel)]="forceDelete"
class="force-checkbox"
color="warn"
>
<span class="checkbox-text">{{ data.forceCheckboxText }}</span>
</mat-checkbox>
<div class="checkbox-warning">
<mat-icon class="warning-icon">warning</mat-icon>
<span class="warning-text">{{ data.forceCheckboxDescription }}</span>
</div>
</div>
}
</mat-dialog-content>
<mat-dialog-actions class="dialog-action">
<button mat-button mat-dialog-close (click)="close('cancel')">{{ data.cancelText }}</button>
<button mat-flat-button mat-dialog-close cdkFocusInitial (click)="close('confirm')">
<button mat-button mat-dialog-close (click)="close('cancel')" class="cancel-button">
{{ data.cancelText }}
</button>
<button
mat-flat-button
mat-dialog-close
cdkFocusInitial
(click)="close('confirm')"
class="confirm-button"
[class.force-delete]="forceDelete"
>
{{ data.confirmText }}
</button>
</mat-dialog-actions>
@ -22,15 +54,33 @@ import type { DialogOptions } from '@lib/models';
})
export class DialogComponent {
readonly dialogRef = inject(MatDialogRef<DialogComponent>);
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';
}
}

View File

@ -5,4 +5,9 @@ export interface DialogOptions {
cancelText?: string;
cancelCallback?: () => void;
confirmCallback?: () => void;
// Force delete options
showForceCheckbox?: boolean;
forceCheckboxText?: string;
forceCheckboxDescription?: string;
forceConfirmCallback?: () => void;
}

View File

@ -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 <b>${roomId}</b>?`,
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 <b>${count}</b> 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
});
}
}

View File

@ -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
});
}