From 1806c6fb1f733545b57396b839064121d8010eca Mon Sep 17 00:00:00 2001 From: Bangara Raju Kottedi Date: Sun, 15 Feb 2026 05:47:32 +0530 Subject: [PATCH] feat: implement confirm dialog for project deletion and item removal in dynamic forms; enhance form field accessibility --- src/app/admin/projects/projects.ts | 36 +++++--- .../shared/confirm-dialog/confirm-dialog.html | 15 ++++ .../shared/confirm-dialog/confirm-dialog.scss | 86 +++++++++++++++++++ .../shared/confirm-dialog/confirm-dialog.ts | 50 +++++++++++ src/app/shared/dynamic-form/dynamic-form.html | 4 +- src/app/shared/dynamic-form/dynamic-form.ts | 24 +++++- src/styles.scss | 12 +++ 7 files changed, 211 insertions(+), 16 deletions(-) create mode 100644 src/app/shared/confirm-dialog/confirm-dialog.html create mode 100644 src/app/shared/confirm-dialog/confirm-dialog.scss create mode 100644 src/app/shared/confirm-dialog/confirm-dialog.ts diff --git a/src/app/admin/projects/projects.ts b/src/app/admin/projects/projects.ts index 9a1316d..c18f169 100644 --- a/src/app/admin/projects/projects.ts +++ b/src/app/admin/projects/projects.ts @@ -5,6 +5,7 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { DynamicPopupComponent } from '../../shared/dynamic-popup/dynamic-popup'; import { DynamicFormConfig } from '../../shared/dynamic-form/dynamic-form-config'; import { ProjectDetailPopupComponent } from './project-detail-popup/project-detail-popup'; +import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog'; import { AdminQuery } from '../state/admin.query'; import { AdminStateService } from '../state/admin-state.service'; import { AdminService } from '../services/admin.service'; @@ -185,20 +186,33 @@ export class Projects implements OnInit, OnDestroy { } deleteProject(project: IProject): void { - if (!confirm(`Are you sure you want to delete "${project.name}"?`)) return; + const ref = this.dialog.open(ConfirmDialogComponent, { + data: { + title: 'Delete Project', + message: `Are you sure you want to delete "${project.name}"? This action cannot be undone.` + }, + panelClass: 'dark-popup-panel', + width: '420px' + }); - this.adminService.deleteProject(project.projectId) + ref.afterClosed() .pipe(takeUntil(this.destroy$)) - .subscribe(() => { - const allProjects = this.adminQuery.getProjects(); - const updatedList = (allProjects?.projects ?? []).filter(p => p.projectId !== project.projectId); - const categories = [...new Set(updatedList.flatMap(p => p.categories ?? []))]; + .subscribe((confirmed: boolean) => { + if (!confirmed) return; - this.adminState.updateProjects({ - projects: updatedList, - projectsCategories: categories.length ? categories : [] - }); - this.filter = 'All'; + this.adminService.deleteProject(project.projectId) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + const allProjects = this.adminQuery.getProjects(); + const updatedList = (allProjects?.projects ?? []).filter(p => p.projectId !== project.projectId); + const categories = [...new Set(updatedList.flatMap(p => p.categories ?? []))]; + + this.adminState.updateProjects({ + projects: updatedList, + projectsCategories: categories.length ? categories : [] + }); + this.filter = 'All'; + }); }); } diff --git a/src/app/shared/confirm-dialog/confirm-dialog.html b/src/app/shared/confirm-dialog/confirm-dialog.html new file mode 100644 index 0000000..43c507e --- /dev/null +++ b/src/app/shared/confirm-dialog/confirm-dialog.html @@ -0,0 +1,15 @@ +
+
+ +

{{ title }}

+
+ +

{{ message }}

+ +
+ + +
+
diff --git a/src/app/shared/confirm-dialog/confirm-dialog.scss b/src/app/shared/confirm-dialog/confirm-dialog.scss new file mode 100644 index 0000000..a5ed86b --- /dev/null +++ b/src/app/shared/confirm-dialog/confirm-dialog.scss @@ -0,0 +1,86 @@ +.confirm-dialog { + padding: 24px; + background: var(--eerie-black-2, #1e1e1e); + border: 1px solid var(--jet, #383838); + border-radius: 14px; + color: var(--white-1, #fff); + min-width: 320px; + max-width: 420px; +} + +.confirm-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 14px; + + h3 { + margin: 0; + font-size: 1.15rem; + color: var(--white-1, #fff); + } +} + +.warn-icon { + color: #ff6b6b; + font-size: 1.2rem; +} + +.confirm-message { + margin: 0 0 20px; + font-size: 0.92rem; + line-height: 1.5; + color: var(--white-2, #ddd); +} + +.confirm-actions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.btn { + padding: 8px 18px; + border: none; + border-radius: 8px; + font-size: 0.88rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s ease, transform 0.1s ease; + + &:active { + transform: scale(0.97); + } +} + +.btn-cancel { + background: rgba(255, 255, 255, 0.06); + color: var(--light-gray-70, #aaa); + border: 1px solid rgba(255, 255, 255, 0.1); + + &:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--white-1, #fff); + } +} + +.btn-warn { + background: rgba(255, 70, 70, 0.15); + color: #ff6b6b; + border: 1px solid rgba(255, 70, 70, 0.25); + + &:hover { + background: rgba(255, 70, 70, 0.25); + color: #ff4444; + } +} + +.btn-primary { + background: rgba(227, 179, 65, 0.15); + color: var(--orange-yellow-crayola, #e3b341); + border: 1px solid rgba(227, 179, 65, 0.25); + + &:hover { + background: rgba(227, 179, 65, 0.25); + } +} diff --git a/src/app/shared/confirm-dialog/confirm-dialog.ts b/src/app/shared/confirm-dialog/confirm-dialog.ts new file mode 100644 index 0000000..edd0985 --- /dev/null +++ b/src/app/shared/confirm-dialog/confirm-dialog.ts @@ -0,0 +1,50 @@ +import { Component, inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; + +export interface ConfirmDialogData { + title?: string; + message: string; + confirmLabel?: string; + cancelLabel?: string; + confirmColor?: 'warn' | 'primary' | 'accent'; +} + +@Component({ + selector: 'app-confirm-dialog', + standalone: true, + imports: [MatDialogModule], + templateUrl: './confirm-dialog.html', + styleUrls: ['./confirm-dialog.scss'] +}) +export class ConfirmDialogComponent { + private dialogRef = inject(MatDialogRef); + data: ConfirmDialogData = inject(MAT_DIALOG_DATA); + + get title(): string { + return this.data.title ?? 'Confirm'; + } + + get message(): string { + return this.data.message; + } + + get confirmLabel(): string { + return this.data.confirmLabel ?? 'Delete'; + } + + get cancelLabel(): string { + return this.data.cancelLabel ?? 'Cancel'; + } + + get isWarn(): boolean { + return (this.data.confirmColor ?? 'warn') === 'warn'; + } + + onCancel(): void { + this.dialogRef.close(false); + } + + onConfirm(): void { + this.dialogRef.close(true); + } +} diff --git a/src/app/shared/dynamic-form/dynamic-form.html b/src/app/shared/dynamic-form/dynamic-form.html index 5fbda29..d5dcc35 100644 --- a/src/app/shared/dynamic-form/dynamic-form.html +++ b/src/app/shared/dynamic-form/dynamic-form.html @@ -6,7 +6,7 @@ @if (f.type === 'text') { {{ f.label }} - + {{ f.placeholder }} This field is required. @@ -17,7 +17,7 @@ @if (f.type === 'textarea') { {{ f.label }} - + {{ f.placeholder }} This field is required. diff --git a/src/app/shared/dynamic-form/dynamic-form.ts b/src/app/shared/dynamic-form/dynamic-form.ts index 4d87056..5e34afc 100644 --- a/src/app/shared/dynamic-form/dynamic-form.ts +++ b/src/app/shared/dynamic-form/dynamic-form.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core'; import { ReactiveFormsModule, FormGroup, FormControl, Validators, FormArray, AbstractControl } from '@angular/forms'; import { DynamicFormConfig } from './dynamic-form-config'; import { DynamicField } from './dynamic-field'; @@ -9,7 +9,9 @@ import { MatSelectModule } from '@angular/material/select'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { provideNativeDateAdapter } from '@angular/material/core'; +import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog'; @Component({ selector: "app-dynamic-form", @@ -17,7 +19,7 @@ import { provideNativeDateAdapter } from '@angular/material/core'; standalone: true, styleUrls: ['./dynamic-form.scss'], providers: [provideNativeDateAdapter()], - imports: [ReactiveFormsModule, CommonModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatButtonModule, MatIconModule, MatDatepickerModule] + imports: [ReactiveFormsModule, CommonModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatButtonModule, MatIconModule, MatDatepickerModule, MatDialogModule] }) export class DynamicFormComponent implements OnInit { @Input() config!: DynamicFormConfig; @@ -25,6 +27,8 @@ export class DynamicFormComponent implements OnInit { @Output() formBuilt = new EventEmitter(); + private dialog = inject(MatDialog); + form!: FormGroup; ngOnInit() { @@ -71,7 +75,21 @@ addArrayItem(field: DynamicField) { removeArrayItem(field: DynamicField, index: number) { const array = this.form.get(field.name) as FormArray; - array.removeAt(index); + + this.dialog.open(ConfirmDialogComponent, { + data: { + title: 'Remove Item', + message: `Are you sure you want to remove this ${field.label?.toLowerCase() ?? 'item'}?`, + confirmLabel: 'Remove', + confirmColor: 'warn' + }, + panelClass: 'dark-popup-panel', + width: '400px' + }).afterClosed().subscribe((confirmed: boolean) => { + if (confirmed) { + array.removeAt(index); + } + }); } getArrayControls(name: string): AbstractControl[] { diff --git a/src/styles.scss b/src/styles.scss index 5ee9f7f..bfc1d0a 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -409,6 +409,18 @@ opacity: 1 !important; } + /* Keep required asterisk inline with label */ + .mat-mdc-floating-label { + white-space: nowrap !important; + overflow: visible !important; + display: inline-flex !important; + align-items: baseline !important; + } + + .mat-mdc-form-field-required-marker { + display: inline !important; + } + /* input / textarea text */ .mat-form-field, .mat-form-field .mat-input-element,