From e43d15d75e5c8a65e62accdd8483470caaeb01be Mon Sep 17 00:00:00 2001 From: Saniya <37302318+Saby-Bishops@users.noreply.github.com> Date: Wed, 5 Nov 2025 13:25:51 -0600 Subject: [PATCH 1/3] refactor: add typed dialog form helpers --- .../shared/dialogs/dialogs-form.component.ts | 75 +++++++----- .../shared/dialogs/dialogs-form.service.ts | 107 ++++++++++++++++-- 2 files changed, 145 insertions(+), 37 deletions(-) diff --git a/src/app/shared/dialogs/dialogs-form.component.ts b/src/app/shared/dialogs/dialogs-form.component.ts index a49fa3b643..80f8700190 100644 --- a/src/app/shared/dialogs/dialogs-form.component.ts +++ b/src/app/shared/dialogs/dialogs-form.component.ts @@ -1,12 +1,22 @@ import { Component, Inject } from '@angular/core'; import { - MatLegacyDialog as MatDialog, MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA + 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 { 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'; +import { + DialogField, + DialogFormControls, + DialogFormGroup, + DialogFormGroupConfig, + DialogFormValueMap, + DialogsFormData +} from './dialogs-form.service'; @Component({ templateUrl: './dialogs-form.component.html', @@ -27,19 +37,20 @@ import { UserService } from '../user.service'; export class DialogsFormComponent { public title: string; - public fields: any; - public modalForm: UntypedFormGroup; - passwordVisibility = new Map(); + 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: DialogFormGroup | FormArray) { + const controls = control instanceof FormGroup ? Object.values(control.controls) : control.controls; + controls.forEach(innerControl => { + innerControl.markAsTouched(); + if (innerControl instanceof FormGroup || innerControl instanceof FormArray) { + this.markFormAsTouched(innerControl); } }); } @@ -47,16 +58,19 @@ export class DialogsFormComponent { 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: DialogsFormData, 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 as DialogFormGroupConfig, + this.data.formOptions || {} + ); this.title = this.data.title; this.fields = this.data.fields.filter(field => !field.planetBeta || this.userService.isBetaEnabled()); this.isSpinnerOk = false; @@ -69,14 +83,14 @@ export class DialogsFormComponent { } } - onSubmit(mForm, dialog) { + onSubmit(mForm: DialogFormGroup, dialog: MatDialogRef) { if (!mForm.valid) { this.markFormAsTouched(mForm); return; } if (this.data && this.data.onSubmit) { this.dialogsLoadingService.start(); - this.data.onSubmit(mForm.value, mForm); + this.data.onSubmit(mForm.value as DialogFormValueMap, mForm); } if (!this.data || this.data.closeOnSubmit === true) { this.dialogsLoadingService.stop(); @@ -84,26 +98,29 @@ 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: DialogField) { + const control = this.modalForm.controls[field.name]; + const currentValue = control.value as Array<{ _id: string }> | null; + const initialSelection = (currentValue || []).map((value) => value._id); this.dialogsLoadingService.start(); - this.dialogsListService.attachDocsData(field.db, 'title', this.dialogOkClick(field).bind(this), initialSelection).subscribe((data) => { - this.dialogsLoadingService.stop(); - this.dialogListRef = this.dialog.open(DialogsListComponent, { - data: data, - maxHeight: '500px', - width: '600px', - autoFocus: false + this.dialogsListService.attachDocsData(field.db, 'title', this.dialogOkClick(field).bind(this), initialSelection) + .subscribe((data) => { + this.dialogsLoadingService.stop(); + this.dialogListRef = this.dialog.open(DialogsListComponent, { + data: data, + maxHeight: '500px', + width: '600px', + autoFocus: false + }); }); - }); } - dialogOkClick(field) { + dialogOkClick(field: DialogField) { return (selection) => { this.modalForm.controls[field.name].setValue(selection); this.dialogListRef.close(); diff --git a/src/app/shared/dialogs/dialogs-form.service.ts b/src/app/shared/dialogs/dialogs-form.service.ts index 0ab0377378..2b2c02fd1e 100644 --- a/src/app/shared/dialogs/dialogs-form.service.ts +++ b/src/app/shared/dialogs/dialogs-form.service.ts @@ -3,34 +3,125 @@ 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 + AbstractControlOptions, + AsyncValidatorFn, + FormArray, + FormBuilder, + FormControl, + FormControlState, + FormGroup, + ValidatorFn } from '@angular/forms'; +export type DialogFieldType = + | 'checkbox' + | 'textbox' + | 'password' + | 'selectbox' + | 'radio' + | 'rating' + | 'textarea' + | 'markdown' + | 'dialog' + | 'date' + | 'time' + | 'toggle'; + +export interface DialogField { + name: TName; + type: DialogFieldType; + placeholder?: string; + label?: string; + required?: boolean; + disabled?: boolean; + multiple?: boolean; + options?: Array<{ name: string; value?: unknown } | string>; + planetBeta?: boolean; + tooltip?: string; + reset?: boolean; + text?: string; + authorizedRoles?: string | string[]; + imageGroup?: unknown; + db?: string; + [key: string]: unknown; +} + +export interface DialogFormValueMap { + [key: string]: unknown; +} + +type DialogControlConfig = + | TValue + | FormControlState + | FormControl + | FormGroup + | FormArray + | [TValue | FormControlState, (ValidatorFn | ValidatorFn[] | null)?, (AsyncValidatorFn | AsyncValidatorFn[] | null)?]; + +export type DialogFormGroupConfig = { + [K in keyof T]: DialogControlConfig; +}; + +export type DialogFormControls = { + [K in keyof T]: FormControl | FormGroup | FormArray; +}; + +export type DialogFormGroup = FormGroup>; + +export type DialogFormGroupInput = + | DialogFormGroup + | DialogFormGroupConfig; + +export interface DialogsFormOptions { + autoFocus?: boolean; + disableIfInvalid?: boolean; + onSubmit?: (value: T, form: DialogFormGroup) => void; + formOptions?: AbstractControlOptions; + closeOnSubmit?: boolean; + [key: string]: unknown; +} + +export interface DialogsFormData + extends DialogsFormOptions { + title: string; + fields: DialogField[]; + formGroup: DialogFormGroupInput; +} + @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: DialogFormGroupInput, + autoFocus = false + ): Observable { let dialogRef: MatDialogRef; dialogRef = this.dialog.open(DialogsFormComponent, { width: '600px', autoFocus: autoFocus }); - if (formGroup instanceof UntypedFormGroup) { + if (formGroup instanceof FormGroup) { dialogRef.componentInstance.modalForm = formGroup; } else { - dialogRef.componentInstance.modalForm = this.fb.group(formGroup); + dialogRef.componentInstance.modalForm = this.fb.group>(formGroup); } dialogRef.componentInstance.title = title; dialogRef.componentInstance.fields = fields; - return dialogRef.afterClosed(); + return dialogRef.afterClosed() as Observable; } - openDialogsForm(title: string, fields: any[], formGroup: any, options: any) { + openDialogsForm( + title: string, + fields: DialogField[], + formGroup: DialogFormGroupInput, + options: DialogsFormOptions = {} + ) { this.dialogRef = this.dialog.open(DialogsFormComponent, { width: '600px', autoFocus: options.autoFocus, From cb6331f5b34c0140054fe651b8393c1a9cab7533 Mon Sep 17 00:00:00 2001 From: Saniya <37302318+Saby-Bishops@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:28:00 -0600 Subject: [PATCH 2/3] Fix dialog form typings for compatibility --- .../shared/dialogs/dialogs-form.component.ts | 18 +++++++----------- src/app/shared/dialogs/dialogs-form.service.ts | 11 +++++------ src/app/surveys/surveys.component.ts | 2 +- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/app/shared/dialogs/dialogs-form.component.ts b/src/app/shared/dialogs/dialogs-form.component.ts index 80f8700190..c6650f38b1 100644 --- a/src/app/shared/dialogs/dialogs-form.component.ts +++ b/src/app/shared/dialogs/dialogs-form.component.ts @@ -4,17 +4,13 @@ import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA } from '@angular/material/legacy-dialog'; -import { FormArray, FormBuilder, FormGroup } 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'; import { DialogField, - DialogFormControls, - DialogFormGroup, - DialogFormGroupConfig, - DialogFormValueMap, DialogsFormData } from './dialogs-form.service'; @@ -38,14 +34,14 @@ export class DialogsFormComponent { public title: string; public fields: DialogField[]; - public modalForm: DialogFormGroup; + public modalForm: FormGroup; passwordVisibility = new Map(); isSpinnerOk = true; errorMessage = ''; dialogListRef: MatDialogRef; disableIfInvalid = false; - private markFormAsTouched(control: DialogFormGroup | FormArray) { + private markFormAsTouched(control: FormGroup | FormArray) { const controls = control instanceof FormGroup ? Object.values(control.controls) : control.controls; controls.forEach(innerControl => { innerControl.markAsTouched(); @@ -66,9 +62,9 @@ export class DialogsFormComponent { ) { if (this.data && this.data.formGroup) { this.modalForm = this.data.formGroup instanceof FormGroup ? - this.data.formGroup as DialogFormGroup : - this.fb.group>( - this.data.formGroup as DialogFormGroupConfig, + this.data.formGroup : + this.fb.group( + this.data.formGroup, this.data.formOptions || {} ); this.title = this.data.title; @@ -83,7 +79,7 @@ export class DialogsFormComponent { } } - onSubmit(mForm: DialogFormGroup, dialog: MatDialogRef) { + onSubmit(mForm: FormGroup, dialog: MatDialogRef) { if (!mForm.valid) { this.markFormAsTouched(mForm); return; diff --git a/src/app/shared/dialogs/dialogs-form.service.ts b/src/app/shared/dialogs/dialogs-form.service.ts index 2b2c02fd1e..1e71a4c38f 100644 --- a/src/app/shared/dialogs/dialogs-form.service.ts +++ b/src/app/shared/dialogs/dialogs-form.service.ts @@ -25,7 +25,8 @@ export type DialogFieldType = | 'dialog' | 'date' | 'time' - | 'toggle'; + | 'toggle' + | string; export interface DialogField { name: TName; @@ -106,11 +107,9 @@ export class DialogsFormService { width: '600px', autoFocus: autoFocus }); - if (formGroup instanceof FormGroup) { - dialogRef.componentInstance.modalForm = formGroup; - } else { - dialogRef.componentInstance.modalForm = this.fb.group>(formGroup); - } + dialogRef.componentInstance.modalForm = formGroup instanceof FormGroup + ? formGroup + : this.fb.group(formGroup); dialogRef.componentInstance.title = title; dialogRef.componentInstance.fields = fields; return dialogRef.afterClosed() as Observable; diff --git a/src/app/surveys/surveys.component.ts b/src/app/surveys/surveys.component.ts index d3a38b2d43..2cce3f87a2 100644 --- a/src/app/surveys/surveys.component.ts +++ b/src/app/surveys/surveys.component.ts @@ -412,7 +412,7 @@ export class SurveysComponent implements OnInit, AfterViewInit, OnDestroy { this.submissionsService.exportSubmissionsPdf(survey, 'survey', options, this.teamId || this.routeTeamId || ''); }, formOptions: { - validator: (ac: FormGroup) => + validators: (ac: FormGroup) => Object.values(ac.controls).some(control => control.value) ? null : { required: true } } } From 0fa20627466bfcf2de886e29f0468eb36cd7c0e1 Mon Sep 17 00:00:00 2001 From: Saniya <37302318+Saby-Bishops@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:46:57 -0600 Subject: [PATCH 3/3] Relax dialog form group config typing --- .../shared/dialogs/dialogs-form.component.ts | 1 + src/app/shared/dialogs/dialogs-form.service.ts | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/app/shared/dialogs/dialogs-form.component.ts b/src/app/shared/dialogs/dialogs-form.component.ts index c6650f38b1..d6192897d9 100644 --- a/src/app/shared/dialogs/dialogs-form.component.ts +++ b/src/app/shared/dialogs/dialogs-form.component.ts @@ -11,6 +11,7 @@ import { DialogsListComponent } from './dialogs-list.component'; import { UserService } from '../user.service'; import { DialogField, + DialogFormValueMap, DialogsFormData } from './dialogs-form.service'; diff --git a/src/app/shared/dialogs/dialogs-form.service.ts b/src/app/shared/dialogs/dialogs-form.service.ts index 1e71a4c38f..9e6d7b1fdf 100644 --- a/src/app/shared/dialogs/dialogs-form.service.ts +++ b/src/app/shared/dialogs/dialogs-form.service.ts @@ -57,10 +57,24 @@ type DialogControlConfig = | FormControl | FormGroup | FormArray - | [TValue | FormControlState, (ValidatorFn | ValidatorFn[] | null)?, (AsyncValidatorFn | AsyncValidatorFn[] | null)?]; + | [ + TValue | FormControlState, + (ValidatorFn | ValidatorFn[] | null)?, + (AsyncValidatorFn | AsyncValidatorFn[] | null)? + ] + | Array< + | TValue + | FormControlState + | ValidatorFn + | ValidatorFn[] + | AsyncValidatorFn + | AsyncValidatorFn[] + >; export type DialogFormGroupConfig = { - [K in keyof T]: DialogControlConfig; + [K in keyof T]?: DialogControlConfig; +} & { + [key: string]: unknown; }; export type DialogFormControls = {