diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index 8703a3af51..256e657474 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -7,7 +7,7 @@ import { DYNAMIC_FORM_CONTROL_TYPE_GROUP, DYNAMIC_FORM_CONTROL_TYPE_INPUT, DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP, - DynamicFormArrayModel, + DynamicFormArrayModel, DynamicFormControlEvent, DynamicFormControlModel, DynamicFormGroupModel, DynamicFormService, DynamicFormValidationService, @@ -26,6 +26,7 @@ import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-inpu import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model'; import { isNgbDateStruct } from '../../date.util'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-ui/ds-dynamic-form-constants'; +import { CONCAT_GROUP_SUFFIX, DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model'; @Injectable() export class FormBuilderService extends DynamicFormService { @@ -54,6 +55,15 @@ export class FormBuilderService extends DynamicFormService { break; } + if (this.isConcatGroup(controlModel)) { + const concatGroupId = controlModel.id.replace(CONCAT_GROUP_SUFFIX, ''); + // if (concatGroupId === findId) { + if (concatGroupId.includes(findId)) { + result = (controlModel as DynamicConcatModel).group[0]; + break; + } + } + if (this.isGroup(controlModel)) { findByIdFn(findId, (controlModel as DynamicFormGroupModel).group, findArrayIndex); } @@ -247,6 +257,10 @@ export class FormBuilderService extends DynamicFormService { return model && ((model as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && (model as any).isCustomGroup === true); } + isConcatGroup(model: DynamicFormControlModel): boolean { + return this.isCustomGroup(model) && (model.id.indexOf(CONCAT_GROUP_SUFFIX) !== -1); + } + isRowGroup(model: DynamicFormControlModel): boolean { return model && ((model as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && (model as any).isRowGroup === true); } @@ -303,4 +317,76 @@ export class FormBuilderService extends DynamicFormService { return (tempModel.id !== tempModel.name) ? tempModel.name : tempModel.id; } + /** + * Calculate the metadata list related to the event. + * @param event + */ + getMetadataIdsFromEvent(event: DynamicFormControlEvent): string[] { + + let model = event.model; + while (model.parent) { + model = model.parent as any; + } + + const iterateControlModels = (findGroupModel: DynamicFormControlModel[], controlModelIndex: number = 0): void => { + let iterateResult = Object.create({}); + + // Iterate over all group's controls + for (const controlModel of findGroupModel) { + + if (this.isRowGroup(controlModel) && !this.isCustomOrListGroup(controlModel)) { + iterateResult = mergeWith(iterateResult, iterateControlModels((controlModel as DynamicFormGroupModel).group)); + continue; + } + + if (this.isGroup(controlModel) && !this.isCustomOrListGroup(controlModel)) { + iterateResult[controlModel.name] = iterateControlModels((controlModel as DynamicFormGroupModel).group); + continue; + } + + if (this.isRowArrayGroup(controlModel)) { + for (const arrayItemModel of (controlModel as DynamicRowArrayModel).groups) { + iterateResult = mergeWith(iterateResult, iterateControlModels(arrayItemModel.group, arrayItemModel.index)); + } + continue; + } + + if (this.isArrayGroup(controlModel)) { + iterateResult[controlModel.name] = []; + for (const arrayItemModel of (controlModel as DynamicFormArrayModel).groups) { + iterateResult[controlModel.name].push(iterateControlModels(arrayItemModel.group, arrayItemModel.index)); + } + continue; + } + + let controlId; + // Get the field's name + if (this.isQualdropGroup(controlModel)) { + // If is instance of DynamicQualdropModel take the qualdrop id as field's name + controlId = (controlModel as DynamicQualdropModel).qualdropId; + } else { + controlId = controlModel.name; + } + + if (this.isRelationGroup(controlModel)) { + const values = (controlModel as DynamicRelationGroupModel).getGroupValue(); + values.forEach((groupValue, groupIndex) => { + Object.keys(groupValue).forEach((key) => { + iterateResult[key] = true; + }); + }); + } else { + iterateResult[controlId] = true; + } + + } + + return iterateResult; + }; + + const result = iterateControlModels([model]); + + return result; + } + } diff --git a/src/app/shared/form/form.actions.ts b/src/app/shared/form/form.actions.ts index 3eb3fb2716..76d9fef253 100644 --- a/src/app/shared/form/form.actions.ts +++ b/src/app/shared/form/form.actions.ts @@ -13,6 +13,7 @@ import { type } from '../ngrx/type'; export const FormActionTypes = { FORM_INIT: type('dspace/form/FORM_INIT'), FORM_CHANGE: type('dspace/form/FORM_CHANGE'), + FORM_ADDITIONAL: type('dspace/form/FORM_ADDITIONAL'), FORM_REMOVE: type('dspace/form/FORM_REMOVE'), FORM_STATUS_CHANGE: type('dspace/form/FORM_STATUS_CHANGE'), FORM_ADD_ERROR: type('dspace/form/FORM_ADD_ERROR'), @@ -27,6 +28,7 @@ export class FormInitAction implements Action { formId: string; formData: any; valid: boolean; + formAdditional: any; }; /** @@ -39,8 +41,8 @@ export class FormInitAction implements Action { * @param valid * the Form validation status */ - constructor(formId: string, formData: any, valid: boolean) { - this.payload = {formId, formData, valid}; + constructor(formId: string, formData: any, valid: boolean, formAdditional?: any) { + this.payload = {formId, formData, valid, formAdditional}; } } @@ -52,7 +54,7 @@ export class FormChangeAction implements Action { }; /** - * Create a new FormInitAction + * Create a new FormChangeAction * * @param formId * the Form's ID @@ -64,6 +66,26 @@ export class FormChangeAction implements Action { } } +export class FormSetAdditionalAction implements Action { + type = FormActionTypes.FORM_ADDITIONAL; + payload: { + formId: string; + additionalData: any; + }; + + /** + * Create a new FormSetAdditionalAction + * + * @param formId + * the Form's ID + * @param additionalData + * the additionalData Object + */ + constructor(formId: string, additionalData: any) { + this.payload = {formId, additionalData}; + } +} + export class FormRemoveAction implements Action { type = FormActionTypes.FORM_REMOVE; payload: { @@ -147,6 +169,7 @@ export class FormClearErrorsAction implements Action { */ export type FormAction = FormInitAction | FormChangeAction + | FormSetAdditionalAction | FormRemoveAction | FormStatusChangeAction | FormAddError diff --git a/src/app/shared/form/form.reducer.ts b/src/app/shared/form/form.reducer.ts index 1d44375c0d..8a5c2a57b8 100644 --- a/src/app/shared/form/form.reducer.ts +++ b/src/app/shared/form/form.reducer.ts @@ -5,7 +5,7 @@ import { FormChangeAction, FormClearErrorsAction, FormInitAction, FormRemoveAction, - FormRemoveErrorAction, + FormRemoveErrorAction, FormSetAdditionalAction, FormStatusChangeAction } from './form.actions'; import { hasValue } from '../empty.util'; @@ -21,6 +21,7 @@ export interface FormEntry { data: any; valid: boolean; errors: FormError[]; + additional: any; } export interface FormState { @@ -40,6 +41,10 @@ export function formReducer(state = initialState, action: FormAction): FormState return changeDataForm(state, action as FormChangeAction); } + case FormActionTypes.FORM_ADDITIONAL: { + return additionalData(state, action as FormSetAdditionalAction); + } + case FormActionTypes.FORM_REMOVE: { return removeForm(state, action as FormRemoveAction); } @@ -127,7 +132,8 @@ function initForm(state: FormState, action: FormInitAction): FormState { const formState = { data: action.payload.formData, valid: action.payload.valid, - errors: [] + errors: [], + additional: action.payload.formAdditional }; if (!hasValue(state[action.payload.formId])) { return Object.assign({}, state, { @@ -212,3 +218,30 @@ function removeForm(state: FormState, action: FormRemoveAction): FormState { return state; } } + +/** + * Compute the additional data state of the form. New touched fields are merged with the previous ones. + * @param state + * @param action + */ +function additionalData(state: FormState, action: FormSetAdditionalAction): FormState { + if (hasValue(state[action.payload.formId])) { + + const newState = Object.assign({}, state); + + const newAdditional = newState[action.payload.formId].additional ? {...newState[action.payload.formId].additional} : {}; + + const newTouchedValue = newAdditional.touched ? {...newAdditional.touched, + ...action.payload.additionalData.touched} : { ...action.payload.additionalData.touched}; + newAdditional.touched = newTouchedValue; + + newState[action.payload.formId] = Object.assign({}, newState[action.payload.formId], { + additional: newAdditional + } + ); + + return newState; + } else { + return state; + } +} diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index 2b0815a40e..6d208b08d2 100644 --- a/src/app/shared/form/form.service.ts +++ b/src/app/shared/form/form.service.ts @@ -7,13 +7,13 @@ import { select, Store } from '@ngrx/store'; import { AppState } from '../../app.reducer'; import { formObjectFromIdSelector } from './selectors'; import { FormBuilderService } from './builder/form-builder.service'; -import { DynamicFormControlModel } from '@ng-dynamic-forms/core'; +import {DynamicFormControlEvent, DynamicFormControlModel} from '@ng-dynamic-forms/core'; import { isEmpty, isNotUndefined } from '../empty.util'; import { uniqueId } from 'lodash'; import { FormChangeAction, FormInitAction, - FormRemoveAction, FormRemoveErrorAction, + FormRemoveAction, FormRemoveErrorAction, FormSetAdditionalAction, FormStatusChangeAction } from './form.actions'; import { FormEntry } from './form.reducer'; @@ -51,6 +51,18 @@ export class FormService { ); } + /** + * Method to retrieve form's additional data from state + */ + public getFormAdditionalData(formId: string): Observable { + return this.store.pipe( + select(formObjectFromIdSelector(formId)), + filter((state) => isNotUndefined(state)), + map((state) => state.additional), + distinctUntilChanged() + ); + } + /** * Method to retrieve form's errors from state */ @@ -149,8 +161,8 @@ export class FormService { return (environment.form.validatorMap.hasOwnProperty(validator)) ? environment.form.validatorMap[validator] : validator; } - public initForm(formId: string, model: DynamicFormControlModel[], valid: boolean) { - this.store.dispatch(new FormInitAction(formId, this.formBuilderService.getValueFromModel(model), valid)); + public initForm(formId: string, model: DynamicFormControlModel[], valid: boolean, additional?: any) { + this.store.dispatch(new FormInitAction(formId, this.formBuilderService.getValueFromModel(model), valid, additional)); } public setStatusChanged(formId: string, valid: boolean) { @@ -169,6 +181,11 @@ export class FormService { this.store.dispatch(new FormChangeAction(formId, this.formBuilderService.getValueFromModel(model))); } + public setTouched(formId: string, model: DynamicFormControlModel[], event: DynamicFormControlEvent) { + const ids = this.formBuilderService.getMetadataIdsFromEvent(event); + this.store.dispatch(new FormSetAdditionalAction(formId, { touched: ids})); + } + public removeError(formId: string, eventModelId: string, fieldIndex: number) { this.store.dispatch(new FormRemoveErrorAction(formId, eventModelId, fieldIndex)); } diff --git a/src/app/submission/objects/submission-objects.actions.ts b/src/app/submission/objects/submission-objects.actions.ts index 2eed4f2c92..6fe42a149a 100644 --- a/src/app/submission/objects/submission-objects.actions.ts +++ b/src/app/submission/objects/submission-objects.actions.ts @@ -206,6 +206,7 @@ export class UpdateSectionDataAction implements Action { sectionId: string; data: WorkspaceitemSectionDataType; errors: SubmissionSectionError[]; + metadata: string[]; }; /** @@ -219,12 +220,15 @@ export class UpdateSectionDataAction implements Action { * the section's data * @param errors * the section's errors + * @param metadata + * the section's metadata */ constructor(submissionId: string, sectionId: string, data: WorkspaceitemSectionDataType, - errors: SubmissionSectionError[]) { - this.payload = { submissionId, sectionId, data, errors }; + errors: SubmissionSectionError[], + metadata?: string[]) { + this.payload = { submissionId, sectionId, data, errors, metadata }; } } diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index 09a6f47104..3a25d79330 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -292,7 +292,7 @@ export class SubmissionObjectEffects { return item$.pipe( map((item: Item) => item.metadata), filter((metadata) => !isEqual(action.payload.data, metadata)), - map((metadata: any) => new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, metadata, action.payload.errors)) + map((metadata: any) => new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, metadata, action.payload.errors, action.payload.metadata)) ); } else { return observableOf(new UpdateSectionDataSuccessAction()); diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts index 098160c737..6f7098532e 100644 --- a/src/app/submission/objects/submission-objects.reducer.ts +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -85,6 +85,11 @@ export interface SubmissionSectionObject { */ enabled: boolean; + /** + * The list of the metadata ids of the section. + */ + metadata: string[]; + /** * The section data object */ @@ -660,7 +665,8 @@ function updateSectionData(state: SubmissionObjectState, action: UpdateSectionDa [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { enabled: true, data: action.payload.data, - errors: action.payload.errors + errors: action.payload.errors, + metadata: reduceSectionMetadata(action.payload.metadata, state[ action.payload.submissionId ].sections [ action.payload.sectionId ].metadata) }) }) }) @@ -670,6 +676,22 @@ function updateSectionData(state: SubmissionObjectState, action: UpdateSectionDa } } +/** + * Updates the state of the section metadata only when a new value is provided. + * Keep the existent otherwise. + * @param newMetadata + * @param oldMetadata + */ +function reduceSectionMetadata(newMetadata: string[], oldMetadata: string[]) { + if (newMetadata) { + return newMetadata; + } + if (oldMetadata) { + return [...oldMetadata]; + } + return undefined; +} + /** * Set a section state. * diff --git a/src/app/submission/sections/form/section-form.component.html b/src/app/submission/sections/form/section-form.component.html index 166e52675b..ab549315ed 100644 --- a/src/app/submission/sections/form/section-form.component.html +++ b/src/app/submission/sections/form/section-form.component.html @@ -2,7 +2,9 @@ { + if (this.sectionMetadata.includes(key)) { + sectionDataToCheck[key] = sectionData[key]; + } + }) + const diffResult = []; // compare current form data state with section data retrieved from store - const diffObj = difference(sectionData, this.formData); + const diffObj = difference(sectionDataToCheck, this.formData); // iterate over differences to check whether they are actually different Object.keys(diffObj) .forEach((key) => { diffObj[key].forEach((value) => { - if (value.hasOwnProperty('value')) { + if (value.hasOwnProperty('value') && !isEmpty(value.value)) { diffResult.push(value); } }); @@ -262,6 +288,9 @@ export class SubmissionSectionformComponent extends SectionModelComponent { sectionData, this.submissionService.getSubmissionScope() ); + this.formBuilderService.enrichWithAdditionalData(this.formModel, this.formAdditionalData); + this.sectionMetadata = this.sectionService.computeSectionConfiguredMetadata(this.formConfig); + } catch (e) { const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString(); const sectionError: SubmissionSectionError = { @@ -283,15 +312,19 @@ export class SubmissionSectionformComponent extends SectionModelComponent { */ updateForm(sectionData: WorkspaceitemSectionFormObject, errors: SubmissionSectionError[]): void { - if (hasValue(sectionData) && !isEqual(sectionData, this.sectionData.data)) { + if (isNotEmpty(sectionData) && !isEqual(sectionData, this.sectionData.data)) { this.sectionData.data = sectionData; - this.isUpdating = true; - this.formModel = null; - this.cdr.detectChanges(); - this.initForm(sectionData); - this.checksForErrors(errors); - this.isUpdating = false; - this.cdr.detectChanges(); + if (this.hasMetadataEnrichment(sectionData)) { + this.isUpdating = true; + this.formModel = null; + this.cdr.detectChanges(); + this.initForm(sectionData); + this.checksForErrors(errors); + this.isUpdating = false; + this.cdr.detectChanges(); + } else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) { + this.checksForErrors(errors); + } } else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) { this.checksForErrors(errors); } @@ -308,6 +341,9 @@ export class SubmissionSectionformComponent extends SectionModelComponent { this.formService.isFormInitialized(this.formId).pipe( find((status: boolean) => status === true && !this.isUpdating)) .subscribe(() => { + + // TODO: filter these errors to only those that had been touched + this.sectionService.checkSectionErrors(this.submissionId, this.sectionData.id, this.formId, errors, this.sectionData.errors); this.sectionData.errors = errors; this.cdr.detectChanges(); @@ -328,6 +364,12 @@ export class SubmissionSectionformComponent extends SectionModelComponent { this.formData = formData; }), + this.formService.getFormAdditionalData(this.formId).pipe( + distinctUntilChanged()) + .subscribe((formAdditional) => { + this.formAdditionalData = formAdditional; + }), + /** * Subscribe to section state */ @@ -375,6 +417,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent { * the [[DynamicFormControlEvent]] emitted */ onFocus(event: DynamicFormControlEvent): void { + this.isFocused = true; const value = this.formOperationsService.getFieldValueFromChangeEvent(event); const path = this.formBuilderService.getPath(event.model); if (this.formBuilderService.hasMappedGroupValue(event.model)) { @@ -386,6 +429,17 @@ export class SubmissionSectionformComponent extends SectionModelComponent { } } + /** + * Method called when a form dfBlur event is fired. + * + * @param event + * the [[DynamicFormControlEvent]] emitted + */ + + onBlur(event: DynamicFormControlEvent): void { + this.isFocused = false; + } + /** * Method called when a form remove event is fired. * Dispatch form operations based on changes. diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts index 5aa3c1d3ea..895bde13ec 100644 --- a/src/app/submission/sections/sections.service.ts +++ b/src/app/submission/sections/sections.service.ts @@ -8,7 +8,7 @@ import { ScrollToConfigOptions, ScrollToService } from '@nicky-lenaers/ngx-scrol import { isEqual } from 'lodash'; import { SubmissionState } from '../submission.reducers'; -import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util'; import { DisableSectionAction, EnableSectionAction, @@ -36,6 +36,8 @@ import { SubmissionService } from '../submission.service'; import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model'; import { SectionsType } from './sections-type'; import { normalizeSectionData } from '../../core/submission/submission-response-parsing.service'; +import { SubmissionFormsModel } from '../../core/config/models/config-submission-forms.model'; +import { parseReviver } from '@ng-dynamic-forms/core'; /** * A service that provides methods used in submission process. @@ -335,8 +337,10 @@ export class SectionsService { * The section data * @param errors * The list of section errors + * @param metadata + * The section metadata */ - public updateSectionData(submissionId: string, sectionId: string, data: WorkspaceitemSectionDataType, errors: SubmissionSectionError[] = []) { + public updateSectionData(submissionId: string, sectionId: string, data: WorkspaceitemSectionDataType, errors: SubmissionSectionError[] = [], metadata?: string[]) { if (isNotEmpty(data)) { const isAvailable$ = this.isSectionAvailable(submissionId, sectionId); const isEnabled$ = this.isSectionEnabled(submissionId, sectionId); @@ -345,7 +349,7 @@ export class SectionsService { take(1), filter(([available, enabled]: [boolean, boolean]) => available)) .subscribe(([available, enabled]: [boolean, boolean]) => { - this.store.dispatch(new UpdateSectionDataAction(submissionId, sectionId, data, errors)); + this.store.dispatch(new UpdateSectionDataAction(submissionId, sectionId, data, errors, metadata)); }); } } @@ -377,4 +381,30 @@ export class SectionsService { public setSectionStatus(submissionId: string, sectionId: string, status: boolean) { this.store.dispatch(new SectionStatusChangeAction(submissionId, sectionId, status)); } + + /** + * Compute the list of selectable metadata for the section configuration. + * @param formConfig + */ + public computeSectionConfiguredMetadata(formConfig: string | SubmissionFormsModel): string[] { + const metadata = []; + const rawData = typeof formConfig === 'string' ? JSON.parse(formConfig, parseReviver) : formConfig; + if (rawData.rows && !isEmpty(rawData.rows)) { + rawData.rows.forEach((currentRow) => { + if (currentRow.fields && !isEmpty(currentRow.fields)) { + currentRow.fields.forEach((field) => { + if (field.selectableMetadata && !isEmpty(field.selectableMetadata)) { + field.selectableMetadata.forEach((selectableMetadata) => { + if (!metadata.includes(selectableMetadata.metadata)) { + metadata.push(selectableMetadata.metadata); + } + }) + } + }) + } + }); + } + return metadata; + } + }