Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UFAL/Enhanced type-bind feature #714

Merged
merged 5 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/pagenotfound/pagenotfound.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ <h2><small>{{"404.page-not-found" | translate}}</small></h2>
<p class="text-center">
<a routerLink="/home" class="btn btn-primary">{{"404.link.home-page" | translate}}</a>
</p>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class DsDynamicTypeBindRelationService {
throw new Error(`FormControl ${model.id} cannot depend on itself`);
}

const bindModel: DynamicFormControlModel = this.formBuilderService.getTypeBindModel();
const bindModel: DynamicFormControlModel = this.formBuilderService.getTypeBindModel(rel?.id);

if (model && !models.some((modelElement) => modelElement === bindModel)) {
models.push(bindModel);
Expand Down Expand Up @@ -96,7 +96,7 @@ export class DsDynamicTypeBindRelationService {
// like relation group component and submission section form component).
// This model (DynamicRelationGroupModel) contains eg. mandatory field, formConfiguration, relationFields,
// submission scope, form/section type and other high level properties
const bindModel: any = this.formBuilderService.getTypeBindModel();
const bindModel: any = this.formBuilderService.getTypeBindModel(condition?.id);

let values: string[];
let bindModelValue = bindModel.value;
Expand Down
5 changes: 3 additions & 2 deletions src/app/shared/form/builder/form-builder.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -912,8 +912,9 @@ describe('FormBuilderService test suite', () => {
});

it(`should request the ${typeFieldProp} property and set value "dc_type"`, () => {
const typeValue = service.getTypeField();
expect(configSpy.findByPropertyName).toHaveBeenCalledTimes(1);
const typeValue = service.getTypeField('dc.type');
// Two times because the first time is when the service is created and the second time is when the method is called
expect(configSpy.findByPropertyName).toHaveBeenCalledTimes(2);
expect(configSpy.findByPropertyName).toHaveBeenCalledWith(typeFieldProp);
expect(typeValue).toEqual('dc_type');
});
Expand Down
161 changes: 109 additions & 52 deletions src/app/shared/form/builder/form-builder.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
isNotEmpty,
isNotNull,
isNotUndefined,
isNull
isNull, isUndefined
} from '../../empty.util';
import { DynamicQualdropModel } from './ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model';
import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model';
Expand All @@ -49,11 +49,20 @@ import {
COMPLEX_GROUP_SUFFIX,
DynamicComplexModel
} from './ds-dynamic-form-ui/models/ds-dynamic-complex.model';
import { FormRowModel } from '../../../core/config/models/config-submission-form.model';

/**
* The key for the default type bind field. {'default': 'dc_type'}
*/
export const TYPE_BIND_DEFAULT = 'default';

@Injectable()
export class FormBuilderService extends DynamicFormService {

private typeBindModel: DynamicFormControlModel;
/**
* This map contains the type bind model
*/
private typeBindModel: Map<string,DynamicFormControlModel>;

/**
* This map contains the active forms model
Expand All @@ -68,7 +77,7 @@ export class FormBuilderService extends DynamicFormService {
/**
* This is the field to use for type binding
*/
private typeField: string;
private typeFields: Map<string, string>;

constructor(
componentService: DynamicFormComponentService,
Expand All @@ -79,6 +88,10 @@ export class FormBuilderService extends DynamicFormService {
super(componentService, validationService);
this.formModels = new Map();
this.formGroups = new Map();
this.typeFields = new Map();
this.typeBindModel = new Map();

this.typeFields.set(TYPE_BIND_DEFAULT, 'dc_type');
// If optional config service was passed, perform an initial set of type field (default dc_type) for type binds
if (hasValue(this.configService)) {
this.setTypeBindFieldFromConfig();
Expand All @@ -96,55 +109,76 @@ export class FormBuilderService extends DynamicFormService {
return {$event, context, control: control, group: group, model: model, type};
}

getTypeBindModel() {
return this.typeBindModel;
/**
* Get the type bind model associated to the `type-bind`
*
* @param typeBingField the special `<type-bind field=..>`
* @returns the default (dc_type) type bind model or the one associated to the `type-bind` field
*/
getTypeBindModel(typeBingField: string): DynamicFormControlModel {
let typeBModelKey = this.typeFields.get(typeBingField);
if (isUndefined(typeBModelKey)) {
typeBModelKey = this.typeFields.get(TYPE_BIND_DEFAULT);
}
return this.typeBindModel.get(typeBModelKey);
}

setTypeBindModel(model: DynamicFormControlModel) {
this.typeBindModel = model;
this.typeBindModel.set(model.id, model);
}

findById(id: string, groupModel: DynamicFormControlModel[], arrayIndex = null): DynamicFormControlModel | null {
findById(id: string | string[], groupModel: DynamicFormControlModel[], arrayIndex = null): DynamicFormControlModel | null {

let result = null;
const findByIdFn = (findId: string, findGroupModel: DynamicFormControlModel[], findArrayIndex): void => {
const findByIdFn = (findId: string | string [], findGroupModel: DynamicFormControlModel[], findArrayIndex): void => {

for (const controlModel of findGroupModel) {

if (controlModel.id === findId) {

if (this.isArrayGroup(controlModel) && isNotNull(findArrayIndex)) {
result = (controlModel as DynamicFormArrayModel).get(findArrayIndex);
} else {
result = controlModel;
}
break;
const findIdArray = [];
// If the id is NOT an array, push it into array because we need to iterate over the array
if (!Array.isArray(findId)) {
findIdArray.push(findId);
} else {
findIdArray.push(...findId);
}

if (this.isConcatGroup(controlModel)) {
if (controlModel.id.match(new RegExp(findId + CONCAT_GROUP_SUFFIX))) {
result = (controlModel as DynamicConcatModel);
for (const findIdIt of findIdArray) {
if (controlModel.id === findIdIt) {

if (this.isArrayGroup(controlModel) && isNotNull(findArrayIndex)) {
result = (controlModel as DynamicFormArrayModel).get(findArrayIndex);
} else {
result = controlModel;
}
break;
}
}

if (this.isComplexGroup(controlModel)) {
const regex = new RegExp(findId + COMPLEX_GROUP_SUFFIX);
if (controlModel.id.match(regex)) {
result = (controlModel as DynamicComplexModel);
break;
if (this.isConcatGroup(controlModel)) {
if (controlModel.id.match(new RegExp(findIdIt + CONCAT_GROUP_SUFFIX))) {
result = (controlModel as DynamicConcatModel);
break;
}
}
}

if (this.isGroup(controlModel)) {
findByIdFn(findId, (controlModel as DynamicFormGroupModel).group, findArrayIndex);
}
if (this.isComplexGroup(controlModel)) {
const regex = new RegExp(findIdIt + COMPLEX_GROUP_SUFFIX);
if (controlModel.id.match(regex)) {
result = (controlModel as DynamicComplexModel);
break;
}
}

if (this.isGroup(controlModel)) {
findByIdFn(findIdIt, (controlModel as DynamicFormGroupModel).group, findArrayIndex);
}

if (this.isArrayGroup(controlModel)
&& (isNull(findArrayIndex) || (controlModel as DynamicFormArrayModel).size > (findArrayIndex))) {
const index = (isNull(findArrayIndex)) ? 0 : findArrayIndex;
findByIdFn(findId, (controlModel as DynamicFormArrayModel).get(index).group, index);
if (this.isArrayGroup(controlModel)
&& (isNull(findArrayIndex) || (controlModel as DynamicFormArrayModel).size > (findArrayIndex))) {
const index = (isNull(findArrayIndex)) ? 0 : findArrayIndex;
findByIdFn(findIdIt, (controlModel as DynamicFormArrayModel).get(index).group, index);
}
}

}
};

Expand Down Expand Up @@ -297,9 +331,9 @@ export class FormBuilderService extends DynamicFormService {
let rows: DynamicFormControlModel[] = [];
const rawData = typeof json === 'string' ? JSON.parse(json, parseReviver) : json;
if (rawData.rows && !isEmpty(rawData.rows)) {
rawData.rows.forEach((currentRow) => {
rawData.rows.forEach((currentRow: FormRowModel) => {
const rowParsed = this.rowParser.parse(submissionId, currentRow, scopeUUID, sectionData, submissionScope,
readOnly, this.getTypeField());
readOnly);
if (isNotNull(rowParsed)) {
if (Array.isArray(rowParsed)) {
rows = rows.concat(rowParsed);
Expand All @@ -311,7 +345,7 @@ export class FormBuilderService extends DynamicFormService {
}

if (hasNoValue(typeBindModel)) {
typeBindModel = this.findById(this.typeField, rows);
typeBindModel = this.findById(Array.from(this.typeFields.values()), rows);
}

if (hasValue(typeBindModel)) {
Expand Down Expand Up @@ -517,36 +551,59 @@ export class FormBuilderService extends DynamicFormService {
/**
* Get the type bind field from config
*/
setTypeBindFieldFromConfig(): void {
setTypeBindFieldFromConfig(metadataField: string = null): void {
this.configService.findByPropertyName('submit.type-bind.field').pipe(
getFirstCompletedRemoteData(),
).subscribe((remoteData: any) => {
// make sure we got a success response from the backend
if (!remoteData.hasSucceeded) {
this.typeField = 'dc_type';
return;
}
// Read type bind value from response and set if non-empty
const typeFieldConfig = remoteData.payload.values[0];
if (isEmpty(typeFieldConfig)) {
this.typeField = 'dc_type';
} else {
this.typeField = typeFieldConfig.replace(/\./g, '_');
}

// All cfg property values
const typeFieldConfigValues = remoteData.payload.values;
let typeFieldConfigValue = '';
// Iterate over each config property value
typeFieldConfigValues.forEach((typeFieldConfig: string) => {
// Check if the typeFieldConfig contains the '=>' delimiter
if (typeFieldConfig.includes('=>')) {
// Split the typeFieldConfig into parts based on the delimiter
const [metadataFieldConfigPart, valuePart] = typeFieldConfig.split('=>');
// Process only custom type-bind fields
if (isNotEmpty(valuePart)) {
// Replace '.' with '_' in the valuePart
const normalizedValuePart = valuePart.replace(/\./g, '_');

// Set the value in the typeFields map
this.typeFields.set(metadataFieldConfigPart, normalizedValuePart);

if (metadataFieldConfigPart === metadataField) {
typeFieldConfigValue = valuePart;
}
}
} else {
// If no delimiter is found, use the entire typeFieldConfig as the default value
typeFieldConfigValue = typeFieldConfig;
}

// Always update the typeFields map with the default value, normalized
this.typeFields.set(TYPE_BIND_DEFAULT, typeFieldConfigValue.replace(/\./g, '_'));
});
});
}

/**
* Get type field. If the type isn't already set, and a ConfigurationDataService is provided, set (with subscribe)
* from back end. Otherwise, get/set a default "dc_type" value
* from back end. Otherwise, get/set a default "dc_type" value or specific value from the typeFields map.
*/
getTypeField(): string {
if (hasValue(this.configService) && hasNoValue(this.typeField)) {
this.setTypeBindFieldFromConfig();
} else if (hasNoValue(this.typeField)) {
this.typeField = 'dc_type';
getTypeField(metadataField: string): string {
if (hasValue(this.configService) && isEmpty(this.typeFields.values())) {
this.setTypeBindFieldFromConfig(metadataField);
} else if (hasNoValue(this.typeFields.get(TYPE_BIND_DEFAULT))) {
this.typeFields.set(TYPE_BIND_DEFAULT, 'dc_type');
}
return this.typeField;

return this.typeFields.get(metadataField) || this.typeFields.get(TYPE_BIND_DEFAULT);
}

}
22 changes: 11 additions & 11 deletions src/app/shared/form/builder/parsers/row-parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,87 +339,87 @@ describe('RowParser test suite', () => {
it('should return a DynamicRowGroupModel object', () => {
const parser = new RowParser(undefined);

const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly, typeField);
const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly);

expect(rowModel instanceof DynamicRowGroupModel).toBe(true);
});

it('should return a row with three fields', () => {
const parser = new RowParser(undefined);

const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly, typeField);
const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly);

expect((rowModel as DynamicRowGroupModel).group.length).toBe(3);
});

it('should return a DynamicRowArrayModel object', () => {
const parser = new RowParser(undefined);

const rowModel = parser.parse(submissionId, row2, scopeUUID, initFormValues, submissionScope, readOnly, typeField);
const rowModel = parser.parse(submissionId, row2, scopeUUID, initFormValues, submissionScope, readOnly);

expect(rowModel instanceof DynamicRowArrayModel).toBe(true);
});

it('should return a row that contains only scoped fields', () => {
const parser = new RowParser(undefined);

const rowModel = parser.parse(submissionId, row3, scopeUUID, initFormValues, submissionScope, readOnly, typeField);
const rowModel = parser.parse(submissionId, row3, scopeUUID, initFormValues, submissionScope, readOnly);

expect((rowModel as DynamicRowGroupModel).group.length).toBe(1);
});

it('should be able to parse a dropdown combo field', () => {
const parser = new RowParser(undefined);

const rowModel = parser.parse(submissionId, row4, scopeUUID, initFormValues, submissionScope, readOnly, typeField);
const rowModel = parser.parse(submissionId, row4, scopeUUID, initFormValues, submissionScope, readOnly);

expect(rowModel).toBeDefined();
});

it('should be able to parse a lookup-name field', () => {
const parser = new RowParser(undefined);

const rowModel = parser.parse(submissionId, row5, scopeUUID, initFormValues, submissionScope, readOnly, typeField);
const rowModel = parser.parse(submissionId, row5, scopeUUID, initFormValues, submissionScope, readOnly);

expect(rowModel).toBeDefined();
});

it('should be able to parse a list field', () => {
const parser = new RowParser(undefined);

const rowModel = parser.parse(submissionId, row6, scopeUUID, initFormValues, submissionScope, readOnly, typeField);
const rowModel = parser.parse(submissionId, row6, scopeUUID, initFormValues, submissionScope, readOnly);

expect(rowModel).toBeDefined();
});

it('should be able to parse a date field', () => {
const parser = new RowParser(undefined);

const rowModel = parser.parse(submissionId, row7, scopeUUID, initFormValues, submissionScope, readOnly, typeField);
const rowModel = parser.parse(submissionId, row7, scopeUUID, initFormValues, submissionScope, readOnly);

expect(rowModel).toBeDefined();
});

it('should be able to parse a tag field', () => {
const parser = new RowParser(undefined);

const rowModel = parser.parse(submissionId, row8, scopeUUID, initFormValues, submissionScope, readOnly, typeField);
const rowModel = parser.parse(submissionId, row8, scopeUUID, initFormValues, submissionScope, readOnly);

expect(rowModel).toBeDefined();
});

it('should be able to parse a textarea field', () => {
const parser = new RowParser(undefined);

const rowModel = parser.parse(submissionId, row9, scopeUUID, initFormValues, submissionScope, readOnly, typeField);
const rowModel = parser.parse(submissionId, row9, scopeUUID, initFormValues, submissionScope, readOnly);

expect(rowModel).toBeDefined();
});

it('should be able to parse a group field', () => {
const parser = new RowParser(undefined);

const rowModel = parser.parse(submissionId, row10, scopeUUID, initFormValues, submissionScope, readOnly, typeField);
const rowModel = parser.parse(submissionId, row10, scopeUUID, initFormValues, submissionScope, readOnly);

expect(rowModel).toBeDefined();
});
Expand Down
Loading
Loading