diff --git a/src/app/shared/dialogs/dialogs-form.component.ts b/src/app/shared/dialogs/dialogs-form.component.ts index a49fa3b643..723562ecea 100644 --- a/src/app/shared/dialogs/dialogs-form.component.ts +++ b/src/app/shared/dialogs/dialogs-form.component.ts @@ -2,12 +2,159 @@ import { Component, Inject } from '@angular/core'; import { MatLegacyDialog as MatDialog, MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA } from '@angular/material/legacy-dialog'; -import { UntypedFormGroup, UntypedFormBuilder } from '@angular/forms'; +import { + AbstractControl, + FormArray, + FormBuilder, + FormGroup +} from '@angular/forms'; import { DialogsLoadingService } from './dialogs-loading.service'; import { DialogsListService } from './dialogs-list.service'; import { DialogsListComponent } from './dialogs-list.component'; import { UserService } from '../user.service'; +export interface DialogFieldOption { + name: string; + value: unknown; +} + +export interface DialogListSelection { + _id: string; + [key: string]: unknown; +} + +type DialogFieldType = + | 'checkbox' + | 'textbox' + | 'password' + | 'selectbox' + | 'radio' + | 'rating' + | 'textarea' + | 'markdown' + | 'dialog' + | 'date' + | 'time' + | 'toggle'; + +interface DialogFieldBase { + name: string; + type: DialogFieldType; + planetBeta?: boolean; + disabled?: boolean; + required?: boolean; + placeholder?: string; + tooltip?: string; + label?: string; + authorizedRoles?: string | string[]; +} + +interface DialogCheckboxField extends DialogFieldBase { + type: 'checkbox'; +} + +interface DialogTextboxField extends DialogFieldBase { + type: 'textbox'; + inputType?: string; + min?: number; + step?: string | number; +} + +interface DialogPasswordField extends DialogFieldBase { + type: 'password'; +} + +interface DialogSelectboxField extends DialogFieldBase { + type: 'selectbox'; + multiple?: boolean; + reset?: boolean; + options: DialogFieldOption[]; +} + +interface DialogRadioField extends DialogFieldBase { + type: 'radio'; + options: string[]; +} + +interface DialogRatingField extends DialogFieldBase { + type: 'rating'; +} + +interface DialogTextareaField extends DialogFieldBase { + type: 'textarea'; +} + +interface DialogMarkdownField extends DialogFieldBase { + type: 'markdown'; + imageGroup?: string; +} + +interface DialogDialogField extends DialogFieldBase { + type: 'dialog'; + text: string; + db: string; +} + +interface DialogDateField extends DialogFieldBase { + type: 'date'; + min?: Date | string; + max?: Date | string; +} + +interface DialogTimeField extends DialogFieldBase { + type: 'time'; +} + +interface DialogToggleField extends DialogFieldBase { + type: 'toggle'; +} + +export type DialogField = + | DialogCheckboxField + | DialogTextboxField + | DialogPasswordField + | DialogSelectboxField + | DialogRadioField + | DialogRatingField + | DialogTextareaField + | DialogMarkdownField + | DialogDialogField + | DialogDateField + | DialogTimeField + | DialogToggleField; + +type DialogFieldValue = + | string + | number + | boolean + | Date + | DialogListSelection[] + | string[] + | number[] + | null + | undefined; + +interface DialogFormControls { + [key: string]: AbstractControl; +} + +export type DialogFormGroup = FormGroup; + +export type DialogFormGroupConfig = Parameters[0]; + +export type DialogFormGroupOptions = Parameters[1]; + +export interface DialogFormData { + title: string; + formGroup: DialogFormGroup | DialogFormGroupConfig; + formOptions?: DialogFormGroupOptions; + fields: DialogField[]; + onSubmit?: (value: DialogFormGroup['value'], form: DialogFormGroup) => void; + closeOnSubmit?: boolean; + disableIfInvalid?: boolean; + autoFocus?: boolean; +} + @Component({ templateUrl: './dialogs-form.component.html', styles: [ ` @@ -26,37 +173,37 @@ import { UserService } from '../user.service'; }) export class DialogsFormComponent { - public title: string; - public fields: any; - public modalForm: UntypedFormGroup; - passwordVisibility = new Map(); + public title = ''; + public fields: DialogField[] = []; + public modalForm!: DialogFormGroup; + passwordVisibility = new Map(); isSpinnerOk = true; errorMessage = ''; dialogListRef: MatDialogRef; disableIfInvalid = false; - private markFormAsTouched (formGroup: UntypedFormGroup) { - (Object).values(formGroup.controls).forEach(control => { - control.markAsTouched(); - if (control.controls) { - this.markFormAsTouched(control); - } - }); + private markFormAsTouched(control: AbstractControl): void { + if (control instanceof FormGroup) { + Object.values(control.controls).forEach(childControl => this.markFormAsTouched(childControl)); + } else if (control instanceof FormArray) { + control.controls.forEach(childControl => this.markFormAsTouched(childControl)); + } + control.markAsTouched(); } constructor( public dialogRef: MatDialogRef, private dialog: MatDialog, - private fb: UntypedFormBuilder, - @Inject(MAT_DIALOG_DATA) public data, + private fb: FormBuilder, + @Inject(MAT_DIALOG_DATA) public data: DialogFormData | null, private dialogsLoadingService: DialogsLoadingService, private dialogsListService: DialogsListService, private userService: UserService ) { if (this.data && this.data.formGroup) { - this.modalForm = this.data.formGroup instanceof UntypedFormGroup ? - this.data.formGroup : - this.fb.group(this.data.formGroup, this.data.formOptions || {}); + this.modalForm = this.data.formGroup instanceof FormGroup ? + this.data.formGroup as DialogFormGroup : + this.fb.group(this.data.formGroup, this.data.formOptions || {}) as DialogFormGroup; this.title = this.data.title; this.fields = this.data.fields.filter(field => !field.planetBeta || this.userService.isBetaEnabled()); this.isSpinnerOk = false; @@ -69,7 +216,7 @@ export class DialogsFormComponent { } } - onSubmit(mForm, dialog) { + onSubmit(mForm: DialogFormGroup, dialog: MatDialogRef) { if (!mForm.valid) { this.markFormAsTouched(mForm); return; @@ -84,15 +231,21 @@ export class DialogsFormComponent { } } - togglePasswordVisibility(fieldName) { + togglePasswordVisibility(fieldName: string) { const visibility = this.passwordVisibility.get(fieldName) || false; this.passwordVisibility.set(fieldName, !visibility); } - openDialog(field) { - const initialSelection = this.modalForm.controls[field.name].value.map((value: any) => value._id); + openDialog(field: DialogDialogField) { + const control = this.modalForm.controls[field.name]; + const currentValue = control?.value; + const initialSelection = Array.isArray(currentValue) + ? currentValue + .filter((value): value is DialogListSelection => this.isDialogSelection(value)) + .map(value => value._id) + : []; this.dialogsLoadingService.start(); - this.dialogsListService.attachDocsData(field.db, 'title', this.dialogOkClick(field).bind(this), initialSelection).subscribe((data) => { + this.dialogsListService.attachDocsData(field.db, 'title', this.dialogOkClick(field), initialSelection).subscribe((data) => { this.dialogsLoadingService.stop(); this.dialogListRef = this.dialog.open(DialogsListComponent, { data: data, @@ -103,10 +256,11 @@ export class DialogsFormComponent { }); } - dialogOkClick(field) { - return (selection) => { - this.modalForm.controls[field.name].setValue(selection); - this.dialogListRef.close(); + dialogOkClick(field: DialogDialogField) { + return (selection: DialogListSelection[]) => { + const control = this.modalForm.controls[field.name]; + control?.setValue(selection); + this.dialogListRef?.close(); this.modalForm.markAsDirty(); }; } @@ -119,4 +273,8 @@ export class DialogsFormComponent { return this.modalForm.dirty; } + private isDialogSelection(value: unknown): value is DialogListSelection { + return typeof value === 'object' && value !== null && '_id' in value; + } + } diff --git a/src/app/shared/dialogs/dialogs-form.service.ts b/src/app/shared/dialogs/dialogs-form.service.ts index 0ab0377378..917aadc907 100644 --- a/src/app/shared/dialogs/dialogs-form.service.ts +++ b/src/app/shared/dialogs/dialogs-form.service.ts @@ -3,34 +3,50 @@ import { DialogsFormComponent } from './dialogs-form.component'; import { MatLegacyDialogRef as MatDialogRef, MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { Injectable } from '@angular/core'; import { - UntypedFormBuilder, - UntypedFormGroup + FormBuilder, + FormGroup } from '@angular/forms'; +import { + DialogField, + DialogFormData, + DialogFormGroup, + DialogFormGroupConfig +} from './dialogs-form.component'; @Injectable() export class DialogsFormService { private dialogRef: MatDialogRef; - constructor(private dialog: MatDialog, private fb: UntypedFormBuilder) { } + constructor(private dialog: MatDialog, private fb: FormBuilder) { } - public confirm(title: string, fields: any, formGroup: any, autoFocus = false): Observable { + public confirm( + title: string, + fields: DialogField[], + formGroup: DialogFormGroup | DialogFormGroupConfig, + autoFocus = false + ): Observable { let dialogRef: MatDialogRef; dialogRef = this.dialog.open(DialogsFormComponent, { width: '600px', autoFocus: autoFocus }); - if (formGroup instanceof UntypedFormGroup) { - dialogRef.componentInstance.modalForm = formGroup; + if (formGroup instanceof FormGroup) { + dialogRef.componentInstance.modalForm = formGroup as DialogFormGroup; } else { - dialogRef.componentInstance.modalForm = this.fb.group(formGroup); + dialogRef.componentInstance.modalForm = this.fb.group(formGroup) as DialogFormGroup; } dialogRef.componentInstance.title = title; dialogRef.componentInstance.fields = fields; return dialogRef.afterClosed(); } - openDialogsForm(title: string, fields: any[], formGroup: any, options: any) { + openDialogsForm( + title: string, + fields: DialogField[], + formGroup: DialogFormGroup | DialogFormGroupConfig, + options: DialogFormOpenOptions = {} + ) { this.dialogRef = this.dialog.open(DialogsFormComponent, { width: '600px', autoFocus: options.autoFocus, @@ -47,3 +63,7 @@ export class DialogsFormService { } } + +type DialogFormOpenOptions = Partial> & { + autoFocus?: boolean; +};