update confirmation dialog component

This commit is contained in:
Dan Percic 2023-03-20 00:02:47 +02:00
parent cb8a0ddcf9
commit a1d11cd9eb
10 changed files with 126 additions and 138 deletions

View File

@ -4,13 +4,11 @@ import { MatIconModule, MatIconRegistry } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
import { CommonUiOptions, IqserAppConfig, ModuleOptions } from './utils';
import { ToastComponent } from './shared';
import { ConnectionStatusComponent, FullPageErrorComponent } from './error';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
import { MatLegacyProgressBarModule as MatProgressBarModule } from '@angular/material/legacy-progress-bar';
import { ConfirmationDialogComponent } from './dialog';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ApiPathInterceptor, DefaultUserPreferenceService, IqserConfigService, IqserUserPreferenceService } from './services';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
@ -30,7 +28,7 @@ const matModules = [
MatTooltipModule,
MatProgressBarModule,
];
const components = [ConnectionStatusComponent, FullPageErrorComponent, ConfirmationDialogComponent, ToastComponent];
const components = [ConnectionStatusComponent, FullPageErrorComponent];
@NgModule({
declarations: [...components],

View File

@ -61,20 +61,20 @@ export abstract class BaseDialogComponent implements OnInit, OnDestroy {
this.#subscriptions.unsubscribe();
}
close(): void {
if (this._isInEditMode && this.changed) {
this._openConfirmDialog().then(result => {
if (result in ConfirmOptions) {
if (result === ConfirmOptions.CONFIRM) {
this.save({ closeAfterSave: true });
} else {
this._dialogRef.close();
}
}
});
} else {
close() {
if (!this._isInEditMode || !this.changed) {
this._dialogRef.close();
}
this._openConfirmDialog().then(result => {
if (result) {
if (result === ConfirmOptions.CONFIRM) {
this.save({ closeAfterSave: true });
} else {
this._dialogRef.close();
}
}
});
}
@HostListener('window:keydown.Enter', ['$event'])
@ -94,7 +94,7 @@ export abstract class BaseDialogComponent implements OnInit, OnDestroy {
}
protected _openConfirmDialog() {
const dialogRef = this.#confirmationDialogService.openDialog({ disableConfirm: !this.valid });
const dialogRef = this.#confirmationDialogService.open({ disableConfirm: !this.valid });
return firstValueFrom(dialogRef.afterClosed());
}
}

View File

@ -1,39 +1,29 @@
import { Injectable } from '@angular/core';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { inject, Injectable } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DialogConfig, DialogService } from '../services';
import { ConfirmationDialogComponent, ConfirmationDialogInput, TitleColors } from '.';
type DialogType = 'confirm';
import { ConfirmationDialogComponent, ConfirmOption, defaultDialogConfig, IConfirmationDialogData, TitleColors } from '.';
import { MatDialog } from '@angular/material/dialog';
@Injectable({
providedIn: 'root',
})
export class ConfirmationDialogService extends DialogService<DialogType> {
protected readonly _config: DialogConfig<DialogType> = {
confirm: {
component: ConfirmationDialogComponent,
dialogConfig: { disableClose: false },
},
};
export class ConfirmationDialogService {
readonly #dialog = inject(MatDialog);
constructor(protected readonly _dialog: MatDialog) {
super(_dialog);
}
open(data?: { disableConfirm: boolean }) {
const dialogData: IConfirmationDialogData = {
title: _('confirmation-dialog.unsaved-changes.title'),
question: _('confirmation-dialog.unsaved-changes.question'),
details: _('confirmation-dialog.unsaved-changes.details'),
confirmationText: _('confirmation-dialog.unsaved-changes.confirmation-text'),
discardChangesText: _('confirmation-dialog.unsaved-changes.discard-changes-text'),
disableConfirm: data?.disableConfirm,
titleColor: TitleColors.WARN,
};
// @ts-ignore
openDialog(data?: { disableConfirm: boolean; [key: string]: unknown }): MatDialogRef<unknown> {
return super.openDialog(
'confirm',
new ConfirmationDialogInput({
title: _('confirmation-dialog.unsaved-changes.title'),
question: _('confirmation-dialog.unsaved-changes.question'),
details: _('confirmation-dialog.unsaved-changes.details'),
confirmationText: _('confirmation-dialog.unsaved-changes.confirmation-text'),
discardChangesText: _('confirmation-dialog.unsaved-changes.discard-changes-text'),
disableConfirm: data?.disableConfirm,
titleColor: TitleColors.WARN,
}),
);
return this.#dialog.open<ConfirmationDialogComponent, IConfirmationDialogData, ConfirmOption>(ConfirmationDialogComponent, {
...defaultDialogConfig,
disableClose: false,
data: dialogData,
});
}
}

View File

@ -34,19 +34,17 @@
[label]="config.confirmationText"
[type]="iconButtonTypes.primary"
buttonId="confirm"
>
</iqser-icon-button>
></iqser-icon-button>
<iqser-icon-button
(action)="confirm(ConfirmOptions.SECOND_CONFIRM)"
(action)="confirm(confirmOptions.SECOND_CONFIRM)"
*ngIf="config.alternativeConfirmationText"
[disabled]="config.requireInput && confirmationDoesNotMatch()"
[label]="config.alternativeConfirmationText"
[type]="iconButtonTypes.primary"
>
</iqser-icon-button>
></iqser-icon-button>
<div (click)="confirm(ConfirmOptions.DISCARD_CHANGES)" *ngIf="config.discardChangesText" class="all-caps-label cancel">
<div (click)="confirm(confirmOptions.DISCARD_CHANGES)" *ngIf="config.discardChangesText" class="all-caps-label cancel">
{{ config.discardChangesText }}
</div>

View File

@ -1,21 +1,28 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject, TemplateRef } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { ChangeDetectionStrategy, Component, HostListener, inject, TemplateRef } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { IconButtonTypes } from '../../buttons';
import { CircleButtonComponent, IconButtonComponent, IconButtonTypes } from '../../buttons';
import { NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { FormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { ValuesOf } from '../../utils';
export type TitleColor = 'default' | 'warn';
export const TitleColors = {
DEFAULT: 'default',
WARN: 'warn',
} as const;
export enum TitleColors {
DEFAULT = 'default',
WARN = 'warn',
}
export type TitleColor = ValuesOf<typeof TitleColors>;
export enum ConfirmOptions {
CONFIRM = 1,
SECOND_CONFIRM = 2,
DISCARD_CHANGES = 3,
}
export const ConfirmOptions = {
CONFIRM: 1,
SECOND_CONFIRM: 2,
DISCARD_CHANGES: 3,
} as const;
export type ConfirmOption = ValuesOf<typeof ConfirmOptions>;
interface CheckBox {
value: boolean;
@ -24,76 +31,71 @@ interface CheckBox {
extraContentData?: Record<string, unknown>;
}
interface IConfirmationDialogInput {
title?: string;
titleColor?: TitleColor;
question?: string;
details?: string;
confirmationText?: string;
alternativeConfirmationText?: string;
discardChangesText?: string;
requireInput?: boolean;
disableConfirm?: boolean;
denyText?: string;
translateParams?: Record<string, unknown>;
checkboxes?: CheckBox[];
checkboxesValidation?: boolean;
toastMessage?: string;
interface InternalConfirmationDialogData {
readonly title: string;
readonly titleColor: TitleColor;
readonly question: string;
readonly details: string;
readonly confirmationText: string;
readonly alternativeConfirmationText?: string;
readonly discardChangesText?: string;
readonly requireInput: boolean;
readonly disableConfirm: boolean;
readonly denyText: string;
readonly translateParams?: Record<string, unknown>;
readonly checkboxes: CheckBox[];
readonly checkboxesValidation: boolean;
readonly toastMessage?: string;
}
export class ConfirmationDialogInput implements IConfirmationDialogInput {
title: string;
titleColor: TitleColor;
question: string;
details: string;
confirmationText: string;
alternativeConfirmationText?: string;
discardChangesText?: string;
requireInput: boolean;
disableConfirm: boolean;
denyText: string;
translateParams: Record<string, unknown>;
checkboxes: CheckBox[];
checkboxesValidation: boolean;
toastMessage?: string;
export type IConfirmationDialogData = Partial<InternalConfirmationDialogData>;
constructor(options?: IConfirmationDialogInput) {
this.title = options?.title || _('common.confirmation-dialog.title');
this.titleColor = options?.titleColor || TitleColors.DEFAULT;
this.question = options?.question || _('common.confirmation-dialog.description');
this.details = options?.details || '';
this.confirmationText = options?.confirmationText || _('common.confirmation-dialog.confirm');
this.alternativeConfirmationText = options?.alternativeConfirmationText;
this.discardChangesText = options?.discardChangesText;
this.requireInput = options?.requireInput || false;
this.disableConfirm = options?.disableConfirm || false;
this.denyText = options?.denyText || _('common.confirmation-dialog.deny');
this.translateParams = options?.translateParams || {};
this.checkboxes = options?.checkboxes || [];
this.checkboxesValidation = typeof options?.checkboxesValidation === 'boolean' ? options.checkboxesValidation : true;
this.toastMessage = options?.toastMessage;
}
function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialogData {
return {
...options,
title: options?.title ?? _('common.confirmation-dialog.title'),
titleColor: options?.titleColor ?? TitleColors.DEFAULT,
question: options?.question ?? _('common.confirmation-dialog.description'),
details: options?.details ?? '',
confirmationText: options?.confirmationText ?? _('common.confirmation-dialog.confirm'),
requireInput: options?.requireInput ?? false,
disableConfirm: options?.disableConfirm ?? false,
denyText: options?.denyText ?? _('common.confirmation-dialog.deny'),
checkboxes: options?.checkboxes ?? [],
checkboxesValidation: typeof options?.checkboxesValidation === 'boolean' ? options.checkboxesValidation : true,
};
}
@Component({
templateUrl: './confirmation-dialog.component.html',
styleUrls: ['./confirmation-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
NgIf,
MatIconModule,
FormsModule,
NgForOf,
MatCheckboxModule,
TranslateModule,
NgTemplateOutlet,
IconButtonComponent,
CircleButtonComponent,
MatDialogModule,
],
})
export class ConfirmationDialogComponent {
config: ConfirmationDialogInput;
readonly config = getConfig(inject(MAT_DIALOG_DATA));
inputValue = '';
showToast = false;
readonly inputLabel: string;
readonly ConfirmOptions = ConfirmOptions;
readonly confirmOptions = ConfirmOptions;
readonly iconButtonTypes = IconButtonTypes;
constructor(
private readonly _dialogRef: MatDialogRef<ConfirmationDialogComponent>,
private readonly _dialogRef: MatDialogRef<ConfirmationDialogComponent, ConfirmOption>,
private readonly _translateService: TranslateService,
@Inject(MAT_DIALOG_DATA) private readonly _confirmationDialogInput: ConfirmationDialogInput,
) {
this.config = _confirmationDialogInput ?? new ConfirmationDialogInput();
this.translate(this.config);
this.inputLabel = `${this._translateService.instant('confirmation-dialog.input-label')} '${this.config.confirmationText || ''}'`;
}
@ -103,10 +105,10 @@ export class ConfirmationDialogComponent {
}
get isDeleteAction(): boolean {
return this.config?.titleColor === TitleColors.WARN;
return this.config.titleColor === TitleColors.WARN;
}
get confirmOption(): ConfirmOptions {
get confirmOption(): ConfirmOption {
if (!this.config.checkboxesValidation && this.config.checkboxes[0]?.value) {
return ConfirmOptions.SECOND_CONFIRM;
}
@ -116,19 +118,19 @@ export class ConfirmationDialogComponent {
@HostListener('window:keyup.enter')
onKeyupEnter(): void {
if (this.config.requireInput && !this.confirmationDoesNotMatch()) {
this.confirm(1);
this.confirm(ConfirmOptions.CONFIRM);
}
}
confirmationDoesNotMatch(): boolean {
return this.inputValue.toLowerCase() !== this.config.confirmationText?.toLowerCase();
return this.inputValue.toLowerCase() !== this.config.confirmationText.toLowerCase();
}
deny(): void {
deny() {
this._dialogRef.close();
}
confirm(option: ConfirmOptions): void {
confirm(option: ConfirmOption) {
if (this.uncheckedBoxes) {
this.showToast = true;
} else {
@ -136,8 +138,8 @@ export class ConfirmationDialogComponent {
}
}
translate(obj: ConfirmationDialogInput): void {
const translateKeys: (keyof typeof obj)[] = [
translate(obj: InternalConfirmationDialogData) {
const translateKeys: (keyof InternalConfirmationDialogData)[] = [
'title',
'question',
'details',
@ -147,12 +149,10 @@ export class ConfirmationDialogComponent {
'denyText',
];
translateKeys
.filter(key => !!obj[key])
.forEach(key => {
Object.assign(obj, {
[key]: this._translateService.instant(obj[key] as string, this.config.translateParams) as string,
});
});
const filtered = translateKeys.filter(key => !!obj[key]);
filtered.forEach(key => {
const value = this._translateService.instant(obj[key] as string, this.config.translateParams);
Object.assign(obj, { [key]: value });
});
}
}

View File

@ -1,3 +1,4 @@
export * from './base-dialog.component';
export * from './confirmation-dialog.service';
export * from './confirmation-dialog/confirmation-dialog.component';
export * from './dialog.service';

View File

@ -1,4 +1,3 @@
export * from './dialog.service';
export * from './toaster.service';
export * from './error-message.service';
export * from './generic.service';

View File

@ -15,6 +15,7 @@
</a>
</div>
</div>
<div class="text-right">
<a (click)="remove()" *ngIf="options.closeButton" class="toast-close-button">
<mat-icon svgIcon="iqser:close"></mat-icon>

View File

@ -1,16 +1,17 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Toast, ToastPackage, ToastrService } from 'ngx-toastr';
import { Toast } from 'ngx-toastr';
import { ToasterActions, ToasterOptions } from '../../services';
import { MatIconModule } from '@angular/material/icon';
import { NgForOf, NgIf } from '@angular/common';
import { StopPropagationDirective } from '../../directives';
@Component({
templateUrl: './toast.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatIconModule, NgIf, StopPropagationDirective, NgForOf],
})
export class ToastComponent extends Toast {
constructor(protected readonly _toastrService: ToastrService, readonly toastPackage: ToastPackage) {
super(_toastrService, toastPackage);
}
get actions(): ToasterActions[] {
return (this.options as ToasterOptions)?.actions || [];
}