feat: implement confirm dialog for project deletion and item removal in dynamic forms; enhance form field accessibility
This commit is contained in:
parent
58929ae6d4
commit
1806c6fb1f
@ -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,7 +186,19 @@ 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'
|
||||
});
|
||||
|
||||
ref.afterClosed()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((confirmed: boolean) => {
|
||||
if (!confirmed) return;
|
||||
|
||||
this.adminService.deleteProject(project.projectId)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
@ -200,6 +213,7 @@ export class Projects implements OnInit, OnDestroy {
|
||||
});
|
||||
this.filter = 'All';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
15
src/app/shared/confirm-dialog/confirm-dialog.html
Normal file
15
src/app/shared/confirm-dialog/confirm-dialog.html
Normal file
@ -0,0 +1,15 @@
|
||||
<div class="confirm-dialog" role="alertdialog">
|
||||
<div class="confirm-header">
|
||||
<i class="fa-solid fa-triangle-exclamation warn-icon"></i>
|
||||
<h3>{{ title }}</h3>
|
||||
</div>
|
||||
|
||||
<p class="confirm-message">{{ message }}</p>
|
||||
|
||||
<div class="confirm-actions">
|
||||
<button class="btn btn-cancel" (click)="onCancel()">{{ cancelLabel }}</button>
|
||||
<button class="btn" [class.btn-warn]="isWarn" [class.btn-primary]="!isWarn" (click)="onConfirm()">
|
||||
{{ confirmLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
86
src/app/shared/confirm-dialog/confirm-dialog.scss
Normal file
86
src/app/shared/confirm-dialog/confirm-dialog.scss
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
50
src/app/shared/confirm-dialog/confirm-dialog.ts
Normal file
50
src/app/shared/confirm-dialog/confirm-dialog.ts
Normal file
@ -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<ConfirmDialogComponent>);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@
|
||||
@if (f.type === 'text') {
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{ f.label }}</mat-label>
|
||||
<input matInput [id]="f.name" [formControlName]="f.name" [placeholder]="f.placeholder || ''" />
|
||||
<input matInput [id]="f.name" [formControlName]="f.name" />
|
||||
<mat-hint *ngIf="f.placeholder">{{ f.placeholder }}</mat-hint>
|
||||
<mat-error *ngIf="form.get(f.name)?.invalid && (form.get(f.name)?.touched || form.get(f.name)?.dirty)">
|
||||
This field is required.
|
||||
@ -17,7 +17,7 @@
|
||||
@if (f.type === 'textarea') {
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{ f.label }}</mat-label>
|
||||
<textarea matInput [id]="f.name" cdkTextareaAutosize cdkAutosizeMinRows="3" cdkAutosizeMaxRows="6" [formControlName]="f.name" [placeholder]="f.placeholder || ''"></textarea>
|
||||
<textarea matInput [id]="f.name" cdkTextareaAutosize cdkAutosizeMinRows="3" cdkAutosizeMaxRows="6" [formControlName]="f.name"></textarea>
|
||||
<mat-hint *ngIf="f.placeholder">{{ f.placeholder }}</mat-hint>
|
||||
<mat-error *ngIf="form.get(f.name)?.invalid && (form.get(f.name)?.touched || form.get(f.name)?.dirty)">
|
||||
This field is required.
|
||||
|
||||
@ -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<FormGroup>();
|
||||
|
||||
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;
|
||||
|
||||
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[] {
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user