From f39f104d4594638b7c426a15403eb8282d852541 Mon Sep 17 00:00:00 2001 From: tomasz hanaj <12088991+tomaszhanaj@users.noreply.github.com> Date: Thu, 20 Feb 2025 09:22:17 +0100 Subject: [PATCH] AAE-30864 Refactored services to accept injected validators (#10660) * [AAE-30864] refactored services to accept injected validators * [AAE-30864] updated documentation, applied pr comments --- docs/core/models/form-model.md | 114 ++++++++++++++++++ docs/core/services/form.service.md | 29 +++++ .../services/form-cloud.service.md | 30 +++++ .../src/lib/form/components/mock/form.mock.ts | 6 + .../widgets/core/form.model.spec.ts | 9 +- .../components/widgets/core/form.model.ts | 10 +- .../lib/form/services/form.service.spec.ts | 18 ++- .../src/lib/form/services/form.service.ts | 13 +- .../form/services/form-cloud.service.spec.ts | 20 ++- .../lib/form/services/form-cloud.service.ts | 15 ++- 10 files changed, 249 insertions(+), 15 deletions(-) create mode 100644 docs/core/models/form-model.md diff --git a/docs/core/models/form-model.md b/docs/core/models/form-model.md new file mode 100644 index 0000000000..f72dc6a945 --- /dev/null +++ b/docs/core/models/form-model.md @@ -0,0 +1,114 @@ +--- +Title: Form model +Added: 2025-02-19 +Status: Active +Last reviewed: 2025-02-19 +--- + +# [Form model](../../../lib/core/src/lib/form/components/widgets/core/form.model.ts "Defined in form.model.ts") + +Contains the value and metadata for a form. + +## Properties + +| Name | Type | Default | Description | +| ---- | ---- | ------- | ----------- | +|UNSET_TASK_NAME| string | 'Nameless task'|static property| +|SAVE_OUTCOME| string | '$save'|static property| +|COMPLETE_OUTCOME| string | '$complete'|static property| +|START_PROCESS_OUTCOME| string | '$startProcess'|static property| +|id| string | number||id of form| +|name| string||form name| +|taskId| string||task id| +|confirmMessage| ConfirmMessage||confirmation message| +|taskName |string| FormModel.UNSET_TASK_NAME|task name| +|processDefinitionId| string||Process definition id | +|selectedOutcome| string||selected outcome| +|enableFixedSpace| boolean||should fixed space be enabled| +|displayMode| any||which mode should be displayed| +|fieldsCache| FormFieldModel[] | []|cache for fields| +|json| any||json with form configuration| +|nodeId| string||id of node| +|values| FormValues | {}|form values| +|tabs| TabModel[] | []|tabs| +|fields| (ContainerModel | FormFieldModel)[] | []|form fields| +|outcomes| FormOutcomeModel[] | []|set of outcomes| +|fieldValidators| FormFieldValidator[] | []|validators for fields| +|customFieldTemplates| FormFieldTemplates | {}|custom templates| +|theme?| ThemeModel||theme| +|className| string||class name| +|readOnly | false||is form read only| +|isValid | true||is form valid| +|processVariables| ProcessVariableModel[] | []|process variables| +|variables| FormVariableModel[] | []|variables| + +## Methods + +- `onFormFieldChanged(field: FormFieldModel)` + Triggered when field is changed. Validates field and calls FormService +- `validateForm(): void` + Validates entire form and all form fields. +- `validateField(field: FormFieldModel): void` + Validates a specific form field, triggers form validation. +- `parseRootFields(json: any): (ContainerModel | FormFieldModel)[]` + Activiti supports 3 types of root fields: container|group|dynamic-table +- `loadData(formValues: FormValues)` + Loads external data and overrides field values. Typically used when form definition and form data coming from different sources +- `canOverrideFieldValueWithProcessValue(field: FormFieldModel, variableId: string, formValues: FormValues): boolean` + Checks if field value can be overriden with process value +- `isDefined(value: string): boolean` + Check if variable is defined +- `getFormVariable(identifier: string): FormVariableModel` + Returns a form variable that matches the identifier. +- `getDefaultFormVariableValue(identifier: string): any` + Returns a value of the form variable that matches the identifier. Provides additional conversion of types (date, boolean). +- `getProcessVariableValue(name: string): any` + Returns a process variable value. When mapping a process variable with a form variable the mapping is already resolved by the rest API with the name of variables.formVariableName. +- `parseValue(type: string, value: any): any` + Parse value data and boolean +- `hasTabs(): boolean` + Check if form has tabs +- `hasFields(): boolean` + Check if there are any fields +- `hasOutcomes(): boolean` + Check if form has outcomes +- `getFieldById(fieldId: string): FormFieldModel` + Find field by id +- `getFormFields(filterTypes?: string[]): FormFieldModel[]` + Get form fields +- `processFields(fields: (ContainerModel | FormFieldModel)[], formFieldModel: FormFieldModel[]): void` + Process fields +- `isContainerField(field: ContainerModel | FormFieldModel): field is ContainerModel` + Check if it is container +- `isSectionField(field: ContainerModel | FormFieldModel): field is FormFieldModel` + Check if it is section +- `handleSectionField(section: FormFieldModel, formFieldModel: FormFieldModel[]): void` + Handle section +- `handleContainerField(container: ContainerModel, formFieldModel: FormFieldModel[]): void` + Handle container +- `handleSingleField(field: FormFieldModel, formFieldModel: FormFieldModel[]): void` + Handle single field +- `filterFieldsByType(fields: FormFieldModel[], types?: string[]): FormFieldModel[]` + Filter fields based on type +- `markAsInvalid(): void` + Set form as invalid +- `parseOutcomes()` + Parse outcomes from json +- `addValuesNotPresent(valuesToSetIfNotPresent: FormValues)` + Set values if they are not present +- `isValidDropDown(key: string): boolean` + Validates dropdown +- `setNodeIdValueForViewersLinkedToUploadWidget(linkedUploadWidgetContentSelected: UploadWidgetContentLinkModel)` + Set node id +- `changeFieldVisibility(fieldId: string, visibility: boolean): void` + Changes field visibility +- `changeFieldDisabled(fieldId: string, disabled: boolean): void` + Changes disabled status of field +- `changeFieldRequired(fieldId: string, required: boolean): void` + Changes required status of field +- `changeFieldValue(fieldId: string, value: any): void` + Changes field value +- `changeVariableValue(variableId: string, value: any): void` + Changes variable value +- `loadInjectedFieldValidators(injectedFieldValidators: FormFieldValidator[]): void` + Checks it there are any injectedValidators and adds them to the array of field validators. diff --git a/docs/core/services/form.service.md b/docs/core/services/form.service.md index c54816ce4a..cfe220eaae 100644 --- a/docs/core/services/form.service.md +++ b/docs/core/services/form.service.md @@ -191,3 +191,32 @@ class MyComponent { - `handleError(error: any):`[`Observable`](http://reactivex.io/documentation/observable.html)`` Reports an error message. - `error` - Data object with optional \`message\` and \`status\` fields for the error + +### Properties +| Name | Type | Description | +| ---- | --------- | ----------- | +| fieldValidators | FormFieldValidator[] | Array of Field Validators injected with token and then passed to FormModel | + + +### Inject Preference service + +Token: [`FORM_SERVICE_FIELD_VALIDATORS_TOKEN`] +A DI token that allows to inject additional form field validators. + +```ts +import { NgModule } from '@angular/core'; +import { FORM_SERVICE_FIELD_VALIDATORS_TOKEN } from '@alfresco/adf-core'; + +@NgModule({ + imports: [ + ...Import Required Modules + ], + providers: [ + { + provide: FORM_SERVICE_FIELD_VALIDATORS_TOKEN, + useValue: [new AdditionalFormFieldValidator()] + } + ] +}) +export class ExampleModule {} +``` diff --git a/docs/process-services-cloud/services/form-cloud.service.md b/docs/process-services-cloud/services/form-cloud.service.md index c36a9c1556..c30d5bf2ad 100644 --- a/docs/process-services-cloud/services/form-cloud.service.md +++ b/docs/process-services-cloud/services/form-cloud.service.md @@ -92,6 +92,36 @@ class MyComponent { - _values:_ [`FormValues`](../../../lib/core/src/lib/form/components/widgets/core/form-values.ts) - [Form](../../../lib/process-services/src/lib/task-list/models/form.model.ts) values object - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TaskDetailsCloudModel`](../../../lib/process-services-cloud/src/lib/task/models/task-details-cloud.model.ts)`>` - Updated task details +### Properties +| Name | Type | Description | +| ---- | --------- | ----------- | +| fieldValidators | FormFieldValidator[] | Array of Field Validators injected with token and then passed to FormModel | + + +### Inject Preference service + +Token: [`FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN`] +A DI token that allows to inject additional form field validators. + +```ts +import { NgModule } from '@angular/core'; +import { FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN } from '@alfresco/adf-process-services-cloud'; + +@NgModule({ + imports: [ + ...Import Required Modules + ], + providers: [ + { + provide: FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN, + useValue: [new AdditionalFormFieldValidator()] + } + ] +}) +export class ExampleModule {} +``` + + ## See also - [Form cloud component](../components/form-cloud.component.md) diff --git a/lib/core/src/lib/form/components/mock/form.mock.ts b/lib/core/src/lib/form/components/mock/form.mock.ts index dea178f187..6316666eaf 100644 --- a/lib/core/src/lib/form/components/mock/form.mock.ts +++ b/lib/core/src/lib/form/components/mock/form.mock.ts @@ -1686,3 +1686,9 @@ export const mockFormWithSections = { } } }; + +export const fakeValidatorMock = { + supportedTypes: ['test'], + isSupported: () => true, + validate: () => true +}; diff --git a/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts b/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts index 9c4aa2fa2f..805ce08c20 100644 --- a/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts +++ b/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts @@ -24,7 +24,7 @@ import { FormFieldModel } from './form-field.model'; import { FormOutcomeModel } from './form-outcome.model'; import { FormModel } from './form.model'; import { TabModel } from './tab.model'; -import { fakeMetadataForm, mockDisplayExternalPropertyForm, mockFormWithSections } from '../../mock/form.mock'; +import { fakeMetadataForm, mockDisplayExternalPropertyForm, mockFormWithSections, fakeValidatorMock } from '../../mock/form.mock'; import { CoreTestingModule } from '../../../../testing'; import { TestBed } from '@angular/core/testing'; @@ -433,6 +433,13 @@ describe('FormModel', () => { expect(FORM_FIELD_VALIDATORS.length).toBe(defaultLength); }); + it('should include injected field validators', () => { + const form = new FormModel({}, null, false, formService, undefined, [fakeValidatorMock]); + const defaultLength = FORM_FIELD_VALIDATORS.length; + + expect(form.fieldValidators.length).toBe(defaultLength + 1); + }); + describe('variables', () => { let form: FormModel; diff --git a/lib/core/src/lib/form/components/widgets/core/form.model.ts b/lib/core/src/lib/form/components/widgets/core/form.model.ts index 5dfe1c8b6a..800bafd480 100644 --- a/lib/core/src/lib/form/components/widgets/core/form.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form.model.ts @@ -85,7 +85,7 @@ export class FormModel implements ProcessFormModel { tabs: TabModel[] = []; fields: (ContainerModel | FormFieldModel)[] = []; outcomes: FormOutcomeModel[] = []; - fieldValidators: FormFieldValidator[] = [...FORM_FIELD_VALIDATORS]; + fieldValidators: FormFieldValidator[] = []; customFieldTemplates: FormFieldTemplates = {}; theme?: ThemeModel; @@ -100,7 +100,8 @@ export class FormModel implements ProcessFormModel { formValues?: FormValues, readOnly: boolean = false, protected formService?: FormValidationService, - enableFixedSpace?: boolean + enableFixedSpace?: boolean, + injectedFieldValidators?: FormFieldValidator[] ) { this.readOnly = readOnly; this.json = json; @@ -133,6 +134,7 @@ export class FormModel implements ProcessFormModel { this.parseOutcomes(); } + this.loadInjectedFieldValidators(injectedFieldValidators); this.validateForm(); } @@ -501,4 +503,8 @@ export class FormModel implements ProcessFormModel { variable.value = value; } } + + private loadInjectedFieldValidators(injectedFieldValidators: FormFieldValidator[]): void { + this.fieldValidators = injectedFieldValidators ? [...FORM_FIELD_VALIDATORS, ...injectedFieldValidators] : [...FORM_FIELD_VALIDATORS]; + } } diff --git a/lib/core/src/lib/form/services/form.service.spec.ts b/lib/core/src/lib/form/services/form.service.spec.ts index f3dfa36f8a..20eb60d41a 100644 --- a/lib/core/src/lib/form/services/form.service.spec.ts +++ b/lib/core/src/lib/form/services/form.service.spec.ts @@ -17,15 +17,23 @@ import { TestBed } from '@angular/core/testing'; import { formModelTabs } from '../../mock'; -import { FormService } from './form.service'; +import { FORM_SERVICE_FIELD_VALIDATORS_TOKEN, FormService } from './form.service'; import { CoreTestingModule } from '../../testing'; +import { FORM_FIELD_VALIDATORS, FormFieldValidator } from '../public-api'; + +const fakeValidator = { + supportedTypes: ['test'], + isSupported: () => true, + validate: () => true +} as FormFieldValidator; describe('Form service', () => { let service: FormService; beforeEach(() => { TestBed.configureTestingModule({ - imports: [CoreTestingModule] + imports: [CoreTestingModule], + providers: [{ provide: FORM_SERVICE_FIELD_VALIDATORS_TOKEN, useValue: [fakeValidator] }] }); service = TestBed.inject(FormService); }); @@ -36,5 +44,11 @@ describe('Form service', () => { const formParsed = service.parseForm(formModelTabs); expect(formParsed).toBeDefined(); }); + + it('should return form with injected field validators', () => { + expect(formModelTabs.formRepresentation.formDefinition).toBeDefined(); + const formParsed = service.parseForm(formModelTabs); + expect(formParsed.fieldValidators).toEqual([...FORM_FIELD_VALIDATORS, fakeValidator]); + }); }); }); diff --git a/lib/core/src/lib/form/services/form.service.ts b/lib/core/src/lib/form/services/form.service.ts index 034668bc55..38dbe187e1 100644 --- a/lib/core/src/lib/form/services/form.service.ts +++ b/lib/core/src/lib/form/services/form.service.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; +import { Inject, Injectable, InjectionToken, Optional } from '@angular/core'; import { Subject } from 'rxjs'; import { ContentLinkModel } from '../components/widgets/core/content-link.model'; import { FormOutcomeEvent } from '../components/widgets/core/form-outcome-event.model'; @@ -30,12 +30,15 @@ import { ValidateFormFieldEvent } from '../events/validate-form-field.event'; import { FormValidationService } from './form-validation-service.interface'; import { FormRulesEvent } from '../events/form-rules.event'; import { FormSpinnerEvent } from '../events'; -import { FormFieldModel } from '../components/widgets'; +import { FormFieldModel, FormFieldValidator } from '../components/widgets'; + +export const FORM_SERVICE_FIELD_VALIDATORS_TOKEN = new InjectionToken('FORM_SERVICE_FIELD_VALIDATORS_TOKEN'); @Injectable({ providedIn: 'root' }) export class FormService implements FormValidationService { + private fieldValidators: FormFieldValidator[]; formLoaded = new Subject(); formDataRefreshed = new Subject(); formFieldValueChanged = new Subject(); @@ -59,7 +62,9 @@ export class FormService implements FormValidationService { formRulesEvent = new Subject(); - constructor() {} + constructor(@Optional() @Inject(FORM_SERVICE_FIELD_VALIDATORS_TOKEN) injectedFieldValidators?: FormFieldValidator[]) { + this.fieldValidators = injectedFieldValidators || []; + } /** * Parses JSON data to create a corresponding Form model. @@ -72,7 +77,7 @@ export class FormService implements FormValidationService { */ parseForm(json: any, data?: FormValues, readOnly: boolean = false, fixedSpace?: boolean): FormModel { if (json) { - const form = new FormModel(json, data, readOnly, this, fixedSpace); + const form = new FormModel(json, data, readOnly, this, fixedSpace, this.fieldValidators); if (!json.fields) { form.outcomes = [ new FormOutcomeModel(form, { diff --git a/lib/process-services-cloud/src/lib/form/services/form-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/form/services/form-cloud.service.spec.ts index b9831fc441..05934d63db 100644 --- a/lib/process-services-cloud/src/lib/form/services/form-cloud.service.spec.ts +++ b/lib/process-services-cloud/src/lib/form/services/form-cloud.service.spec.ts @@ -16,10 +16,11 @@ */ import { TestBed } from '@angular/core/testing'; -import { FormCloudService } from './form-cloud.service'; +import { FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN, FormCloudService } from './form-cloud.service'; import { of } from 'rxjs'; import { ProcessServiceCloudTestingModule } from '../../testing/process-service-cloud.testing.module'; import { AdfHttpClient } from '@alfresco/adf-core/api'; +import { FORM_FIELD_VALIDATORS, FormFieldValidator } from '@alfresco/adf-core'; const mockTaskResponseBody = { entry: { id: 'id', name: 'name', formKey: 'form-key' } @@ -27,6 +28,12 @@ const mockTaskResponseBody = { const mockFormResponseBody = { formRepresentation: { id: 'form-id', name: 'task-form', taskId: 'task-id' } }; +const fakeValidator = { + supportedTypes: ['test'], + isSupported: () => true, + validate: () => true +} as FormFieldValidator; + describe('Form Cloud service', () => { let service: FormCloudService; let adfHttpClient: AdfHttpClient; @@ -37,7 +44,8 @@ describe('Form Cloud service', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ProcessServiceCloudTestingModule] + imports: [ProcessServiceCloudTestingModule], + providers: [{ provide: FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN, useValue: [fakeValidator] }] }); service = TestBed.inject(FormCloudService); adfHttpClient = TestBed.inject(AdfHttpClient); @@ -68,6 +76,14 @@ describe('Form Cloud service', () => { expect(result.id).toBe(formId); expect(result.name).toBe('task-form'); }); + + it('should create form with injected validators', () => { + const formId = 'form-id'; + const json = { formRepresentation: { id: formId, name: 'task-form', taskId: 'task-id', formDefinition: {} } }; + const result = service.parseForm(json, undefined, undefined); + expect(result).toBeDefined(); + expect(result.fieldValidators).toEqual([...FORM_FIELD_VALIDATORS, fakeValidator]); + }); }); describe('Task tests', () => { diff --git a/lib/process-services-cloud/src/lib/form/services/form-cloud.service.ts b/lib/process-services-cloud/src/lib/form/services/form-cloud.service.ts index 007bcc818d..4ba62c4622 100644 --- a/lib/process-services-cloud/src/lib/form/services/form-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/form/services/form-cloud.service.ts @@ -15,8 +15,8 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; -import { FormValues, FormModel, FormFieldOption } from '@alfresco/adf-core'; +import { Inject, Injectable, InjectionToken, Optional } from '@angular/core'; +import { FormValues, FormModel, FormFieldOption, FormFieldValidator } from '@alfresco/adf-core'; import { Observable, from, EMPTY } from 'rxjs'; import { expand, map, reduce, switchMap } from 'rxjs/operators'; import { TaskDetailsCloudModel } from '../../task/models/task-details-cloud.model'; @@ -27,18 +27,25 @@ import { FormContent } from '../../services/form-fields.interfaces'; import { FormCloudServiceInterface } from './form-cloud.service.interface'; import { AdfHttpClient } from '@alfresco/adf-core/api'; +export const FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN = new InjectionToken('FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN'); + @Injectable({ providedIn: 'root' }) export class FormCloudService extends BaseCloudService implements FormCloudServiceInterface { private _uploadApi: UploadApi; + private fieldValidators: FormFieldValidator[]; get uploadApi(): UploadApi { this._uploadApi = this._uploadApi ?? new UploadApi(this.apiService.getInstance()); return this._uploadApi; } - constructor(adfHttpClient: AdfHttpClient) { + constructor( + adfHttpClient: AdfHttpClient, + @Optional() @Inject(FORM_CLOUD_SERVICE_FIELD_VALIDATORS_TOKEN) injectedFieldValidators?: FormFieldValidator[] + ) { super(adfHttpClient); + this.fieldValidators = injectedFieldValidators || []; } /** @@ -219,7 +226,7 @@ export class FormCloudService extends BaseCloudService implements FormCloudServi formValues[variable.name] = variable.value; }); - return new FormModel(flattenForm, formValues, readOnly); + return new FormModel(flattenForm, formValues, readOnly, undefined, undefined, this.fieldValidators); } return null; }