task/popup-for-dynamic-form #1
@ -5,6 +5,7 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
|||||||
import { DynamicPopupComponent } from '../../shared/dynamic-popup/dynamic-popup';
|
import { DynamicPopupComponent } from '../../shared/dynamic-popup/dynamic-popup';
|
||||||
import { DynamicFormConfig } from '../../shared/dynamic-form/dynamic-form-config';
|
import { DynamicFormConfig } from '../../shared/dynamic-form/dynamic-form-config';
|
||||||
import { ProjectDetailPopupComponent } from './project-detail-popup/project-detail-popup';
|
import { ProjectDetailPopupComponent } from './project-detail-popup/project-detail-popup';
|
||||||
|
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog';
|
||||||
import { AdminQuery } from '../state/admin.query';
|
import { AdminQuery } from '../state/admin.query';
|
||||||
import { AdminStateService } from '../state/admin-state.service';
|
import { AdminStateService } from '../state/admin-state.service';
|
||||||
import { AdminService } from '../services/admin.service';
|
import { AdminService } from '../services/admin.service';
|
||||||
@ -185,20 +186,33 @@ export class Projects implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteProject(project: IProject): void {
|
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$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe(() => {
|
.subscribe((confirmed: boolean) => {
|
||||||
const allProjects = this.adminQuery.getProjects();
|
if (!confirmed) return;
|
||||||
const updatedList = (allProjects?.projects ?? []).filter(p => p.projectId !== project.projectId);
|
|
||||||
const categories = [...new Set(updatedList.flatMap(p => p.categories ?? []))];
|
|
||||||
|
|
||||||
this.adminState.updateProjects({
|
this.adminService.deleteProject(project.projectId)
|
||||||
projects: updatedList,
|
.pipe(takeUntil(this.destroy$))
|
||||||
projectsCategories: categories.length ? categories : []
|
.subscribe(() => {
|
||||||
});
|
const allProjects = this.adminQuery.getProjects();
|
||||||
this.filter = 'All';
|
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';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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') {
|
@if (f.type === 'text') {
|
||||||
<mat-form-field appearance="outline" class="full-width">
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
<mat-label>{{ f.label }}</mat-label>
|
<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-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)">
|
<mat-error *ngIf="form.get(f.name)?.invalid && (form.get(f.name)?.touched || form.get(f.name)?.dirty)">
|
||||||
This field is required.
|
This field is required.
|
||||||
@ -17,7 +17,7 @@
|
|||||||
@if (f.type === 'textarea') {
|
@if (f.type === 'textarea') {
|
||||||
<mat-form-field appearance="outline" class="full-width">
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
<mat-label>{{ f.label }}</mat-label>
|
<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-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)">
|
<mat-error *ngIf="form.get(f.name)?.invalid && (form.get(f.name)?.touched || form.get(f.name)?.dirty)">
|
||||||
This field is required.
|
This field is required.
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
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 { ReactiveFormsModule, FormGroup, FormControl, Validators, FormArray, AbstractControl } from '@angular/forms';
|
||||||
import { DynamicFormConfig } from './dynamic-form-config';
|
import { DynamicFormConfig } from './dynamic-form-config';
|
||||||
import { DynamicField } from './dynamic-field';
|
import { DynamicField } from './dynamic-field';
|
||||||
@ -9,7 +9,9 @@ import { MatSelectModule } from '@angular/material/select';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
|
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||||
import { provideNativeDateAdapter } from '@angular/material/core';
|
import { provideNativeDateAdapter } from '@angular/material/core';
|
||||||
|
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-dynamic-form",
|
selector: "app-dynamic-form",
|
||||||
@ -17,7 +19,7 @@ import { provideNativeDateAdapter } from '@angular/material/core';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
styleUrls: ['./dynamic-form.scss'],
|
styleUrls: ['./dynamic-form.scss'],
|
||||||
providers: [provideNativeDateAdapter()],
|
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 {
|
export class DynamicFormComponent implements OnInit {
|
||||||
@Input() config!: DynamicFormConfig;
|
@Input() config!: DynamicFormConfig;
|
||||||
@ -25,6 +27,8 @@ export class DynamicFormComponent implements OnInit {
|
|||||||
|
|
||||||
@Output() formBuilt = new EventEmitter<FormGroup>();
|
@Output() formBuilt = new EventEmitter<FormGroup>();
|
||||||
|
|
||||||
|
private dialog = inject(MatDialog);
|
||||||
|
|
||||||
form!: FormGroup;
|
form!: FormGroup;
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -71,7 +75,21 @@ addArrayItem(field: DynamicField) {
|
|||||||
|
|
||||||
removeArrayItem(field: DynamicField, index: number) {
|
removeArrayItem(field: DynamicField, index: number) {
|
||||||
const array = this.form.get(field.name) as FormArray;
|
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[] {
|
getArrayControls(name: string): AbstractControl[] {
|
||||||
|
|||||||
@ -409,6 +409,18 @@
|
|||||||
opacity: 1 !important;
|
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 */
|
/* input / textarea text */
|
||||||
.mat-form-field,
|
.mat-form-field,
|
||||||
.mat-form-field .mat-input-element,
|
.mat-form-field .mat-input-element,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user