Skip to content

Commit

Permalink
editor: create a new editor service
Browse files Browse the repository at this point in the history
* Creates a specific JSONSchema service to be usable by custom formly editor.
* Removes dependencies between formly extensions and the record core editor.

Co-Authored-by: Bertrand Zuchuat <[email protected]>
Co-Authored-by: Johnny Mariéthoz <[email protected]>
  • Loading branch information
jma and Garfield-fr committed Jul 31, 2024
1 parent 75dc9f4 commit 54f191d
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 248 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,5 @@
<ng-core-error [error]="error"></ng-core-error>
}
<ng-template ngCoreRecordDetail></ng-template>
@if (record && filesEnabled && updateStatus && updateStatus.can) {
<ng-core-record-files [type]="type" [pid]="record.id"></ng-core-record-files>
}

</div>
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<div class="row editor">
<!-- Editor title and editor actions buttons -->
<div class="header py-2 mb-3 col-12 border-bottom">
@if (rootFormlyConfig) {
@if (rootField) {
<legend>
<span [tooltip]="description">
{{ title || recordType | ucfirst | translate }}
Expand Down
212 changes: 28 additions & 184 deletions projects/rero/ng-core/src/lib/record/editor/editor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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<any>;
Expand Down Expand Up @@ -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[] = [];

Expand All @@ -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;
Expand Down Expand Up @@ -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({});
Expand Down Expand Up @@ -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;
}
})
Expand All @@ -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];
}
}

Expand Down Expand Up @@ -776,147 +742,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
Expand Down
27 changes: 13 additions & 14 deletions projects/rero/ng-core/src/lib/record/editor/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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')) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -243,11 +242,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;
}
}
}
Expand All @@ -263,15 +262,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
Expand Down Expand Up @@ -415,6 +413,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 || {};

Expand Down
Loading

0 comments on commit 54f191d

Please sign in to comment.