From c9f4568b69e8a62289d03f8e505d93f8bea73386 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 16 Nov 2020 16:15:00 +0100 Subject: [PATCH 01/20] [835] Auto-save in new Item Submission form breaks the form Disable autosave when timer is equal to 0 --- src/app/submission/submission.service.spec.ts | 20 +++++++++++++++++++ src/app/submission/submission.service.ts | 4 ++++ 2 files changed, 24 insertions(+) diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index 5816c97bde..14c369581a 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -915,6 +915,13 @@ describe('SubmissionService test suite', () => { }); describe('startAutoSave', () => { + + let environmentAutoSaveTimerOriginalValue; + + beforeEach(() => { + environmentAutoSaveTimerOriginalValue = environment.submission.autosave.timer; + }); + it('should start Auto Save', fakeAsync(() => { const duration = environment.submission.autosave.timer * (1000 * 60); @@ -930,6 +937,19 @@ describe('SubmissionService test suite', () => { sub.unsubscribe(); (service as any).autoSaveSub.unsubscribe(); })); + + it('should not start Auto Save if timer is 0', fakeAsync(() => { + environment.submission.autosave.timer = 0; + + service.startAutoSave('826'); + + expect((service as any).autoSaveSub).toBeUndefined(); + })); + + afterEach(() => { + environment.submission.autosave.timer = environmentAutoSaveTimerOriginalValue; + }) + }); describe('stopAutoSave', () => { diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index c9b1d41b40..728c860ca9 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -562,6 +562,10 @@ export class SubmissionService { */ startAutoSave(submissionId) { this.stopAutoSave(); + if (environment.submission.autosave.timer === 0) { + return; + } + // AUTOSAVE submission // Retrieve interval from config and convert to milliseconds const duration = environment.submission.autosave.timer * (1000 * 60); From 5ae649f7a587a0c320d7056596d9c876d6d0f83e Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 16 Nov 2020 16:15:26 +0100 Subject: [PATCH 02/20] [835] Auto-save in new Item Submission form breaks the form Label Add on Form Array inputs. --- src/app/shared/form/form.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 32ccc3b696..8ac2ca2d23 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -18,7 +18,7 @@ From 6136162a216c631b3f87d8c4ca33fd6b38c1660f Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 16 Nov 2020 18:23:51 +0100 Subject: [PATCH 03/20] [835] Auto-save in new Item Submission form breaks the form Notifications are disabled for submission section savings. --- .../submission-objects.effects.spec.ts | 207 +++++++++++++++++- .../objects/submission-objects.effects.ts | 16 +- 2 files changed, 218 insertions(+), 5 deletions(-) diff --git a/src/app/submission/objects/submission-objects.effects.spec.ts b/src/app/submission/objects/submission-objects.effects.spec.ts index c35968c0a0..7a9d6977e8 100644 --- a/src/app/submission/objects/submission-objects.effects.spec.ts +++ b/src/app/submission/objects/submission-objects.effects.spec.ts @@ -57,9 +57,9 @@ describe('SubmissionObjectEffects test suite', () => { let actions: Observable; let store: StoreMock; - const notificationsServiceStub = new NotificationsServiceStub(); - const submissionServiceStub = new SubmissionServiceStub(); - const submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub(); + let notificationsServiceStub; + let submissionServiceStub; + let submissionJsonPatchOperationsServiceStub; const collectionId: string = mockSubmissionCollectionId; const submissionId: string = mockSubmissionId; const submissionDefinitionResponse: any = mockSubmissionDefinitionResponse; @@ -68,6 +68,11 @@ describe('SubmissionObjectEffects test suite', () => { const submissionState: any = Object.assign({}, mockSubmissionState); beforeEach(() => { + + notificationsServiceStub = new NotificationsServiceStub(); + submissionServiceStub = new SubmissionServiceStub(); + submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub(); + TestBed.configureTestingModule({ imports: [ StoreModule.forRoot({}, storeModuleConfig), @@ -471,6 +476,202 @@ describe('SubmissionObjectEffects test suite', () => { }); + describe('saveSubmissionSectionSuccess$', () => { + + it('should return a UPDATE_SECTION_DATA action for each updated section', () => { + store.nextState({ + submission: { + objects: submissionState + } + } as any); + + const response = [Object.assign({}, mockSubmissionRestResponse[0], { + sections: mockSectionsData, + errors: mockSectionsErrors + })]; + actions = hot('--a-', { + a: { + type: SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS, + payload: { + submissionId: submissionId, + submissionObject: response + } + } + }); + + const errorsList = parseSectionErrors(mockSectionsErrors); + const expected = cold('--(bcd)-', { + b: new UpdateSectionDataAction( + submissionId, + 'traditionalpageone', + mockSectionsData.traditionalpageone as any, + errorsList.traditionalpageone || [] + ), + c: new UpdateSectionDataAction( + submissionId, + 'license', + mockSectionsData.license as any, + errorsList.license || [] + ), + d: new UpdateSectionDataAction( + submissionId, + 'upload', + mockSectionsData.upload as any, + errorsList.upload || [] + ), + }); + + expect(submissionObjectEffects.saveSubmissionSectionSuccess$).toBeObservable(expected); + + }); + + it('should not display a success notification', () => { + store.nextState({ + submission: { + objects: submissionState + } + } as any); + + const response = [Object.assign({}, mockSubmissionRestResponse[0], { + sections: mockSectionsData + })]; + actions = hot('--a-', { + a: { + type: SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS, + payload: { + submissionId: submissionId, + submissionObject: response + } + } + }); + + const expected = cold('--(bcd)-', { + b: new UpdateSectionDataAction( + submissionId, + 'traditionalpageone', + mockSectionsData.traditionalpageone as any, + [] + ), + c: new UpdateSectionDataAction( + submissionId, + 'license', + mockSectionsData.license as any, + [] + ), + d: new UpdateSectionDataAction( + submissionId, + 'upload', + mockSectionsData.upload as any, + [] + ), + }); + + expect(submissionObjectEffects.saveSubmissionSectionSuccess$).toBeObservable(expected); + expect(notificationsServiceStub.success).not.toHaveBeenCalled(); + }); + + it('should not display a warning notification when there are errors', () => { + store.nextState({ + submission: { + objects: submissionState + } + } as any); + + const response = [Object.assign({}, mockSubmissionRestResponse[0], { + sections: mockSectionsData, + errors: mockSectionsErrors + })]; + actions = hot('--a-', { + a: { + type: SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS, + payload: { + submissionId: submissionId, + submissionObject: response + } + } + }); + + const errorsList = parseSectionErrors(mockSectionsErrors); + const expected = cold('--(bcd)-', { + b: new UpdateSectionDataAction( + submissionId, + 'traditionalpageone', + mockSectionsData.traditionalpageone as any, + errorsList.traditionalpageone || [] + ), + c: new UpdateSectionDataAction( + submissionId, + 'license', + mockSectionsData.license as any, + errorsList.license || [] + ), + d: new UpdateSectionDataAction( + submissionId, + 'upload', + mockSectionsData.upload as any, + errorsList.upload || [] + ), + }); + + expect(submissionObjectEffects.saveSubmissionSectionSuccess$).toBeObservable(expected); + expect(notificationsServiceStub.warning).not.toHaveBeenCalled(); + }); + + it('should detect new sections but not notify for it', () => { + store.nextState({ + submission: { + objects: submissionState + } + } as any); + + const response = [Object.assign({}, mockSubmissionRestResponse[0], { + sections: mockSectionsDataTwo, + errors: mockSectionsErrors + })]; + actions = hot('--a-', { + a: { + type: SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS, + payload: { + submissionId: submissionId, + submissionObject: response + } + } + }); + + const errorsList = parseSectionErrors(mockSectionsErrors); + const expected = cold('--(bcde)-', { + b: new UpdateSectionDataAction( + submissionId, + 'traditionalpageone', + mockSectionsDataTwo.traditionalpageone as any, + errorsList.traditionalpageone || [] + ), + c: new UpdateSectionDataAction( + submissionId, + 'traditionalpagetwo', + mockSectionsDataTwo.traditionalpagetwo as any, + errorsList.traditionalpagetwo || [] + ), + d: new UpdateSectionDataAction( + submissionId, + 'license', + mockSectionsDataTwo.license as any, + errorsList.license || [] + ), + e: new UpdateSectionDataAction( + submissionId, + 'upload', + mockSectionsDataTwo.upload as any, + errorsList.upload || [] + ), + }); + + expect(submissionObjectEffects.saveSubmissionSectionSuccess$).toBeObservable(expected); + expect(submissionServiceStub.notifyNewSection).not.toHaveBeenCalled(); + }); + + }); + describe('saveSection$', () => { it('should return a SAVE_SUBMISSION_SECTION_FORM_SUCCESS action on success', () => { actions = hot('--a-', { diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index 0af7b6c275..f882470f48 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -154,13 +154,25 @@ export class SubmissionObjectEffects { * Call parseSaveResponse and dispatch actions */ @Effect() saveSubmissionSuccess$ = this.actions$.pipe( - ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS, SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS), + ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS), withLatestFrom(this.store$), - map(([action, currentState]: [SaveSubmissionFormSuccessAction | SaveSubmissionSectionFormSuccessAction, any]) => { + map(([action, currentState]: [SaveSubmissionFormSuccessAction, any]) => { return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], action.payload.submissionObject, action.payload.submissionId, action.payload.notify); }), mergeMap((actions) => observableFrom(actions))); + /** + * Call parseSaveResponse and dispatch actions. + * Notification system is forced to be disabled. + */ + @Effect() saveSubmissionSectionSuccess$ = this.actions$.pipe( + ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS), + withLatestFrom(this.store$), + map(([action, currentState]: [SaveSubmissionSectionFormSuccessAction, any]) => { + return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], action.payload.submissionObject, action.payload.submissionId, false); + }), + mergeMap((actions) => observableFrom(actions))); + /** * Dispatch a [SaveSubmissionSectionFormSuccessAction] or a [SaveSubmissionSectionFormErrorAction] on error */ From 82b7b8aa6f82a8ddec162bc8743415d1699c4b6f Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 16 Nov 2020 19:09:17 +0100 Subject: [PATCH 04/20] [835] Auto-save in new Item Submission form breaks the form Notifications are enable only for manual submission savings. --- .../shared/testing/submission-service.stub.ts | 1 + .../submission-form-footer.component.spec.ts | 4 +- .../submission-form-footer.component.ts | 2 +- .../objects/submission-objects.actions.ts | 5 +- .../submission-objects.effects.spec.ts | 46 +++++++++++++++++++ .../objects/submission-objects.effects.ts | 2 +- src/app/submission/submission.service.spec.ts | 9 ++++ src/app/submission/submission.service.ts | 14 ++++++ 8 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/app/shared/testing/submission-service.stub.ts b/src/app/shared/testing/submission-service.stub.ts index 35c3ddfee0..7550201c9f 100644 --- a/src/app/shared/testing/submission-service.stub.ts +++ b/src/app/shared/testing/submission-service.stub.ts @@ -9,6 +9,7 @@ export class SubmissionServiceStub { dispatchDeposit = jasmine.createSpy('dispatchDeposit'); dispatchDiscard = jasmine.createSpy('dispatchDiscard'); dispatchSave = jasmine.createSpy('dispatchSave'); + dispatchManualSave = jasmine.createSpy('dispatchManualSave'); dispatchSaveForLater = jasmine.createSpy('dispatchSaveForLater'); dispatchSaveSection = jasmine.createSpy('dispatchSaveSection'); getActiveSectionId = jasmine.createSpy('getActiveSectionId'); diff --git a/src/app/submission/form/footer/submission-form-footer.component.spec.ts b/src/app/submission/form/footer/submission-form-footer.component.spec.ts index d9d58aa4f2..8f278fe17a 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.spec.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.spec.ts @@ -164,12 +164,12 @@ describe('SubmissionFormFooterComponent Component', () => { }); }); - it('should call dispatchSave on save', () => { + it('should call dispatchManualSave on save', () => { comp.save(null); fixture.detectChanges(); - expect(submissionServiceStub.dispatchSave).toHaveBeenCalledWith(submissionId); + expect(submissionServiceStub.dispatchManualSave).toHaveBeenCalledWith(submissionId); }); it('should call dispatchSaveForLater on save for later', () => { diff --git a/src/app/submission/form/footer/submission-form-footer.component.ts b/src/app/submission/form/footer/submission-form-footer.component.ts index 1b885b98b8..96179430b8 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.ts @@ -80,7 +80,7 @@ export class SubmissionFormFooterComponent implements OnChanges { * Dispatch a submission save action */ save(event) { - this.submissionService.dispatchSave(this.submissionId); + this.submissionService.dispatchManualSave(this.submissionId); } /** diff --git a/src/app/submission/objects/submission-objects.actions.ts b/src/app/submission/objects/submission-objects.actions.ts index 73c070846c..2eed4f2c92 100644 --- a/src/app/submission/objects/submission-objects.actions.ts +++ b/src/app/submission/objects/submission-objects.actions.ts @@ -368,6 +368,7 @@ export class SaveSubmissionFormAction implements Action { type = SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM; payload: { submissionId: string; + isManual?: boolean; }; /** @@ -376,8 +377,8 @@ export class SaveSubmissionFormAction implements Action { * @param submissionId * the submission's ID */ - constructor(submissionId: string) { - this.payload = { submissionId }; + constructor(submissionId: string, isManual: boolean = false) { + this.payload = { submissionId, isManual }; } } diff --git a/src/app/submission/objects/submission-objects.effects.spec.ts b/src/app/submission/objects/submission-objects.effects.spec.ts index 7a9d6977e8..ae0fa04ed5 100644 --- a/src/app/submission/objects/submission-objects.effects.spec.ts +++ b/src/app/submission/objects/submission-objects.effects.spec.ts @@ -211,6 +211,52 @@ describe('SubmissionObjectEffects test suite', () => { expect(submissionObjectEffects.saveSubmission$).toBeObservable(expected); }); + it('should enable notifications if is manual', () => { + actions = hot('--a-', { + a: { + type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM, + payload: { + submissionId: submissionId, + isManual: true + } + } + }); + + submissionJsonPatchOperationsServiceStub.jsonPatchByResourceType.and.returnValue(observableOf(mockSubmissionRestResponse)); + const expected = cold('--b-', { + b: new SaveSubmissionFormSuccessAction( + submissionId, + mockSubmissionRestResponse as any, + true + ) + }); + + expect(submissionObjectEffects.saveSubmission$).toBeObservable(expected); + }); + + it('should disable notifications if is not manual', () => { + actions = hot('--a-', { + a: { + type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM, + payload: { + submissionId: submissionId, + isManual: false + } + } + }); + + submissionJsonPatchOperationsServiceStub.jsonPatchByResourceType.and.returnValue(observableOf(mockSubmissionRestResponse)); + const expected = cold('--b-', { + b: new SaveSubmissionFormSuccessAction( + submissionId, + mockSubmissionRestResponse as any, + false + ) + }); + + expect(submissionObjectEffects.saveSubmission$).toBeObservable(expected); + }); + it('should return a SAVE_SUBMISSION_FORM_ERROR action on error', () => { actions = hot('--a-', { a: { diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index f882470f48..09a6f47104 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -132,7 +132,7 @@ export class SubmissionObjectEffects { this.submissionService.getSubmissionObjectLinkName(), action.payload.submissionId, 'sections').pipe( - map((response: SubmissionObject[]) => new SaveSubmissionFormSuccessAction(action.payload.submissionId, response)), + map((response: SubmissionObject[]) => new SaveSubmissionFormSuccessAction(action.payload.submissionId, response, action.payload.isManual)), catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId)))); })); diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index 14c369581a..076028d550 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -494,6 +494,15 @@ describe('SubmissionService test suite', () => { }); }); + describe('dispatchManualSave', () => { + it('should dispatch a new SaveSubmissionFormAction', () => { + service.dispatchManualSave(submissionId,); + const expected = new SaveSubmissionFormAction(submissionId, true); + + expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); + }); + }); + describe('dispatchSaveForLater', () => { it('should dispatch a new SaveForLaterSubmissionFormAction', () => { service.dispatchSaveForLater(submissionId,); diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index 728c860ca9..895e68dba6 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -204,6 +204,20 @@ export class SubmissionService { this.store.dispatch(new DiscardSubmissionAction(submissionId)); } + /** + * Dispatch a new [SaveSubmissionFormAction] + * + * @param submissionId + * The submission id + */ + dispatchManualSave(submissionId) { + this.getSubmissionSaveProcessingStatus(submissionId).pipe( + find((isPending: boolean) => !isPending) + ).subscribe(() => { + this.store.dispatch(new SaveSubmissionFormAction(submissionId, true)); + }) + } + /** * Dispatch a new [SaveSubmissionFormAction] * From 8111bdd3cee29ce668505e41a7f7f99d6dfece55 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Thu, 19 Nov 2020 15:47:03 +0100 Subject: [PATCH 05/20] [835] Auto-save in new Item Submission form breaks the form Store additions: 1. Form AdditionalData: contains the list of the touched metadata 2. Submission metadata: contains the list of the metadata ids assignable for each section We keep also track whether a section ha focused fields or not. --- .../form/builder/form-builder.service.ts | 88 ++++++++++++++++++- src/app/shared/form/form.actions.ts | 29 +++++- src/app/shared/form/form.reducer.ts | 37 +++++++- src/app/shared/form/form.service.ts | 25 +++++- .../objects/submission-objects.actions.ts | 8 +- .../objects/submission-objects.effects.ts | 2 +- .../objects/submission-objects.reducer.ts | 24 ++++- .../sections/form/section-form.component.html | 2 + .../sections/form/section-form.component.ts | 76 +++++++++++++--- .../submission/sections/sections.service.ts | 36 +++++++- 10 files changed, 299 insertions(+), 28 deletions(-) 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; + } + } From 9f33855a0fc9f7014c94450acab6bb8756804db7 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Thu, 19 Nov 2020 15:51:52 +0100 Subject: [PATCH 06/20] [835] Auto-save in new Item Submission form breaks the form Autosave deactivated --- src/environments/environment.common.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index eb961a38eb..80b383a801 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -65,9 +65,9 @@ export const environment: GlobalConfig = { submission: { autosave: { // NOTE: which metadata trigger an autosave - metadata: ['dc.title', 'dc.identifier.doi', 'dc.identifier.pmid', 'dc.identifier.arxiv'], + metadata: [], // NOTE: every how many minutes submission is saved automatically - timer: 5 + timer: 0 }, icons: { metadata: [ From 875a43a14ed732a7180a685d35ff9e39ea7b4809 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Fri, 20 Nov 2020 15:02:56 +0100 Subject: [PATCH 07/20] [835] Auto-save in new Item Submission form breaks the form Section formId added to the section state. Error filtering during the parsing of the submission response. --- .../form/builder/form-builder.service.ts | 2 +- src/app/shared/form/form.component.ts | 1 + src/app/shared/form/form.reducer.ts | 2 +- src/app/shared/form/form.service.ts | 4 +- .../objects/submission-objects.actions.ts | 25 ++++++++++ .../objects/submission-objects.effects.ts | 47 +++++++++++++++++-- .../objects/submission-objects.reducer.ts | 37 +++++++++++++++ .../sections/form/section-form.component.html | 1 - .../sections/form/section-form.component.ts | 26 ++++------ .../submission/sections/sections.service.ts | 13 +++++ 10 files changed, 130 insertions(+), 28 deletions(-) diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index 256e657474..f0baa3f8a3 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -328,7 +328,7 @@ export class FormBuilderService extends DynamicFormService { model = model.parent as any; } - const iterateControlModels = (findGroupModel: DynamicFormControlModel[], controlModelIndex: number = 0): void => { + const iterateControlModels = (findGroupModel: DynamicFormControlModel[], controlModelIndex: number = 0): string[] => { let iterateResult = Object.create({}); // Iterate over all group's controls diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index 7a5d3932c8..43f9bdfa90 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -253,6 +253,7 @@ export class FormComponent implements OnDestroy, OnInit { } onFocus(event: DynamicFormControlEvent): void { + this.formService.setTouched(this.formId, this.formModel, event); this.focus.emit(event); } diff --git a/src/app/shared/form/form.reducer.ts b/src/app/shared/form/form.reducer.ts index 8a5c2a57b8..2a2b3cd2de 100644 --- a/src/app/shared/form/form.reducer.ts +++ b/src/app/shared/form/form.reducer.ts @@ -133,7 +133,7 @@ function initForm(state: FormState, action: FormInitAction): FormState { data: action.payload.formData, valid: action.payload.valid, errors: [], - additional: action.payload.formAdditional + additional: action.payload.formAdditional ? action.payload.formAdditional : {} }; if (!hasValue(state[action.payload.formId])) { return Object.assign({}, state, { diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index 6d208b08d2..8983bade44 100644 --- a/src/app/shared/form/form.service.ts +++ b/src/app/shared/form/form.service.ts @@ -161,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, additional?: any) { - this.store.dispatch(new FormInitAction(formId, this.formBuilderService.getValueFromModel(model), valid, additional)); + public initForm(formId: string, model: DynamicFormControlModel[], valid: boolean) { + this.store.dispatch(new FormInitAction(formId, this.formBuilderService.getValueFromModel(model), valid)); } public setStatusChanged(formId: string, valid: boolean) { diff --git a/src/app/submission/objects/submission-objects.actions.ts b/src/app/submission/objects/submission-objects.actions.ts index 6fe42a149a..962f216c7d 100644 --- a/src/app/submission/objects/submission-objects.actions.ts +++ b/src/app/submission/objects/submission-objects.actions.ts @@ -40,6 +40,7 @@ export const SubmissionObjectActionTypes = { INIT_SECTION: type('dspace/submission/INIT_SECTION'), ENABLE_SECTION: type('dspace/submission/ENABLE_SECTION'), DISABLE_SECTION: type('dspace/submission/DISABLE_SECTION'), + SET_SECTION_FORM_ID: type('dspace/submission/SET_SECTION_FORM_ID'), SECTION_STATUS_CHANGE: type('dspace/submission/SECTION_STATUS_CHANGE'), SECTION_LOADING_STATUS_CHANGE: type('dspace/submission/SECTION_LOADING_STATUS_CHANGE'), UPDATE_SECTION_DATA: type('dspace/submission/UPDATE_SECTION_DATA'), @@ -256,6 +257,29 @@ export class RemoveSectionErrorsAction implements Action { } } +export class SetSectionFormId implements Action { + type = SubmissionObjectActionTypes.SET_SECTION_FORM_ID; + payload: { + submissionId: string; + sectionId: string; + formId: string; + }; + + /** + * Create a new SetSectionFormId + * + * @param submissionId + * the submission's ID + * @param sectionId + * the section's ID + * @param formId + * the section's formId + */ + constructor(submissionId: string, sectionId: string, formId: string) { + this.payload = { submissionId, sectionId, formId }; + } +} + // Submission actions export class CompleteInitSubmissionFormAction implements Action { @@ -782,6 +806,7 @@ export class DeleteUploadedFileAction implements Action { */ export type SubmissionObjectAction = DisableSectionAction | InitSectionAction + | SetSectionFormId | EnableSectionAction | InitSubmissionFormAction | ResetSubmissionFormAction diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index 3a25d79330..07436fc87f 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -52,12 +52,13 @@ import { UpdateSectionDataAction, UpdateSectionDataSuccessAction } from './submission-objects.actions'; -import { SubmissionObjectEntry, SubmissionSectionObject } from './submission-objects.reducer'; +import {SubmissionObjectEntry, SubmissionSectionError, SubmissionSectionObject} from './submission-objects.reducer'; import { Item } from '../../core/shared/item.model'; import { RemoteData } from '../../core/data/remote-data'; import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service'; import { followLink } from '../../shared/utils/follow-link-config.model'; +import parseSectionErrorPaths, {SectionErrorPath} from '../utils/parseSectionErrorPaths'; @Injectable() export class SubmissionObjectEffects { @@ -157,7 +158,8 @@ export class SubmissionObjectEffects { ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS), withLatestFrom(this.store$), map(([action, currentState]: [SaveSubmissionFormSuccessAction, any]) => { - return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], action.payload.submissionObject, action.payload.submissionId, action.payload.notify); + return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], + action.payload.submissionObject, action.payload.submissionId, currentState.forms, action.payload.notify); }), mergeMap((actions) => observableFrom(actions))); @@ -169,7 +171,8 @@ export class SubmissionObjectEffects { ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS), withLatestFrom(this.store$), map(([action, currentState]: [SaveSubmissionSectionFormSuccessAction, any]) => { - return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], action.payload.submissionObject, action.payload.submissionId, false); + return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], + action.payload.submissionObject, action.payload.submissionId, currentState.forms, false); }), mergeMap((actions) => observableFrom(actions))); @@ -212,7 +215,8 @@ export class SubmissionObjectEffects { return new DepositSubmissionAction(action.payload.submissionId); } else { this.notificationsService.warning(null, this.translate.get('submission.sections.general.sections_not_valid')); - return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], response, action.payload.submissionId); + return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], + response, action.payload.submissionId, currentState.forms); } }), catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId)))); @@ -365,6 +369,7 @@ export class SubmissionObjectEffects { currentState: SubmissionObjectEntry, response: SubmissionObject[], submissionId: string, + forms, notify: boolean = true): SubmissionObjectAction[] { const mappedActions = []; @@ -404,10 +409,42 @@ export class SubmissionObjectEffects { if (notify && !currentState.sections[sectionId].enabled) { this.submissionService.notifyNewSection(submissionId, sectionId, currentState.sections[sectionId].sectionType); } - mappedActions.push(new UpdateSectionDataAction(submissionId, sectionId, sectionData, sectionErrors)); + + const sectionForm = forms[currentState.sections[sectionId].formId]; + const filteredErrors = filterErrors(sectionForm, sectionErrors, currentState.sections[sectionId].sectionType, notify); + + mappedActions.push(new UpdateSectionDataAction(submissionId, sectionId, sectionData, filteredErrors)); } }); } return mappedActions; } } + +/** + * Filter sectionErrors accordingly to this rules: + * 1. if notifications are enabled return all errors + * 2. if sectionType is different from submission-form return all errors + * 3. otherwise return errors only for those fields marked as touched inside the section form + * @param sectionForm + * @param sectionErrors + * @param notify + */ +function filterErrors(sectionForm, sectionErrors, sectionType, notify): any { + if (notify || sectionType !== SectionsType.SubmissionForm) { + return sectionErrors; + } + if (!sectionForm || !sectionForm.additional || !sectionForm.additional.touched) { + return []; + } + const filteredErrors = []; + sectionErrors.forEach((error: SubmissionSectionError) => { + const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path); + errorPaths.forEach((path: SectionErrorPath) => { + if (path.fieldId && sectionForm.additional.touched[path.fieldId]) { + filteredErrors.push(error); + } + }); + }); + return filteredErrors; +} diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts index 6f7098532e..91caeb1e77 100644 --- a/src/app/submission/objects/submission-objects.reducer.ts +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -30,6 +30,7 @@ import { SaveSubmissionSectionFormSuccessAction, SectionStatusChangeAction, SetActiveSectionAction, + SetSectionFormId, SubmissionObjectAction, SubmissionObjectActionTypes, UpdateSectionDataAction @@ -109,6 +110,11 @@ export interface SubmissionSectionObject { * A boolean representing if this section is valid */ isValid: boolean; + + /** + * The formId related to this section + */ + formId: string; } /** @@ -263,6 +269,10 @@ export function submissionObjectReducer(state = initialState, action: Submission return initSection(state, action as InitSectionAction); } + case SubmissionObjectActionTypes.SET_SECTION_FORM_ID: { + return setSectionFormId(state, action as SetSectionFormId); + } + case SubmissionObjectActionTypes.ENABLE_SECTION: { return changeSectionState(state, action as EnableSectionAction, true); } @@ -646,6 +656,33 @@ function initSection(state: SubmissionObjectState, action: InitSectionAction): S } } +/** + * Set a section form id. + * + * @param state + * the current state + * @param action + * an SetSectionFormId + * @return SubmissionObjectState + * the new state + */ +function setSectionFormId(state: SubmissionObjectState, action: SetSectionFormId): SubmissionObjectState { + if (hasValue(state[ action.payload.submissionId ])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + sections: Object.assign({}, state[ action.payload.submissionId ].sections, { + [ action.payload.sectionId ]: { + ...state[ action.payload.submissionId ].sections [action.payload.sectionId], + formId: action.payload.formId + } + }) + }) + }); + } else { + return state; + } +} + /** * Update section's data. * diff --git a/src/app/submission/sections/form/section-form.component.html b/src/app/submission/sections/form/section-form.component.html index ab549315ed..894c8b1d17 100644 --- a/src/app/submission/sections/form/section-form.component.html +++ b/src/app/submission/sections/form/section-form.component.html @@ -2,7 +2,6 @@ ) => configData.payload), +======= + this.sectionService.dispatchSetSectionFormId(this.submissionId, this.sectionData.id, this.formId); + this.formConfigService.getConfigByHref(this.sectionData.config).pipe( + map((configData: ConfigData) => configData.payload), +>>>>>>> [835] Auto-save in new Item Submission form breaks the form tap((config: SubmissionFormsModel) => this.formConfig = config), flatMap(() => observableCombineLatest( @@ -265,7 +265,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent { Object.keys(diffObj) .forEach((key) => { diffObj[key].forEach((value) => { - if (value.hasOwnProperty('value') && !isEmpty(value.value)) { + if (value.hasOwnProperty('value') && findIndex(this.formData[key], { value: value.value }) < 0) { diffResult.push(value); } }); @@ -288,7 +288,6 @@ 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) { @@ -341,9 +340,6 @@ 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(); @@ -364,12 +360,6 @@ export class SubmissionSectionformComponent extends SectionModelComponent { this.formData = formData; }), - this.formService.getFormAdditionalData(this.formId).pipe( - distinctUntilChanged()) - .subscribe((formAdditional) => { - this.formAdditionalData = formAdditional; - }), - /** * Subscribe to section state */ diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts index 895bde13ec..7b9701671c 100644 --- a/src/app/submission/sections/sections.service.ts +++ b/src/app/submission/sections/sections.service.ts @@ -15,6 +15,7 @@ import { InertSectionErrorsAction, RemoveSectionErrorsAction, SectionStatusChangeAction, + SetSectionFormId, UpdateSectionDataAction } from '../objects/submission-objects.actions'; import { @@ -135,6 +136,18 @@ export class SectionsService { this.store.dispatch(new RemoveSectionErrorsAction(submissionId, sectionId)); } + /** + * Dispatch a new [SetSectionFormId] + * The submission id + * @param sectionId + * The section id + * @param formId + * The form id + */ + public dispatchSetSectionFormId(submissionId, sectionId, formId) { + this.store.dispatch(new SetSectionFormId(submissionId, sectionId, formId)); + } + /** * Return the data object for the specified section * From 999993734f02da479aa7419903fd2b64712654cc Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Fri, 20 Nov 2020 17:25:20 +0100 Subject: [PATCH 08/20] [835] Auto-save in new Item Submission form breaks the form Tests fixed. --- src/app/shared/form/form.component.spec.ts | 3 +- src/app/shared/form/form.reducer.spec.ts | 33 ++++++++++++------- src/app/shared/form/form.service.spec.ts | 3 +- .../shared/testing/sections-service.stub.ts | 3 +- .../submission-objects.effects.spec.ts | 9 ++--- .../objects/submission-objects.effects.ts | 12 +++++-- .../form/section-form.component.spec.ts | 7 ++-- 7 files changed, 48 insertions(+), 22 deletions(-) diff --git a/src/app/shared/form/form.component.spec.ts b/src/app/shared/form/form.component.spec.ts index f7a0564191..e7193246fb 100644 --- a/src/app/shared/form/form.component.spec.ts +++ b/src/app/shared/form/form.component.spec.ts @@ -119,7 +119,8 @@ function init() { dc_identifier_issn: null }, valid: false, - errors: [] + errors: [], + additional: {} } }; diff --git a/src/app/shared/form/form.reducer.spec.ts b/src/app/shared/form/form.reducer.spec.ts index 01e3e6b1ba..978d4822d5 100644 --- a/src/app/shared/form/form.reducer.spec.ts +++ b/src/app/shared/form/form.reducer.spec.ts @@ -21,7 +21,8 @@ describe('formReducer', () => { description: null }, valid: false, - errors: [] + errors: [], + additional: {} } }; const formId = 'testForm'; @@ -48,7 +49,8 @@ describe('formReducer', () => { description: null }, valid: false, - errors: [] + errors: [], + additional: {} } }; const formId = 'testForm'; @@ -67,7 +69,8 @@ describe('formReducer', () => { description: null }, valid: false, - errors: [] + errors: [], + additional: {} } }; @@ -88,7 +91,8 @@ describe('formReducer', () => { description: null }, valid: false, - errors: [] + errors: [], + additional: {} } }; const state = { @@ -100,7 +104,8 @@ describe('formReducer', () => { description: null }, valid: false, - errors: [] + errors: [], + additional: {} } }; const formId = 'testForm'; @@ -127,7 +132,8 @@ describe('formReducer', () => { description: null }, valid: false, - errors: [] + errors: [], + additional: {} } }; const state = { @@ -139,7 +145,8 @@ describe('formReducer', () => { description: null }, valid: true, - errors: [] + errors: [], + additional: {} } }; const formId = 'testForm'; @@ -160,7 +167,8 @@ describe('formReducer', () => { description: null }, valid: true, - errors: [] + errors: [], + additional: {} } }; @@ -204,7 +212,8 @@ describe('formReducer', () => { fieldIndex: 0, message: 'error.validation.required' } - ] + ], + additional: {} } }; @@ -236,7 +245,8 @@ describe('formReducer', () => { description: null }, valid: true, - errors: [] + errors: [], + additional: {} } }; @@ -264,7 +274,8 @@ describe('formReducer', () => { fieldIndex: 0, message: 'error.validation.required' } - ] + ], + additional: {} } }; diff --git a/src/app/shared/form/form.service.spec.ts b/src/app/shared/form/form.service.spec.ts index ee90e377ee..031c83e457 100644 --- a/src/app/shared/form/form.service.spec.ts +++ b/src/app/shared/form/form.service.spec.ts @@ -84,7 +84,8 @@ describe('FormService test suite', () => { testForm: { data: formData, valid: false, - errors: [] + errors: [], + additional: {} } }; diff --git a/src/app/shared/testing/sections-service.stub.ts b/src/app/shared/testing/sections-service.stub.ts index 2110d71d8e..3b311c5e19 100644 --- a/src/app/shared/testing/sections-service.stub.ts +++ b/src/app/shared/testing/sections-service.stub.ts @@ -2,6 +2,7 @@ export class SectionsServiceStub { checkSectionErrors = jasmine.createSpy('checkSectionErrors'); dispatchRemoveSectionErrors = jasmine.createSpy('dispatchRemoveSectionErrors'); + dispatchSetSectionFormId = jasmine.createSpy('dispatchSetSectionFormId'); getSectionData = jasmine.createSpy('getSectionData'); getSectionErrors = jasmine.createSpy('getSectionErrors'); getSectionState = jasmine.createSpy('getSectionState'); @@ -14,5 +15,5 @@ export class SectionsServiceStub { updateSectionData = jasmine.createSpy('updateSectionData'); setSectionError = jasmine.createSpy('setSectionError'); setSectionStatus = jasmine.createSpy('setSectionStatus'); - + computeSectionConfiguredMetadata = jasmine.createSpy('computeSectionConfiguredMetadata'); } diff --git a/src/app/submission/objects/submission-objects.effects.spec.ts b/src/app/submission/objects/submission-objects.effects.spec.ts index ae0fa04ed5..90c4e6284d 100644 --- a/src/app/submission/objects/submission-objects.effects.spec.ts +++ b/src/app/submission/objects/submission-objects.effects.spec.ts @@ -551,7 +551,7 @@ describe('SubmissionObjectEffects test suite', () => { submissionId, 'traditionalpageone', mockSectionsData.traditionalpageone as any, - errorsList.traditionalpageone || [] + [] ), c: new UpdateSectionDataAction( submissionId, @@ -638,12 +638,13 @@ describe('SubmissionObjectEffects test suite', () => { }); const errorsList = parseSectionErrors(mockSectionsErrors); + console.log(errorsList); const expected = cold('--(bcd)-', { b: new UpdateSectionDataAction( submissionId, 'traditionalpageone', mockSectionsData.traditionalpageone as any, - errorsList.traditionalpageone || [] + [] ), c: new UpdateSectionDataAction( submissionId, @@ -679,7 +680,7 @@ describe('SubmissionObjectEffects test suite', () => { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS, payload: { submissionId: submissionId, - submissionObject: response + submissionObject: response, } } }); @@ -690,7 +691,7 @@ describe('SubmissionObjectEffects test suite', () => { submissionId, 'traditionalpageone', mockSectionsDataTwo.traditionalpageone as any, - errorsList.traditionalpageone || [] + [] ), c: new UpdateSectionDataAction( submissionId, diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index 07436fc87f..4eb3f8109d 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -410,9 +410,8 @@ export class SubmissionObjectEffects { this.submissionService.notifyNewSection(submissionId, sectionId, currentState.sections[sectionId].sectionType); } - const sectionForm = forms[currentState.sections[sectionId].formId]; + const sectionForm = getForm(forms, currentState, sectionId); const filteredErrors = filterErrors(sectionForm, sectionErrors, currentState.sections[sectionId].sectionType, notify); - mappedActions.push(new UpdateSectionDataAction(submissionId, sectionId, sectionData, filteredErrors)); } }); @@ -421,6 +420,15 @@ export class SubmissionObjectEffects { } } +function getForm(forms, currentState, sectionId) { + if (!forms) { + return null; + } + const formId = currentState.sections[sectionId].formId; + return forms[formId]; +} + + /** * Filter sectionErrors accordingly to this rules: * 1. if notifications are enabled return all errors diff --git a/src/app/submission/sections/form/section-form.component.spec.ts b/src/app/submission/sections/form/section-form.component.spec.ts index 79e24dd451..7a62628d02 100644 --- a/src/app/submission/sections/form/section-form.component.spec.ts +++ b/src/app/submission/sections/form/section-form.component.spec.ts @@ -287,6 +287,7 @@ describe('SubmissionSectionformComponent test suite', () => { 'dc.title': [new FormFieldMetadataValueObject('test')] }; compAsAny.formData = {}; + compAsAny.sectionMetadata = ['dc.title']; expect(comp.hasMetadataEnrichment(newSectionData)).toBeTruthy(); }); @@ -296,7 +297,7 @@ describe('SubmissionSectionformComponent test suite', () => { 'dc.title': [new FormFieldMetadataValueObject('test')] }; compAsAny.formData = newSectionData; - + compAsAny.sectionMetadata = ['dc.title']; expect(comp.hasMetadataEnrichment(newSectionData)).toBeFalsy(); }); @@ -310,6 +311,7 @@ describe('SubmissionSectionformComponent test suite', () => { comp.sectionData.data = {}; comp.sectionData.errors = []; compAsAny.formData = {}; + compAsAny.sectionMetadata = ['dc.title']; comp.updateForm(sectionData, sectionError); @@ -329,10 +331,11 @@ describe('SubmissionSectionformComponent test suite', () => { comp.sectionData.data = {}; comp.sectionData.errors = []; compAsAny.formData = sectionData; + compAsAny.sectionMetadata = ['dc.title']; comp.updateForm(sectionData, parsedSectionErrors); - expect(comp.initForm).toHaveBeenCalled(); + expect(comp.initForm).not.toHaveBeenCalled(); expect(comp.checksForErrors).toHaveBeenCalled(); expect(comp.sectionData.data).toEqual(sectionData); }); From 55bcdf0a2527e16ed41848689ff37667948bd73c Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Fri, 20 Nov 2020 18:17:17 +0100 Subject: [PATCH 09/20] [835] Auto-save in new Item Submission form breaks the form Minor changes and cleanup. --- .../form/builder/form-builder.service.ts | 6 ++---- src/app/shared/form/form.component.html | 2 +- .../shared/testing/submission-service.stub.ts | 1 - .../submission-form-footer.component.spec.ts | 4 ++-- .../submission-form-footer.component.ts | 2 +- .../sections/form/section-form.component.html | 1 - .../sections/form/section-form.component.ts | 19 +----------------- src/app/submission/submission.service.spec.ts | 8 +++----- src/app/submission/submission.service.ts | 20 ++++--------------- 9 files changed, 14 insertions(+), 49 deletions(-) diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index f0baa3f8a3..a6100e7ce7 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -16,7 +16,7 @@ import { import { isObject, isString, mergeWith } from 'lodash'; import { hasValue, isEmpty, isNotEmpty, isNotNull, isNotUndefined, isNull } from '../../empty.util'; -import { DynamicQualdropModel } from './ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; +import {DynamicQualdropModel} from './ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model'; import { RowParser } from './parsers/row-parser'; @@ -56,9 +56,7 @@ export class FormBuilderService extends DynamicFormService { } if (this.isConcatGroup(controlModel)) { - const concatGroupId = controlModel.id.replace(CONCAT_GROUP_SUFFIX, ''); - // if (concatGroupId === findId) { - if (concatGroupId.includes(findId)) { + if (controlModel.id.match(new RegExp(findId + CONCAT_GROUP_SUFFIX + `_\\d+$`))) { result = (controlModel as DynamicConcatModel).group[0]; break; } diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 8ac2ca2d23..97879cc025 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -18,7 +18,7 @@ diff --git a/src/app/shared/testing/submission-service.stub.ts b/src/app/shared/testing/submission-service.stub.ts index 7550201c9f..35c3ddfee0 100644 --- a/src/app/shared/testing/submission-service.stub.ts +++ b/src/app/shared/testing/submission-service.stub.ts @@ -9,7 +9,6 @@ export class SubmissionServiceStub { dispatchDeposit = jasmine.createSpy('dispatchDeposit'); dispatchDiscard = jasmine.createSpy('dispatchDiscard'); dispatchSave = jasmine.createSpy('dispatchSave'); - dispatchManualSave = jasmine.createSpy('dispatchManualSave'); dispatchSaveForLater = jasmine.createSpy('dispatchSaveForLater'); dispatchSaveSection = jasmine.createSpy('dispatchSaveSection'); getActiveSectionId = jasmine.createSpy('getActiveSectionId'); diff --git a/src/app/submission/form/footer/submission-form-footer.component.spec.ts b/src/app/submission/form/footer/submission-form-footer.component.spec.ts index 8f278fe17a..c8860e3541 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.spec.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.spec.ts @@ -164,12 +164,12 @@ describe('SubmissionFormFooterComponent Component', () => { }); }); - it('should call dispatchManualSave on save', () => { + it('should call dispatchSave on save', () => { comp.save(null); fixture.detectChanges(); - expect(submissionServiceStub.dispatchManualSave).toHaveBeenCalledWith(submissionId); + expect(submissionServiceStub.dispatchSave).toHaveBeenCalledWith(submissionId, true); }); it('should call dispatchSaveForLater on save for later', () => { diff --git a/src/app/submission/form/footer/submission-form-footer.component.ts b/src/app/submission/form/footer/submission-form-footer.component.ts index 96179430b8..0636c3f6d3 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.ts @@ -80,7 +80,7 @@ export class SubmissionFormFooterComponent implements OnChanges { * Dispatch a submission save action */ save(event) { - this.submissionService.dispatchManualSave(this.submissionId); + this.submissionService.dispatchSave(this.submissionId, true); } /** diff --git a/src/app/submission/sections/form/section-form.component.html b/src/app/submission/sections/form/section-form.component.html index 894c8b1d17..166e52675b 100644 --- a/src/app/submission/sections/form/section-form.component.html +++ b/src/app/submission/sections/form/section-form.component.html @@ -3,7 +3,6 @@ [formId]="formId" [formModel]="formModel" [displaySubmit]="false" - (dfBlur)="onBlur($event)" (dfChange)="onChange($event)" (dfFocus)="onFocus($event)" (remove)="onRemove($event)" diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index 8a50d00b9f..c5d9999da5 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -131,12 +131,6 @@ export class SubmissionSectionformComponent extends SectionModelComponent { */ @ViewChild('formRef', {static: false}) private formRef: FormComponent; - /** - * Keep track whether the section is focused or not. - * @protected - */ - protected isFocused = false; - /** * Initialize instance variables * @@ -265,6 +259,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent { Object.keys(diffObj) .forEach((key) => { diffObj[key].forEach((value) => { + // the findIndex extra check excludes values already present in the form but in different positions if (value.hasOwnProperty('value') && findIndex(this.formData[key], { value: value.value }) < 0) { diffResult.push(value); } @@ -407,7 +402,6 @@ 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)) { @@ -419,17 +413,6 @@ 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/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index 076028d550..6455638eef 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -487,16 +487,14 @@ describe('SubmissionService test suite', () => { describe('dispatchSave', () => { it('should dispatch a new SaveSubmissionFormAction', () => { - service.dispatchSave(submissionId,); + service.dispatchSave(submissionId); const expected = new SaveSubmissionFormAction(submissionId); expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); }); - }); - describe('dispatchManualSave', () => { - it('should dispatch a new SaveSubmissionFormAction', () => { - service.dispatchManualSave(submissionId,); + it('should dispatch a new SaveSubmissionFormAction with manual flag', () => { + service.dispatchSave(submissionId, true); const expected = new SaveSubmissionFormAction(submissionId, true); expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index 895e68dba6..0277ac6e5a 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -209,26 +209,14 @@ export class SubmissionService { * * @param submissionId * The submission id + * @param manual + * whether is a manual save, default false */ - dispatchManualSave(submissionId) { + dispatchSave(submissionId, manual?: boolean) { this.getSubmissionSaveProcessingStatus(submissionId).pipe( find((isPending: boolean) => !isPending) ).subscribe(() => { - this.store.dispatch(new SaveSubmissionFormAction(submissionId, true)); - }) - } - - /** - * Dispatch a new [SaveSubmissionFormAction] - * - * @param submissionId - * The submission id - */ - dispatchSave(submissionId) { - this.getSubmissionSaveProcessingStatus(submissionId).pipe( - find((isPending: boolean) => !isPending) - ).subscribe(() => { - this.store.dispatch(new SaveSubmissionFormAction(submissionId)); + this.store.dispatch(new SaveSubmissionFormAction(submissionId, manual)); }) } From 9b752b443ea417ca231d42b6ccf49e7e1fce4f2d Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 23 Nov 2020 10:55:39 +0100 Subject: [PATCH 10/20] [835] Auto-save in new Item Submission form breaks the form Lint corrections. --- src/app/submission/objects/submission-objects.effects.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index 4eb3f8109d..9e2db13776 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -428,7 +428,6 @@ function getForm(forms, currentState, sectionId) { return forms[formId]; } - /** * Filter sectionErrors accordingly to this rules: * 1. if notifications are enabled return all errors From 0acaa3e57fb7bbdb0f634be8fbc9983ab338b575 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 23 Nov 2020 12:58:16 +0100 Subject: [PATCH 11/20] [835] Auto-save in new Item Submission form breaks the form Section metadata dispatched to the store and retrieved in subscription. Added test case for hasMetadataEnrichment. --- .../sections/form/section-form.component.spec.ts | 9 +++++++++ .../submission/sections/form/section-form.component.ts | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app/submission/sections/form/section-form.component.spec.ts b/src/app/submission/sections/form/section-form.component.spec.ts index 7a62628d02..d649f985ca 100644 --- a/src/app/submission/sections/form/section-form.component.spec.ts +++ b/src/app/submission/sections/form/section-form.component.spec.ts @@ -301,6 +301,15 @@ describe('SubmissionSectionformComponent test suite', () => { expect(comp.hasMetadataEnrichment(newSectionData)).toBeFalsy(); }); + it('should return false when metadata has Metadata Enrichment but not belonging to sectionMetadata', () => { + const newSectionData = { + 'dc.title': [new FormFieldMetadataValueObject('test')] + }; + compAsAny.formData = newSectionData; + compAsAny.sectionMetadata = []; + expect(comp.hasMetadataEnrichment(newSectionData)).toBeFalsy(); + }); + it('should update form properly', () => { spyOn(comp, 'initForm'); spyOn(comp, 'checksForErrors'); diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index c5d9999da5..02907df16a 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -283,7 +283,8 @@ export class SubmissionSectionformComponent extends SectionModelComponent { sectionData, this.submissionService.getSubmissionScope() ); - this.sectionMetadata = this.sectionService.computeSectionConfiguredMetadata(this.formConfig); + const sectionMetadata = this.sectionService.computeSectionConfiguredMetadata(this.formConfig); + this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, sectionData, [], sectionMetadata); } catch (e) { const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString(); @@ -365,6 +366,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent { distinctUntilChanged()) .subscribe((sectionState: SubmissionSectionObject) => { this.fieldsOnTheirWayToBeRemoved = new Map(); + this.sectionMetadata = sectionState.metadata; this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errors); }) ) From eb144b1551eecceab381e933245652e1a02fa7e2 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 23 Nov 2020 12:59:22 +0100 Subject: [PATCH 12/20] [835] Auto-save in new Item Submission form breaks the form Test FormSetAdditionalAction. --- src/app/shared/form/form.reducer.spec.ts | 95 ++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/app/shared/form/form.reducer.spec.ts b/src/app/shared/form/form.reducer.spec.ts index 978d4822d5..ff89ae27cb 100644 --- a/src/app/shared/form/form.reducer.spec.ts +++ b/src/app/shared/form/form.reducer.spec.ts @@ -6,6 +6,7 @@ import { FormInitAction, FormRemoveAction, FormRemoveErrorAction, + FormSetAdditionalAction, FormStatusChangeAction } from './form.actions'; @@ -286,4 +287,98 @@ describe('formReducer', () => { expect(newState.testForm.errors).toEqual([]); }); + + it('should set new touched field to the form state', () => { + const initState = { + testForm: { + data: { + author: null, + title: ['test'], + date: null, + description: null + }, + valid: false, + errors: [], + additional: {} + } + }; + const state = { + testForm: { + data: { + author: null, + title: ['test'], + date: null, + description: null + }, + valid: false, + errors: [], + additional: { + touched: { + title: true + } + } + } + }; + const formId = 'testForm'; + const additionalData = { + touched: { + title: true + } + }; + + const action = new FormSetAdditionalAction(formId, additionalData); + const newState = formReducer(initState, action); + + expect(newState).toEqual(state); + }); + + it('should add new touched field to the form state', () => { + const initState = { + testForm: { + data: { + author: null, + title: ['test'], + date: null, + description: null + }, + valid: false, + errors: [], + additional: { + touched: { + title: true + } + } + } + }; + const state = { + testForm: { + data: { + author: null, + title: ['test'], + date: null, + description: null + }, + valid: false, + errors: [], + additional: { + touched: { + title: true, + author: true + } + } + } + }; + const formId = 'testForm'; + const additionalData = { + touched: { + author: true + } + }; + + const action = new FormSetAdditionalAction(formId, additionalData); + const newState = formReducer(initState, action); + + expect(newState).toEqual(state); + }); + }); From d47f686b9597583248b3e5cee919e523b1944912 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 23 Nov 2020 14:29:10 +0100 Subject: [PATCH 13/20] [835] Auto-save in new Item Submission form breaks the form Section Metadata field tested. Form Touched filter tested. --- src/app/shared/mocks/submission.mock.ts | 16 +++++ .../submission-objects.effects.spec.ts | 63 ++++++++++++++++++- .../submission-objects.reducer.spec.ts | 11 ++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/app/shared/mocks/submission.mock.ts b/src/app/shared/mocks/submission.mock.ts index 98564a4d18..1fb5dcd1b3 100644 --- a/src/app/shared/mocks/submission.mock.ts +++ b/src/app/shared/mocks/submission.mock.ts @@ -60,6 +60,21 @@ export const mockSectionsErrors = [ } ]; +export const mockSectionsErrorsTwo = [ + { + message: 'error.validation.required', + paths: [ + '/sections/traditionalpageone/dc.title', + ] + }, + { + message: 'error.validation.license.notgranted', + paths: [ + '/sections/license' + ] + } +]; + export const mockUploadResponse1Errors = { errors: [ { @@ -1033,6 +1048,7 @@ export const mockSubmissionState: SubmissionObjectState = Object.assign({}, { enabled: true, data: {}, errors: [], + formId: '2_traditionalpageone', isLoading: false, isValid: false } as any, diff --git a/src/app/submission/objects/submission-objects.effects.spec.ts b/src/app/submission/objects/submission-objects.effects.spec.ts index 90c4e6284d..0f0f2aa7d7 100644 --- a/src/app/submission/objects/submission-objects.effects.spec.ts +++ b/src/app/submission/objects/submission-objects.effects.spec.ts @@ -32,7 +32,7 @@ import { mockSubmissionId, mockSubmissionSelfUrl, mockSubmissionState, - mockSubmissionRestResponse + mockSubmissionRestResponse, mockSectionsErrorsTwo } from '../../shared/mocks/submission.mock'; import { SubmissionSectionModel } from '../../core/config/models/config-submission-section.model'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; @@ -51,6 +51,7 @@ import { Item } from '../../core/shared/item.model'; import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service'; import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; +import {formStateSelector} from '../../shared/form/selectors'; describe('SubmissionObjectEffects test suite', () => { let submissionObjectEffects: SubmissionObjectEffects; @@ -343,7 +344,8 @@ describe('SubmissionObjectEffects test suite', () => { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS, payload: { submissionId: submissionId, - submissionObject: response + submissionObject: response, + notify: true } } }); @@ -375,6 +377,63 @@ describe('SubmissionObjectEffects test suite', () => { }); + it('should not display errors when notification are disabled and field are not touched', () => { + store.nextState({ + submission: { + objects: submissionState + }, + forms: { + '2_traditionalpageone': { + additional: { + touched: { + 'dc.title': true + } + } + } + } + } as any); + + const response = [Object.assign({}, mockSubmissionRestResponse[0], { + sections: mockSectionsData, + errors: mockSectionsErrors + })]; + actions = hot('--a-', { + a: { + type: SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS, + payload: { + submissionId: submissionId, + submissionObject: response, + notify: false + } + } + }); + + const errorsList = parseSectionErrors(mockSectionsErrorsTwo); + const expected = cold('--(bcd)-', { + b: new UpdateSectionDataAction( + submissionId, + 'traditionalpageone', + mockSectionsData.traditionalpageone as any, + errorsList.traditionalpageone + ), + c: new UpdateSectionDataAction( + submissionId, + 'license', + mockSectionsData.license as any, + errorsList.license || [] + ), + d: new UpdateSectionDataAction( + submissionId, + 'upload', + mockSectionsData.upload as any, + errorsList.upload || [] + ), + }); + + expect(submissionObjectEffects.saveSubmissionSectionSuccess$).toBeObservable(expected); + expect(notificationsServiceStub.warning).not.toHaveBeenCalled(); + }); + it('should display a success notification', () => { store.nextState({ submission: { diff --git a/src/app/submission/objects/submission-objects.reducer.spec.ts b/src/app/submission/objects/submission-objects.reducer.spec.ts index 0c585e4bca..0431cdff79 100644 --- a/src/app/submission/objects/submission-objects.reducer.spec.ts +++ b/src/app/submission/objects/submission-objects.reducer.spec.ts @@ -335,6 +335,17 @@ describe('submissionReducer test suite', () => { expect(newState[826].sections.traditionalpageone.data).toEqual(data); }); + it('should update submission section metadata properly', () => { + const data = { + } as any; + const metadata = ['dc.title', 'dc.contributor.author']; + + const action = new UpdateSectionDataAction(submissionId, 'traditionalpageone', data, [], metadata); + const newState = submissionObjectReducer(initState, action); + + expect(newState[826].sections.traditionalpageone.metadata).toEqual(metadata); + }); + it('should add submission section errors properly', () => { const errors = [ { From de372896e74cf9e649f11543b3dfe049287351bf Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Fri, 27 Nov 2020 16:25:41 +0100 Subject: [PATCH 14/20] [835] Auto-save in new Item Submission form breaks the form Submission form Save button disabled when no pending operations are present --- .../json-patch-operations.service.spec.ts | 30 +++++++++++++++++-- .../json-patch-operations.service.ts | 12 ++++++++ .../shared/testing/submission-service.stub.ts | 1 + .../submission-form-footer.component.html | 2 +- .../submission-form-footer.component.spec.ts | 16 ++++++++++ .../submission-form-footer.component.ts | 6 ++++ src/app/submission/submission.service.spec.ts | 18 +++++++++++ src/app/submission/submission.service.ts | 14 ++++++++- 8 files changed, 95 insertions(+), 4 deletions(-) diff --git a/src/app/core/json-patch/json-patch-operations.service.spec.ts b/src/app/core/json-patch/json-patch-operations.service.spec.ts index 5055fabbd1..1107d27e56 100644 --- a/src/app/core/json-patch/json-patch-operations.service.spec.ts +++ b/src/app/core/json-patch/json-patch-operations.service.spec.ts @@ -1,9 +1,8 @@ -import { getTestScheduler } from 'jasmine-marbles'; +import { getTestScheduler, hot } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; import { of as observableOf } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { Store } from '@ngrx/store'; - import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { RequestService } from '../data/request.service'; import { SubmissionPatchRequest } from '../data/request.models'; @@ -22,6 +21,7 @@ import { } from './json-patch-operations.actions'; import { RequestEntry } from '../data/request.reducer'; import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { _deepClone } from 'fast-json-patch/lib/helpers'; class TestService extends JsonPatchOperationsService { protected linkPath = ''; @@ -196,6 +196,32 @@ describe('JsonPatchOperationsService test suite', () => { }); }); + describe('hasPendingOperations', () => { + + it('should return true when there are pending operations', () => { + + const expected = hot('(x|)', { x: true }); + + const result = service.hasPendingOperations(testJsonPatchResourceType); + expect(result).toBeObservable(expected); + + }); + + it('should return false when there are not pending operations', () => { + + const mockStateNoOp = _deepClone(mockState); + mockStateNoOp['json/patch'][testJsonPatchResourceType].children = []; + store.select.and.returnValue(observableOf(mockStateNoOp['json/patch'][testJsonPatchResourceType])); + + const expected = hot('(x|)', { x: false }); + + const result = service.hasPendingOperations(testJsonPatchResourceType); + expect(result).toBeObservable(expected); + + }); + + }); + describe('jsonPatchByResourceID', () => { it('should call submitJsonPatchOperations method', () => { diff --git a/src/app/core/json-patch/json-patch-operations.service.ts b/src/app/core/json-patch/json-patch-operations.service.ts index 6646e67862..5cf3d503a6 100644 --- a/src/app/core/json-patch/json-patch-operations.service.ts +++ b/src/app/core/json-patch/json-patch-operations.service.ts @@ -161,6 +161,18 @@ export abstract class JsonPatchOperationsService { + return this.store.select(jsonPatchOperationsByResourceType(resourceType)).pipe( + map((val) => !isEmpty(val) && Object.values(val.children) + .filter((section) => !isEmpty((section as any).body)).length > 0), + distinctUntilChanged(), + ); + } + /** * Make a new JSON Patch request with all operations related to the specified resource id * diff --git a/src/app/shared/testing/submission-service.stub.ts b/src/app/shared/testing/submission-service.stub.ts index 35c3ddfee0..93192371c6 100644 --- a/src/app/shared/testing/submission-service.stub.ts +++ b/src/app/shared/testing/submission-service.stub.ts @@ -20,6 +20,7 @@ export class SubmissionServiceStub { getSubmissionStatus = jasmine.createSpy('getSubmissionStatus'); getSubmissionSaveProcessingStatus = jasmine.createSpy('getSubmissionSaveProcessingStatus'); getSubmissionDepositProcessingStatus = jasmine.createSpy('getSubmissionDepositProcessingStatus'); + hasNotSavedModification = jasmine.createSpy('hasNotSavedModification'); isSectionHidden = jasmine.createSpy('isSectionHidden'); isSubmissionLoading = jasmine.createSpy('isSubmissionLoading'); notifyNewSection = jasmine.createSpy('notifyNewSection'); diff --git a/src/app/submission/form/footer/submission-form-footer.component.html b/src/app/submission/form/footer/submission-form-footer.component.html index 938c81a33f..29bfff2660 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.html +++ b/src/app/submission/form/footer/submission-form-footer.component.html @@ -12,7 +12,7 @@ diff --git a/src/app/submission/form/footer/submission-form-footer.component.spec.ts b/src/app/submission/form/footer/submission-form-footer.component.spec.ts index c8860e3541..aa5e7f996b 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.spec.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.spec.ts @@ -224,6 +224,22 @@ describe('SubmissionFormFooterComponent Component', () => { expect(depositBtn.nativeElement.disabled).toBeFalsy(); }); + it('should disable save button when all modifications had been saved', () => { + comp.hasNotSavedModification = observableOf(false); + fixture.detectChanges(); + + const saveBtn: any = fixture.debugElement.query(By.css('#save')); + expect(saveBtn.nativeElement.disabled).toBeTruthy(); + }); + + it('should enable save button when there are not saved modifications', () => { + comp.hasNotSavedModification = observableOf(true); + fixture.detectChanges(); + + const saveBtn: any = fixture.debugElement.query(By.css('#save')); + expect(saveBtn.nativeElement.disabled).toBeFalsy(); + }); + }); }); diff --git a/src/app/submission/form/footer/submission-form-footer.component.ts b/src/app/submission/form/footer/submission-form-footer.component.ts index 0636c3f6d3..be1faf57ec 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.ts @@ -49,6 +49,11 @@ export class SubmissionFormFooterComponent implements OnChanges { */ public submissionIsInvalid: Observable = observableOf(true); + /** + * A boolean representing if submission form has unsaved modifications + */ + public hasNotSavedModification: Observable; + /** * Initialize instance variables * @@ -73,6 +78,7 @@ export class SubmissionFormFooterComponent implements OnChanges { this.processingSaveStatus = this.submissionService.getSubmissionSaveProcessingStatus(this.submissionId); this.processingDepositStatus = this.submissionService.getSubmissionDepositProcessingStatus(this.submissionId); this.showDepositAndDiscard = observableOf(this.submissionService.getSubmissionScope() === SubmissionScopeType.WorkspaceItem); + this.hasNotSavedModification = this.submissionService.hasNotSavedModification(); } } diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index 6455638eef..f601239387 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -46,6 +46,8 @@ import { SearchService } from '../core/shared/search/search.service'; import { Item } from '../core/shared/item.model'; import { storeModuleConfig } from '../app.reducer'; import { environment } from '../../environments/environment'; +import { SubmissionJsonPatchOperationsService } from '../core/submission/submission-json-patch-operations.service'; +import { SubmissionJsonPatchOperationsServiceStub } from '../shared/testing/submission-json-patch-operations-service.stub'; describe('SubmissionService test suite', () => { const collectionId = '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f'; @@ -345,6 +347,7 @@ describe('SubmissionService test suite', () => { const router = new RouterMock(); const selfUrl = 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826'; const submissionDefinition: any = mockSubmissionDefinition; + const submissionJsonPatchOperationsService = new SubmissionJsonPatchOperationsServiceStub(); let scheduler: TestScheduler; let service: SubmissionService; @@ -371,6 +374,7 @@ describe('SubmissionService test suite', () => { { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, { provide: SearchService, useValue: searchService }, { provide: RequestService, useValue: requestServce }, + { provide: SubmissionJsonPatchOperationsService, useValue: submissionJsonPatchOperationsService }, NotificationsService, RouteService, SubmissionService, @@ -753,6 +757,20 @@ describe('SubmissionService test suite', () => { }); }); + describe('hasNotSavedModification', () => { + it('should call jsonPatchOperationService hasPendingOperation observable', () => { + (service as any).jsonPatchOperationService.hasPendingOperations = jasmine.createSpy('hasPendingOperations') + .and.returnValue(observableOf(true)); + + scheduler = getTestScheduler(); + scheduler.schedule(() => service.hasNotSavedModification()); + scheduler.flush(); + + expect((service as any).jsonPatchOperationService.hasPendingOperations).toHaveBeenCalledWith('sections'); + + }); + }); + describe('isSectionHidden', () => { it('should return true/false when section is hidden/visible', () => { let section: any = { diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index 0277ac6e5a..f7bfa3fbed 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -45,6 +45,7 @@ import { RequestService } from '../core/data/request.service'; import { SearchService } from '../core/shared/search/search.service'; import { Item } from '../core/shared/item.model'; import { environment } from '../../environments/environment'; +import { SubmissionJsonPatchOperationsService } from '../core/submission/submission-json-patch-operations.service'; /** * A service that provides methods used in submission process. @@ -82,7 +83,8 @@ export class SubmissionService { protected store: Store, protected translate: TranslateService, protected searchService: SearchService, - protected requestService: RequestService) { + protected requestService: RequestService, + protected jsonPatchOperationService: SubmissionJsonPatchOperationsService) { } /** @@ -429,6 +431,16 @@ export class SubmissionService { startWith(false)); } + /** + * Return whether submission unsaved modification are present + * + * @return Observable + * observable with submission unsaved modification presence + */ + hasNotSavedModification(): Observable { + return this.jsonPatchOperationService.hasPendingOperations('sections'); + } + /** * Return the visibility status of the specified section * From 0794c50d19ad47eaf8e99f960e8b31d2550481d4 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Thu, 3 Dec 2020 11:06:38 +0100 Subject: [PATCH 15/20] [835] Auto-save in new Item Submission form breaks the form Fixed unused imports. --- src/app/submission/sections/form/section-form.component.ts | 7 +------ src/app/submission/sections/sections.service.ts | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index 02907df16a..5a04187ea3 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -174,14 +174,9 @@ export class SubmissionSectionformComponent extends SectionModelComponent { onSectionInit() { this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id); this.formId = this.formService.getUniqueId(this.sectionData.id); -<<<<<<< HEAD + this.sectionService.dispatchSetSectionFormId(this.submissionId, this.sectionData.id, this.formId); this.formConfigService.findByHref(this.sectionData.config).pipe( map((configData: RemoteData) => configData.payload), -======= - this.sectionService.dispatchSetSectionFormId(this.submissionId, this.sectionData.id, this.formId); - this.formConfigService.getConfigByHref(this.sectionData.config).pipe( - map((configData: ConfigData) => configData.payload), ->>>>>>> [835] Auto-save in new Item Submission form breaks the form tap((config: SubmissionFormsModel) => this.formConfig = config), flatMap(() => observableCombineLatest( diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts index 7b9701671c..a38d7d6981 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, isNotNull, isNotUndefined } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; import { DisableSectionAction, EnableSectionAction, From 451881ef08c8bdaa57f11545fa1b253ce24753c3 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Tue, 15 Dec 2020 10:35:55 +0100 Subject: [PATCH 16/20] [835] Auto-save in new Item Submission form breaks the form Methods renaming --- .../builder/ds-dynamic-form-ui/ds-dynamic-form.component.html | 2 +- src/app/shared/testing/submission-service.stub.ts | 2 +- .../form/footer/submission-form-footer.component.html | 2 +- .../form/footer/submission-form-footer.component.spec.ts | 4 ++-- .../form/footer/submission-form-footer.component.ts | 4 ++-- src/app/submission/submission.service.spec.ts | 4 ++-- src/app/submission/submission.service.ts | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html index 5684f4eac9..8b84b592d7 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html @@ -2,7 +2,7 @@ [formId]="formId" [group]="formGroup" [hasErrorMessaging]="model.hasErrorMessages" - [hidden]="model.hidden" + [hidden]="model.hidden"linke [layout]="formLayout" [model]="model" [ngClass]="[getClass(model, 'element', 'host'), getClass(model, 'grid', 'host')]" diff --git a/src/app/shared/testing/submission-service.stub.ts b/src/app/shared/testing/submission-service.stub.ts index 93192371c6..d9d28bde0e 100644 --- a/src/app/shared/testing/submission-service.stub.ts +++ b/src/app/shared/testing/submission-service.stub.ts @@ -20,7 +20,7 @@ export class SubmissionServiceStub { getSubmissionStatus = jasmine.createSpy('getSubmissionStatus'); getSubmissionSaveProcessingStatus = jasmine.createSpy('getSubmissionSaveProcessingStatus'); getSubmissionDepositProcessingStatus = jasmine.createSpy('getSubmissionDepositProcessingStatus'); - hasNotSavedModification = jasmine.createSpy('hasNotSavedModification'); + hasUnsavedModification = jasmine.createSpy('hasUnsavedModification'); isSectionHidden = jasmine.createSpy('isSectionHidden'); isSubmissionLoading = jasmine.createSpy('isSubmissionLoading'); notifyNewSection = jasmine.createSpy('notifyNewSection'); diff --git a/src/app/submission/form/footer/submission-form-footer.component.html b/src/app/submission/form/footer/submission-form-footer.component.html index 29bfff2660..459241ae1c 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.html +++ b/src/app/submission/form/footer/submission-form-footer.component.html @@ -12,7 +12,7 @@ diff --git a/src/app/submission/form/footer/submission-form-footer.component.spec.ts b/src/app/submission/form/footer/submission-form-footer.component.spec.ts index aa5e7f996b..704346f445 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.spec.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.spec.ts @@ -225,7 +225,7 @@ describe('SubmissionFormFooterComponent Component', () => { }); it('should disable save button when all modifications had been saved', () => { - comp.hasNotSavedModification = observableOf(false); + comp.hasUnsavedModification = observableOf(false); fixture.detectChanges(); const saveBtn: any = fixture.debugElement.query(By.css('#save')); @@ -233,7 +233,7 @@ describe('SubmissionFormFooterComponent Component', () => { }); it('should enable save button when there are not saved modifications', () => { - comp.hasNotSavedModification = observableOf(true); + comp.hasUnsavedModification = observableOf(true); fixture.detectChanges(); const saveBtn: any = fixture.debugElement.query(By.css('#save')); diff --git a/src/app/submission/form/footer/submission-form-footer.component.ts b/src/app/submission/form/footer/submission-form-footer.component.ts index be1faf57ec..9cabdcbf6d 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.ts @@ -52,7 +52,7 @@ export class SubmissionFormFooterComponent implements OnChanges { /** * A boolean representing if submission form has unsaved modifications */ - public hasNotSavedModification: Observable; + public hasUnsavedModification: Observable; /** * Initialize instance variables @@ -78,7 +78,7 @@ export class SubmissionFormFooterComponent implements OnChanges { this.processingSaveStatus = this.submissionService.getSubmissionSaveProcessingStatus(this.submissionId); this.processingDepositStatus = this.submissionService.getSubmissionDepositProcessingStatus(this.submissionId); this.showDepositAndDiscard = observableOf(this.submissionService.getSubmissionScope() === SubmissionScopeType.WorkspaceItem); - this.hasNotSavedModification = this.submissionService.hasNotSavedModification(); + this.hasUnsavedModification = this.submissionService.hasUnsavedModification(); } } diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index f601239387..3894787e48 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -757,13 +757,13 @@ describe('SubmissionService test suite', () => { }); }); - describe('hasNotSavedModification', () => { + describe('hasUnsavedModification', () => { it('should call jsonPatchOperationService hasPendingOperation observable', () => { (service as any).jsonPatchOperationService.hasPendingOperations = jasmine.createSpy('hasPendingOperations') .and.returnValue(observableOf(true)); scheduler = getTestScheduler(); - scheduler.schedule(() => service.hasNotSavedModification()); + scheduler.schedule(() => service.hasUnsavedModification()); scheduler.flush(); expect((service as any).jsonPatchOperationService.hasPendingOperations).toHaveBeenCalledWith('sections'); diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index f7bfa3fbed..92f0e116ea 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -437,7 +437,7 @@ export class SubmissionService { * @return Observable * observable with submission unsaved modification presence */ - hasNotSavedModification(): Observable { + hasUnsavedModification(): Observable { return this.jsonPatchOperationService.hasPendingOperations('sections'); } From e8255927c5e07c3b283f631e540f1fd1e1876151 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Tue, 15 Dec 2020 10:39:13 +0100 Subject: [PATCH 17/20] [835] Auto-save in new Item Submission form breaks the form Autosave timer switched to milliseconds. --- src/app/submission/submission.service.spec.ts | 2 +- src/app/submission/submission.service.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index 3894787e48..579d1c0624 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -948,7 +948,7 @@ describe('SubmissionService test suite', () => { }); it('should start Auto Save', fakeAsync(() => { - const duration = environment.submission.autosave.timer * (1000 * 60); + const duration = environment.submission.autosave.timer; service.startAutoSave('826'); const sub = (service as any).timer$.subscribe(); diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index 92f0e116ea..87d6cbc9a4 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -581,8 +581,7 @@ export class SubmissionService { } // AUTOSAVE submission - // Retrieve interval from config and convert to milliseconds - const duration = environment.submission.autosave.timer * (1000 * 60); + const duration = environment.submission.autosave.timer; // Dispatch save action after given duration this.timer$ = observableTimer(duration, duration); this.autoSaveSub = this.timer$ From 042d2e71f0e01cb39fbbd49b996ca3d7da449597 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Tue, 15 Dec 2020 11:38:46 +0100 Subject: [PATCH 18/20] [835] Auto-save in new Item Submission form breaks the form Improved form touched state in ngrx store. --- .../form/builder/form-builder.service.ts | 2 +- src/app/shared/form/form.actions.ts | 25 ++++---- src/app/shared/form/form.component.spec.ts | 2 +- src/app/shared/form/form.reducer.spec.ts | 62 +++++++------------ src/app/shared/form/form.reducer.ts | 35 +++++------ src/app/shared/form/form.service.spec.ts | 2 +- src/app/shared/form/form.service.ts | 16 ++--- .../submission-objects.effects.spec.ts | 6 +- .../objects/submission-objects.effects.ts | 14 +++-- 9 files changed, 75 insertions(+), 89 deletions(-) diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index a6100e7ce7..c621fe2aa7 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -384,7 +384,7 @@ export class FormBuilderService extends DynamicFormService { const result = iterateControlModels([model]); - return result; + return Object.keys(result); } } diff --git a/src/app/shared/form/form.actions.ts b/src/app/shared/form/form.actions.ts index 76d9fef253..5733cb90bc 100644 --- a/src/app/shared/form/form.actions.ts +++ b/src/app/shared/form/form.actions.ts @@ -13,7 +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_ADD_TOUCHED: type('dspace/form/FORM_ADD_TOUCHED'), 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'), @@ -28,7 +28,6 @@ export class FormInitAction implements Action { formId: string; formData: any; valid: boolean; - formAdditional: any; }; /** @@ -41,8 +40,8 @@ export class FormInitAction implements Action { * @param valid * the Form validation status */ - constructor(formId: string, formData: any, valid: boolean, formAdditional?: any) { - this.payload = {formId, formData, valid, formAdditional}; + constructor(formId: string, formData: any, valid: boolean) { + this.payload = {formId, formData, valid}; } } @@ -66,23 +65,23 @@ export class FormChangeAction implements Action { } } -export class FormSetAdditionalAction implements Action { - type = FormActionTypes.FORM_ADDITIONAL; +export class FormAddTouchedAction implements Action { + type = FormActionTypes.FORM_ADD_TOUCHED; payload: { formId: string; - additionalData: any; + touched: string[]; }; /** - * Create a new FormSetAdditionalAction + * Create a new FormAddTouchedAction * * @param formId * the Form's ID - * @param additionalData - * the additionalData Object + * @param touched + * the array containing new touched fields */ - constructor(formId: string, additionalData: any) { - this.payload = {formId, additionalData}; + constructor(formId: string, touched: string[]) { + this.payload = {formId, touched}; } } @@ -169,7 +168,7 @@ export class FormClearErrorsAction implements Action { */ export type FormAction = FormInitAction | FormChangeAction - | FormSetAdditionalAction + | FormAddTouchedAction | FormRemoveAction | FormStatusChangeAction | FormAddError diff --git a/src/app/shared/form/form.component.spec.ts b/src/app/shared/form/form.component.spec.ts index e7193246fb..bda2b3e38c 100644 --- a/src/app/shared/form/form.component.spec.ts +++ b/src/app/shared/form/form.component.spec.ts @@ -120,7 +120,7 @@ function init() { }, valid: false, errors: [], - additional: {} + touched: {} } }; diff --git a/src/app/shared/form/form.reducer.spec.ts b/src/app/shared/form/form.reducer.spec.ts index ff89ae27cb..5547eee7a4 100644 --- a/src/app/shared/form/form.reducer.spec.ts +++ b/src/app/shared/form/form.reducer.spec.ts @@ -6,7 +6,7 @@ import { FormInitAction, FormRemoveAction, FormRemoveErrorAction, - FormSetAdditionalAction, + FormAddTouchedAction, FormStatusChangeAction } from './form.actions'; @@ -23,7 +23,7 @@ describe('formReducer', () => { }, valid: false, errors: [], - additional: {} + touched: {} } }; const formId = 'testForm'; @@ -51,7 +51,7 @@ describe('formReducer', () => { }, valid: false, errors: [], - additional: {} + touched: {} } }; const formId = 'testForm'; @@ -71,7 +71,7 @@ describe('formReducer', () => { }, valid: false, errors: [], - additional: {} + touched: {} } }; @@ -93,7 +93,7 @@ describe('formReducer', () => { }, valid: false, errors: [], - additional: {} + touched: {} } }; const state = { @@ -106,7 +106,7 @@ describe('formReducer', () => { }, valid: false, errors: [], - additional: {} + touched: {} } }; const formId = 'testForm'; @@ -134,7 +134,7 @@ describe('formReducer', () => { }, valid: false, errors: [], - additional: {} + touched: {} } }; const state = { @@ -147,7 +147,7 @@ describe('formReducer', () => { }, valid: true, errors: [], - additional: {} + touched: {} } }; const formId = 'testForm'; @@ -169,7 +169,7 @@ describe('formReducer', () => { }, valid: true, errors: [], - additional: {} + touched: {} } }; @@ -214,7 +214,7 @@ describe('formReducer', () => { message: 'error.validation.required' } ], - additional: {} + touched: {} } }; @@ -247,7 +247,7 @@ describe('formReducer', () => { }, valid: true, errors: [], - additional: {} + touched: {} } }; @@ -276,7 +276,7 @@ describe('formReducer', () => { message: 'error.validation.required' } ], - additional: {} + touched: {} } }; @@ -299,7 +299,7 @@ describe('formReducer', () => { }, valid: false, errors: [], - additional: {} + touched: {} } }; const state = { @@ -312,21 +312,15 @@ describe('formReducer', () => { }, valid: false, errors: [], - additional: { - touched: { - title: true - } + touched: { + title: true } } }; const formId = 'testForm'; - const additionalData = { - touched: { - title: true - } - }; + const touched = ['title']; - const action = new FormSetAdditionalAction(formId, additionalData); + const action = new FormAddTouchedAction(formId, touched); const newState = formReducer(initState, action); expect(newState).toEqual(state); @@ -343,10 +337,8 @@ describe('formReducer', () => { }, valid: false, errors: [], - additional: { - touched: { - title: true - } + touched: { + title: true } } }; @@ -360,22 +352,16 @@ describe('formReducer', () => { }, valid: false, errors: [], - additional: { - touched: { - title: true, - author: true - } + touched: { + title: true, + author: true } } }; const formId = 'testForm'; - const additionalData = { - touched: { - author: true - } - }; + const touched = ['author']; - const action = new FormSetAdditionalAction(formId, additionalData); + const action = new FormAddTouchedAction(formId, touched); const newState = formReducer(initState, action); expect(newState).toEqual(state); diff --git a/src/app/shared/form/form.reducer.ts b/src/app/shared/form/form.reducer.ts index 2a2b3cd2de..b5b4090ef9 100644 --- a/src/app/shared/form/form.reducer.ts +++ b/src/app/shared/form/form.reducer.ts @@ -4,9 +4,8 @@ import { FormAddError, FormChangeAction, FormClearErrorsAction, FormInitAction, - FormRemoveAction, - FormRemoveErrorAction, FormSetAdditionalAction, - FormStatusChangeAction + FormRemoveAction, FormRemoveErrorAction, + FormStatusChangeAction, FormAddTouchedAction } from './form.actions'; import { hasValue } from '../empty.util'; import { isEqual, uniqWith } from 'lodash'; @@ -17,11 +16,15 @@ export interface FormError { fieldIndex: number; } +export interface FormTouchedState { + [key: string]: boolean +} + export interface FormEntry { data: any; valid: boolean; errors: FormError[]; - additional: any; + touched: FormTouchedState; } export interface FormState { @@ -41,8 +44,8 @@ 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_ADD_TOUCHED: { + return changeTouchedState(state, action as FormAddTouchedAction); } case FormActionTypes.FORM_REMOVE: { @@ -132,8 +135,8 @@ function initForm(state: FormState, action: FormInitAction): FormState { const formState = { data: action.payload.formData, valid: action.payload.valid, + touched: {}, errors: [], - additional: action.payload.formAdditional ? action.payload.formAdditional : {} }; if (!hasValue(state[action.payload.formId])) { return Object.assign({}, state, { @@ -220,25 +223,19 @@ function removeForm(state: FormState, action: FormRemoveAction): FormState { } /** - * Compute the additional data state of the form. New touched fields are merged with the previous ones. + * Compute the touched state of the form. New touched fields are merged with the previous ones. * @param state * @param action */ -function additionalData(state: FormState, action: FormSetAdditionalAction): FormState { +function changeTouchedState(state: FormState, action: FormAddTouchedAction): 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 newForm = Object.assign({}, newState[action.payload.formId]); + newState[action.payload.formId] = newForm; - 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 - } - ); + newForm.touched = { ... newForm.touched}; + action.payload.touched.forEach((field) => newForm[field] = true); return newState; } else { diff --git a/src/app/shared/form/form.service.spec.ts b/src/app/shared/form/form.service.spec.ts index 031c83e457..a0f7f9980b 100644 --- a/src/app/shared/form/form.service.spec.ts +++ b/src/app/shared/form/form.service.spec.ts @@ -85,7 +85,7 @@ describe('FormService test suite', () => { data: formData, valid: false, errors: [], - additional: {} + touched: {} } }; diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index 8983bade44..b895a188a4 100644 --- a/src/app/shared/form/form.service.ts +++ b/src/app/shared/form/form.service.ts @@ -1,5 +1,5 @@ import { map, distinctUntilChanged, filter } from 'rxjs/operators'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; import { select, Store } from '@ngrx/store'; @@ -7,16 +7,16 @@ import { select, Store } from '@ngrx/store'; import { AppState } from '../../app.reducer'; import { formObjectFromIdSelector } from './selectors'; import { FormBuilderService } from './builder/form-builder.service'; -import {DynamicFormControlEvent, 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, FormSetAdditionalAction, + FormRemoveAction, FormRemoveErrorAction, FormAddTouchedAction, FormStatusChangeAction } from './form.actions'; -import { FormEntry } from './form.reducer'; +import { FormEntry, FormTouchedState } from './form.reducer'; import { environment } from '../../../environments/environment'; @Injectable() @@ -52,13 +52,13 @@ export class FormService { } /** - * Method to retrieve form's additional data from state + * Method to retrieve form's touched state */ - public getFormAdditionalData(formId: string): Observable { + public getFormTouchedState(formId: string): Observable { return this.store.pipe( select(formObjectFromIdSelector(formId)), filter((state) => isNotUndefined(state)), - map((state) => state.additional), + map((state) => state.touched), distinctUntilChanged() ); } @@ -183,7 +183,7 @@ export class FormService { public setTouched(formId: string, model: DynamicFormControlModel[], event: DynamicFormControlEvent) { const ids = this.formBuilderService.getMetadataIdsFromEvent(event); - this.store.dispatch(new FormSetAdditionalAction(formId, { touched: ids})); + this.store.dispatch(new FormAddTouchedAction(formId, ids)); } public removeError(formId: string, eventModelId: string, fieldIndex: number) { diff --git a/src/app/submission/objects/submission-objects.effects.spec.ts b/src/app/submission/objects/submission-objects.effects.spec.ts index 0f0f2aa7d7..477a84531e 100644 --- a/src/app/submission/objects/submission-objects.effects.spec.ts +++ b/src/app/submission/objects/submission-objects.effects.spec.ts @@ -384,10 +384,8 @@ describe('SubmissionObjectEffects test suite', () => { }, forms: { '2_traditionalpageone': { - additional: { - touched: { - 'dc.title': true - } + touched: { + 'dc.title': true } } } diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index 9e2db13776..a4b3a94ff8 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -59,6 +59,7 @@ import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators' import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service'; import { followLink } from '../../shared/utils/follow-link-config.model'; import parseSectionErrorPaths, {SectionErrorPath} from '../utils/parseSectionErrorPaths'; +import { FormState } from '../../shared/form/form.reducer'; @Injectable() export class SubmissionObjectEffects { @@ -431,24 +432,29 @@ function getForm(forms, currentState, sectionId) { /** * Filter sectionErrors accordingly to this rules: * 1. if notifications are enabled return all errors - * 2. if sectionType is different from submission-form return all errors + * 2. if sectionType is different from 'submission-form' return all errors * 3. otherwise return errors only for those fields marked as touched inside the section form * @param sectionForm + * The form related to the section * @param sectionErrors + * The section errors array + * @param sectionType + * The section type * @param notify + * Whether notifications are enabled */ -function filterErrors(sectionForm, sectionErrors, sectionType, notify): any { +function filterErrors(sectionForm: FormState, sectionErrors: SubmissionSectionError[], sectionType: string, notify: boolean): any { if (notify || sectionType !== SectionsType.SubmissionForm) { return sectionErrors; } - if (!sectionForm || !sectionForm.additional || !sectionForm.additional.touched) { + if (!sectionForm || !sectionForm.touched) { return []; } const filteredErrors = []; sectionErrors.forEach((error: SubmissionSectionError) => { const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path); errorPaths.forEach((path: SectionErrorPath) => { - if (path.fieldId && sectionForm.additional.touched[path.fieldId]) { + if (path.fieldId && sectionForm.touched[path.fieldId]) { filteredErrors.push(error); } }); From 8e77fac63876393f33eb88b8856b29266b981605 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 21 Dec 2020 19:21:49 +0100 Subject: [PATCH 19/20] [835] Auto-save in new Item Submission form breaks the form Minor fixes and method computeSectionConfiguredMetadata tested --- .../ds-dynamic-form.component.html | 2 +- src/app/shared/form/form.reducer.ts | 2 +- .../objects/submission-objects.effects.ts | 2 +- .../objects/submission-objects.reducer.ts | 6 ++++-- .../sections/form/section-form.component.ts | 2 +- .../sections/sections.service.spec.ts | 21 +++++++++++++++++++ 6 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html index 8b84b592d7..5684f4eac9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html @@ -2,7 +2,7 @@ [formId]="formId" [group]="formGroup" [hasErrorMessaging]="model.hasErrorMessages" - [hidden]="model.hidden"linke + [hidden]="model.hidden" [layout]="formLayout" [model]="model" [ngClass]="[getClass(model, 'element', 'host'), getClass(model, 'grid', 'host')]" diff --git a/src/app/shared/form/form.reducer.ts b/src/app/shared/form/form.reducer.ts index b5b4090ef9..59b942ebc3 100644 --- a/src/app/shared/form/form.reducer.ts +++ b/src/app/shared/form/form.reducer.ts @@ -235,7 +235,7 @@ function changeTouchedState(state: FormState, action: FormAddTouchedAction): For newState[action.payload.formId] = newForm; newForm.touched = { ... newForm.touched}; - action.payload.touched.forEach((field) => newForm[field] = true); + action.payload.touched.forEach((field) => newForm.touched[field] = true); return newState; } else { diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index a4b3a94ff8..4f6563c662 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -443,7 +443,7 @@ function getForm(forms, currentState, sectionId) { * @param notify * Whether notifications are enabled */ -function filterErrors(sectionForm: FormState, sectionErrors: SubmissionSectionError[], sectionType: string, notify: boolean): any { +function filterErrors(sectionForm: FormState, sectionErrors: SubmissionSectionError[], sectionType: string, notify: boolean): SubmissionSectionError[] { if (notify || sectionType !== SectionsType.SubmissionForm) { return sectionErrors; } diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts index 91caeb1e77..9d2030ce25 100644 --- a/src/app/submission/objects/submission-objects.reducer.ts +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -695,7 +695,7 @@ function setSectionFormId(state: SubmissionObjectState, action: SetSectionFormId */ function updateSectionData(state: SubmissionObjectState, action: UpdateSectionDataAction): SubmissionObjectState { if (isNotEmpty(state[ action.payload.submissionId ]) - && isNotEmpty(state[ action.payload.submissionId ].sections[ action.payload.sectionId])) { + && isNotEmpty(state[ action.payload.submissionId ].sections[ action.payload.sectionId])) { return Object.assign({}, state, { [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { sections: Object.assign({}, state[ action.payload.submissionId ].sections, { @@ -718,8 +718,10 @@ function updateSectionData(state: SubmissionObjectState, action: UpdateSectionDa * Keep the existent otherwise. * @param newMetadata * @param oldMetadata + * @return + * new sectionMetadata value */ -function reduceSectionMetadata(newMetadata: string[], oldMetadata: string[]) { +function reduceSectionMetadata(newMetadata: string[], oldMetadata: string[]): string[] { if (newMetadata) { return newMetadata; } diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index 5a04187ea3..6e1e6d52f3 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -240,7 +240,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent { const sectionDataToCheck = {}; Object.keys(sectionData).forEach((key) => { - if (this.sectionMetadata.includes(key)) { + if (this.sectionMetadata && this.sectionMetadata.includes(key)) { sectionDataToCheck[key] = sectionData[key]; } }) diff --git a/src/app/submission/sections/sections.service.spec.ts b/src/app/submission/sections/sections.service.spec.ts index 5c7bff13ce..75b6dfe67e 100644 --- a/src/app/submission/sections/sections.service.spec.ts +++ b/src/app/submission/sections/sections.service.spec.ts @@ -380,4 +380,25 @@ describe('SectionsService test suite', () => { expect(store.dispatch).toHaveBeenCalledWith(new UpdateSectionDataAction(submissionId, sectionId, data, [])); }); }); + + describe('computeSectionConfiguredMetadata', () => { + it('should return the configured metadata of the section from the form configuration', () => { + + const formConfig = { + rows: [{ + fields: [{ + selectableMetadata: [{ + metadata: 'dc.contributor.author' + }] + }] + }] + } + + const expectedConfiguredMetadata = [ 'dc.contributor.author' ]; + + const configuredMetadata = service.computeSectionConfiguredMetadata(formConfig as any); + + expect(configuredMetadata).toEqual(expectedConfiguredMetadata); + }); + }); }); From 6cf6dee5e61a2c18b839f8eca2928afceb2566f6 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Tue, 22 Dec 2020 08:33:23 +0100 Subject: [PATCH 20/20] [835] Auto-save in new Item Submission form breaks the form Fixed unused imports. --- src/app/submission/sections/form/section-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index 6e1e6d52f3..1f37be89e2 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -20,7 +20,7 @@ import { FormComponent } from '../../../shared/form/form.component'; import { FormService } from '../../../shared/form/form.service'; import { SectionModelComponent } from '../models/section.model'; import { SubmissionFormsConfigService } from '../../../core/config/submission-forms-config.service'; -import { hasNoValue, hasValue, isEmpty, isNotEmpty, isUndefined } from '../../../shared/empty.util'; +import { hasNoValue, hasValue, isNotEmpty, isUndefined } from '../../../shared/empty.util'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; import {