diff --git a/projects/rero/ng-core/src/lib/record/editor/editor.component.html b/projects/rero/ng-core/src/lib/record/editor/editor.component.html index a26bbc80..834d1a82 100644 --- a/projects/rero/ng-core/src/lib/record/editor/editor.component.html +++ b/projects/rero/ng-core/src/lib/record/editor/editor.component.html @@ -18,7 +18,7 @@
- @if (rootFormlyConfig) { + @if (rootField) { {{ title || recordType | ucfirst | translate }} diff --git a/projects/rero/ng-core/src/lib/record/editor/editor.component.ts b/projects/rero/ng-core/src/lib/record/editor/editor.component.ts index 199baeb2..6150dca7 100644 --- a/projects/rero/ng-core/src/lib/record/editor/editor.component.ts +++ b/projects/rero/ng-core/src/lib/record/editor/editor.component.ts @@ -16,7 +16,7 @@ */ import { Location } from '@angular/common'; import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewEncapsulation } from '@angular/core'; -import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; +import { Form, UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core'; import { FormlyJsonschema } from '@ngx-formly/core/json-schema'; @@ -32,9 +32,9 @@ import { AbstractCanDeactivateComponent } from '../../component/abstract-can-dea import { Error } from '../../error/error'; import { RouteCollectionService } from '../../route/route-collection.service'; import { LoggerService } from '../../service/logger.service'; -import { Record } from '../record'; import { RecordUiService } from '../record-ui.service'; import { RecordService } from '../record.service'; +import { JSONSchemaService } from './services/jsonschema.service'; import { processJsonSchema, removeEmptyValues, resolve$ref } from './utils'; import { LoadTemplateFormComponent } from './widgets/load-template-form/load-template-form.component'; import { SaveTemplateFormComponent } from './widgets/save-template-form/save-template-form.component'; @@ -83,7 +83,7 @@ export class EditorComponent extends AbstractCanDeactivateComponent implements O fields: FormlyFieldConfig[]; // root element of the editor - rootFormlyConfig: FormlyFieldConfig; + rootField: FormlyFieldConfig; // list of fields to display in the TOC tocFields$: Observable; @@ -123,15 +123,6 @@ export class EditorComponent extends AbstractCanDeactivateComponent implements O // Config for resource private _resourceConfig: any; - // list of custom validators - private _customValidators = [ - 'valueAlreadyExists', - 'uniqueValueKeysInObject', - 'numberOfSpecificValuesInObject', - 'dateMustBeGreaterThan', - 'dateMustBeLessThan' - ]; - // list of fields to be hidden private _hiddenFields: FormlyFieldConfig[] = []; @@ -148,16 +139,6 @@ export class EditorComponent extends AbstractCanDeactivateComponent implements O return this.editorSettings.longMode; } - // Editor edit mode - public get editMode(): boolean { - return this.pid ? true : false; - } - - // Editor root field - public get rootField(): FormlyFieldConfig { - return this.rootFormlyConfig; - } - // Editor function public get editorComponent(): () => EditorComponent { return () => this; @@ -188,7 +169,8 @@ export class EditorComponent extends AbstractCanDeactivateComponent implements O protected location: Location, protected modalService: BsModalService, protected routeCollectionService: RouteCollectionService, - protected loggerService: LoggerService + protected loggerService: LoggerService, + protected jsonschemaService: JSONSchemaService ) { super(); this.form = new UntypedFormGroup({}); @@ -458,39 +440,23 @@ export class EditorComponent extends AbstractCanDeactivateComponent implements O this.clearHiddenFields(); // form configuration + const editorConfig = { + pid: this.pid, + longMode: this.longMode, + recordType: this.recordType + } const fields = [ this.formlyJsonschema.toFieldConfig(this.schema, { // post process JSONSChema7 to FormlyFieldConfig conversion map: (field: FormlyFieldConfig, jsonSchema: JSONSchema7) => { /**** additional JSONSchema configurations *******/ - - // initial population of arrays with a minItems constraints - if (jsonSchema.minItems && !jsonSchema.hasOwnProperty('default')) { - field.defaultValue = new Array(jsonSchema.minItems); - } - // If 'format' is defined into the jsonSchema, use it as props to try a validation on this field. - // See: `email.validator.ts` file - if (jsonSchema.format) { - field.props.type = jsonSchema.format; - } - - if (jsonSchema?.widget?.formlyConfig) { - const { props } = jsonSchema.widget.formlyConfig; - - if (props) { - this._setSimpleOptions(field, props); - this._setValidation(field, props); - this._setRemoteSelectOptions(field, props); - this._setRemoteTypeahead(field, props); - } - } - // Add editor component function on the field - field.props.editorComponent = this.editorComponent; - + field = this.jsonschemaService.processField(field, jsonSchema); + field.props.editorConfig = editorConfig; + field.props.getRoot = (() => this.rootField); + field.props.setHide = ((field: FormlyFieldConfig, value: boolean) => this.setHide(field, value)); if (this._resourceConfig != null && this._resourceConfig.formFieldMap) { return this._resourceConfig.formFieldMap(field, jsonSchema); } - return field; } }) @@ -504,7 +470,7 @@ export class EditorComponent extends AbstractCanDeactivateComponent implements O if (this.fields) { this.title = this.fields[0].props?.label; this.description = this.fields[0].props?.description; - this.rootFormlyConfig = this.fields[0]; + this.rootField = this.fields[0]; } } @@ -775,147 +741,25 @@ export class EditorComponent extends AbstractCanDeactivateComponent implements O * Hide the given formly field. * @param field - FormlyFieldConfig, the field to hide */ - hide(field: FormlyFieldConfig): void { - field.hide = true; - if (this.isRoot(field.parent)) { - this.addHiddenField(field); + setHide(field: FormlyFieldConfig, value: boolean): void { + if (value) { + if (field.parent.props.isRoot) { + this.addHiddenField(field); + } + } else { + this.removeHiddenField(field); + // scroll at the right position + // to avoid: Expression has changed after it was checked + // See: https://blog.angular-university.io/angular-debugging/ + // wait that the component is present in the DOM + setTimeout(() => this.setFieldFocus(field, true)); } + field.hide = value; } /********************* Private ***************************************/ - /** - * Populate a select options with a remote API call. - * @param field formly field config - * @param formOptions JSONSchema object - */ - private _setRemoteSelectOptions( - field: FormlyFieldConfig, - formOptions: any - ): void { - if (formOptions.remoteOptions && formOptions.remoteOptions.type) { - field.type = 'select'; - field.hooks = { - ...field.hooks, - afterContentInit: (f: FormlyFieldConfig) => { - const recordType = formOptions.remoteOptions.type; - const query = formOptions.remoteOptions.query || ''; - f.props.options = this.recordService - .getRecords(recordType, query, 1, RecordService.MAX_REST_RESULTS_SIZE) - .pipe( - map((data: Record) => - data.hits.hits.map((record: any) => { - return { - label: formOptions.remoteOptions.labelField && formOptions.remoteOptions.labelField in record.metadata - ? record.metadata[formOptions.remoteOptions.labelField] - : record.metadata.name, - value: this.apiService.getRefEndpoint( - recordType, - record.id - ) - }; - }) - ) - ); - } - }; - } - } - /** - * Store the remote typeahead options. - * @param field formly field config - * @param formOptions JSONSchema object - */ - private _setRemoteTypeahead( - field: FormlyFieldConfig, - formOptions: any - ): void { - if (formOptions.remoteTypeahead && formOptions.remoteTypeahead.type) { - field.type = 'remoteTypeahead'; - field.props = { - ...field.props, - ...{ remoteTypeahead: formOptions.remoteTypeahead } - }; - } - } - - /** - * - * @param field formly field config - * @param formOptions JSONSchema object - */ - private _setValidation(field: FormlyFieldConfig, formOptions: any): void { - if (formOptions.validation) { - // custom validation messages - // TODO: use widget instead - const { messages } = formOptions.validation; - if (messages) { - if (!field.validation) { - field.validation = {}; - } - if (!field.validation.messages) { - field.validation.messages = {}; - } - for (const key of Object.keys(messages)) { - const msg = messages[key]; - // add support of key with or without Message suffix (required == requiredMessage), - // this is useful for backend translation extraction - field.validation.messages[key.replace(/Message$/, '')] = (error, f: FormlyFieldConfig) => - // translate the validation messages coming from the JSONSchema - // TODO: need to remove `as any` once it is fixed in ngx-formly v.5.7.2 - this.translateService.stream(msg) as any; - } - } - - // store the custom validators config - field.props.customValidators = {}; - if (formOptions.validation && formOptions.validation.validators) { - for (const customValidator of this._customValidators) { - const validatorConfig = formOptions.validation.validators[customValidator]; - if (validatorConfig != null) { - field.props.customValidators[customValidator] = validatorConfig; - } - } - } - - if (formOptions.validation.validators) { - // validators: add validator with expressions - // TODO: use widget - const validatorsKey = Object.keys(formOptions.validation.validators); - validatorsKey.map(validatorKey => { - const validator = formOptions.validation.validators[validatorKey]; - if ('expression' in validator && 'message' in validator) { - const { expression } = validator; - const expressionFn = Function('formControl', `return ${expression};`); - const validatorExpression = { - expression: (fc: UntypedFormControl) => expressionFn(fc), - // translate the validation message coming form the JSONSchema - message: this.translateService.stream(validator.message) - }; - field.validators = field.validators !== undefined ? field.validators : {}; - field.validators[validatorKey] = validatorExpression; - } - }); - } - } - } - - /** - * Convert JSONSchema form options to formly field options. - * @param field formly field config - * @param formOptions JSONSchema object - */ - private _setSimpleOptions(field: FormlyFieldConfig, formOptions: any): void { - // some fields should not submit the form when enter key is pressed - if (field.props.doNotSubmitOnEnter != null) { - field.props.keydown = (f: FormlyFieldConfig, event?: any) => { - if (event.key === 'Enter') { - event.preventDefault(); - } - }; - } - } /** * Handle form error diff --git a/projects/rero/ng-core/src/lib/record/editor/extensions.ts b/projects/rero/ng-core/src/lib/record/editor/extensions.ts index 977b3162..4c9dc6c1 100644 --- a/projects/rero/ng-core/src/lib/record/editor/extensions.ts +++ b/projects/rero/ng-core/src/lib/record/editor/extensions.ts @@ -24,6 +24,7 @@ import { isObservable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { RecordService } from '../record.service'; import { isEmpty, removeEmptyValues } from './utils'; +import { FormlyFieldConfigCache, FormlyValueChangeEvent } from '@ngx-formly/core/lib/models'; export class NgCoreFormlyExtension { // Types to apply horizontal wrapper on @@ -44,8 +45,8 @@ export class NgCoreFormlyExtension { * Constructor * @params _recordService - ng core record service */ - constructor(private _recordService: RecordService) {} - + constructor(private _recordService: RecordService) { + } /** * prePopulate Formly hook * @param field - FormlyFieldConfig @@ -103,8 +104,7 @@ export class NgCoreFormlyExtension { ]; } - const editorComponent = field.props?.editorComponent; - if (field.props && editorComponent && editorComponent().longMode) { + if (field?.props?.editorConfig?.longMode) { // add automatically a card wrapper for the first level fields const { parent } = field; if (parent && parent.props && parent.props.isRoot === true && !field.wrappers.includes('card')) { @@ -206,11 +206,10 @@ export class NgCoreFormlyExtension { * @param field - FormlyFieldConfig */ private _hideEmptyField(field: FormlyFieldConfig): void { - // find the root field in the form tree - if (!field.props?.editorComponent) { + if (!field.props?.editorConfig) { return; } - const {rootField, editMode, longMode} = field.props.editorComponent(); + const {pid, longMode} = field.props?.editorConfig; if ( // only in longMode else it will not be possible to unhide a field !longMode @@ -241,11 +240,11 @@ export class NgCoreFormlyExtension { // do not hide field has been already manipulated && field.hide === undefined) // in edition empty fields should be hidden - || (editMode === true + || (pid != null // only during the editor initialization - && !rootField?.formControl?.touched) + && !field?.props?.getRoot()?.formControl?.touched) ) { - field.props.editorComponent().hide(field); + field.props.setHide ? field.props.setHide(field, true): field.hide = true; } } } @@ -261,15 +260,14 @@ export class NgCoreFormlyExtension { const customValidators = field.props.customValidators ? field.props.customValidators : {}; // asyncValidators: valueAlreadyExists if (customValidators.valueAlreadyExists) { - const { filter, limitToValues, remoteRecordType, term } = customValidators.valueAlreadyExists; - const { editorComponent } = field.props; + const { filter, limitToValues, term } = customValidators.valueAlreadyExists; field.asyncValidators = { validation: [ (control: UntypedFormControl) => { return this._recordService.uniqueValue( field, - remoteRecordType ? remoteRecordType : editorComponent().recordType, - editorComponent().pid, + field.props.editorConfig.recordType, + field.props.editorConfig.pid, term ? term : null, limitToValues ? limitToValues : [], filter ? filter : null @@ -413,6 +411,7 @@ export class TranslateExtension implements FormlyExtension { * It translates the label, the description and the placeholder. * @param field formly field config */ + prePopulate(field: FormlyFieldConfig): void { const props = field.props || {}; diff --git a/projects/rero/ng-core/src/lib/record/editor/formly/primeng/input/src/input.type.ts b/projects/rero/ng-core/src/lib/record/editor/formly/primeng/input/src/input.type.ts index 5658daed..e2d6562f 100644 --- a/projects/rero/ng-core/src/lib/record/editor/formly/primeng/input/src/input.type.ts +++ b/projects/rero/ng-core/src/lib/record/editor/formly/primeng/input/src/input.type.ts @@ -36,6 +36,7 @@ export interface NgCoreFormlyInputFieldConfig extends FormlyFieldConfig { @if (props.type !== 'number') { . + */ +import { Injectable, inject } from '@angular/core'; +import { UntypedFormControl } from '@angular/forms'; +import { FormlyFieldConfig } from '@ngx-formly/core'; +import { TranslateService } from '@ngx-translate/core'; +import { map } from 'rxjs'; +import { ApiService } from '../../../api/api.service'; +import { Record } from '../../record'; +import { RecordService } from '../../record.service'; +import { JSONSchema7 } from '../editor.component'; + +@Injectable({ + providedIn: 'root' +}) +export class JSONSchemaService { + + private translateService = inject(TranslateService) + + private recordService = inject(RecordService) + + private apiService = inject(ApiService) + + // list of custom validators + private customValidators = [ + 'valueAlreadyExists', + 'uniqueValueKeysInObject', + 'numberOfSpecificValuesInObject', + 'dateMustBeGreaterThan', + 'dateMustBeLessThan' + ]; + + processField(field: FormlyFieldConfig, jsonSchema: JSONSchema7) { + // initial population of arrays with a minItems constraints + if (jsonSchema.minItems && !jsonSchema.hasOwnProperty('default')) { + field.defaultValue = new Array(jsonSchema.minItems); + } + // If 'format' is defined into the jsonSchema, use it as props to try a validation on this field. + // See: `email.validator.ts` file + if (jsonSchema.format) { + field.props.type = jsonSchema.format; + } + + if (jsonSchema?.widget?.formlyConfig) { + const { props } = jsonSchema.widget.formlyConfig; + + if (props) { + this.setSimpleOptions(field, props); + this.setValidation(field, props); + this.setRemoteSelectOptions(field, props); + this.setRemoteTypeahead(field, props); + } + } + + return field; + } + + /** + * Populate a select options with a remote API call. + * @param field formly field config + * @param formOptions JSONSchema object + */ + protected setRemoteSelectOptions( + field: FormlyFieldConfig, + formOptions: any + ): void { + if (formOptions.remoteOptions && formOptions.remoteOptions.type) { + field.type = 'select'; + field.hooks = { + ...field.hooks, + afterContentInit: (f: FormlyFieldConfig) => { + const recordType = formOptions.remoteOptions.type; + const query = formOptions.remoteOptions.query || ''; + f.props.options = this.recordService + .getRecords(recordType, query, 1, RecordService.MAX_REST_RESULTS_SIZE) + .pipe( + map((data: Record) => + data.hits.hits.map((record: any) => { + return { + label: formOptions.remoteOptions.labelField && formOptions.remoteOptions.labelField in record.metadata + ? record.metadata[formOptions.remoteOptions.labelField] + : record.metadata.name, + value: this.apiService.getRefEndpoint( + recordType, + record.id + ) + }; + }) + ) + ); + } + }; + } + } + + /** + * Store the remote typeahead options. + * @param field formly field config + * @param formOptions JSONSchema object + */ + protected setRemoteTypeahead( + field: FormlyFieldConfig, + formOptions: any + ): void { + if (formOptions.remoteTypeahead && formOptions.remoteTypeahead.type) { + field.type = 'remoteTypeahead'; + field.props = { + ...field.props, + ...{ remoteTypeahead: formOptions.remoteTypeahead } + }; + } + } + + /** + * + * @param field formly field config + * @param formOptions JSONSchema object + */ + protected setValidation(field: FormlyFieldConfig, formOptions: any): void { + if (formOptions.validation) { + // custom validation messages + // TODO: use widget instead + const { messages } = formOptions.validation; + if (messages) { + if (!field.validation) { + field.validation = {}; + } + if (!field.validation.messages) { + field.validation.messages = {}; + } + for (const key of Object.keys(messages)) { + const msg = messages[key]; + // add support of key with or without Message suffix (required == requiredMessage), + // this is useful for backend translation extraction + field.validation.messages[key.replace(/Message$/, '')] = (error, f: FormlyFieldConfig) => + // translate the validation messages coming from the JSONSchema + // TODO: need to remove `as any` once it is fixed in ngx-formly v.5.7.2 + this.translateService.stream(msg) as any; + } + } + + // store the custom validators config + field.props.customValidators = {}; + if (formOptions.validation && formOptions.validation.validators) { + for (const customValidator of this.customValidators) { + const validatorConfig = formOptions.validation.validators[customValidator]; + if (validatorConfig != null) { + field.props.customValidators[customValidator] = validatorConfig; + } + } + } + + if (formOptions.validation.validators) { + // validators: add validator with expressions + // TODO: use widget + const validatorsKey = Object.keys(formOptions.validation.validators); + validatorsKey.map(validatorKey => { + const validator = formOptions.validation.validators[validatorKey]; + if ('expression' in validator && 'message' in validator) { + const { expression } = validator; + const expressionFn = Function('formControl', `return ${expression};`); + const validatorExpression = { + expression: (fc: UntypedFormControl) => expressionFn(fc), + // translate the validation message coming form the JSONSchema + message: this.translateService.stream(validator.message) + }; + field.validators = field.validators !== undefined ? field.validators : {}; + field.validators[validatorKey] = validatorExpression; + } + }); + } + } + } + + /** + * Convert JSONSchema form options to formly field options. + * @param field formly field config + * @param formOptions JSONSchema object + */ + protected setSimpleOptions(field: FormlyFieldConfig, formOptions: any): void { + // some fields should not submit the form when enter key is pressed + if (field.props.doNotSubmitOnEnter != null) { + field.props.keydown = (f: FormlyFieldConfig, event?: any) => { + if (event.key === 'Enter') { + event.preventDefault(); + } + }; + } + } +} diff --git a/projects/rero/ng-core/src/lib/record/editor/type/object-type/object-type.component.html b/projects/rero/ng-core/src/lib/record/editor/type/object-type/object-type.component.html index 523deeac..41ed9767 100644 --- a/projects/rero/ng-core/src/lib/record/editor/type/object-type/object-type.component.html +++ b/projects/rero/ng-core/src/lib/record/editor/type/object-type/object-type.component.html @@ -20,7 +20,7 @@ }
-
+
@if (showError && formControl.errors) {