From af280098deec0b075ec6df3ba63e72708898f46f Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 16 Nov 2020 16:15:00 +0100 Subject: [PATCH 01/14] [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 51cfa655628..5824843ddf3 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -917,6 +917,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); @@ -932,6 +939,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 280b4114801..934464d95a5 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -565,6 +565,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 b6d9014ecae61a3e49447edbced4d434003c21fc Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 16 Nov 2020 16:15:26 +0100 Subject: [PATCH 02/14] [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 32ccc3b6963..8ac2ca2d237 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -18,7 +18,7 @@ From 8e46b2b5bc0a6e93504f59ffd1aa1587a4bb9e2d Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 16 Nov 2020 18:23:51 +0100 Subject: [PATCH 03/14] [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 c35968c0a07..7a9d6977e81 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 2a69a61a8c8..0db853e5612 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 98367c1666eab250fdc8894acc993097e1090cf8 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 16 Nov 2020 19:09:17 +0100 Subject: [PATCH 04/14] [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 35c3ddfee08..7550201c9fa 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 d9d58aa4f2d..8f278fe17ae 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 1b885b98b86..96179430b80 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 73c070846c3..2eed4f2c924 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 7a9d6977e81..ae0fa04ed56 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 0db853e5612..f287f55e081 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 5824843ddf3..ee9575574c3 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -496,6 +496,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 934464d95a5..8de2df67f57 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -205,6 +205,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 bf0bb0ca0203732d84427f93e3f737388146abec Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Thu, 19 Nov 2020 15:47:03 +0100 Subject: [PATCH 05/14] [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 8703a3af515..256e6574743 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 3eb3fb2716a..76d9fef2532 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 1d44375c0d3..8a5c2a57b8f 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 2b0815a40e1..6d208b08d27 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 2eed4f2c924..6fe42a149a9 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 f287f55e081..34ce657dbc9 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -289,7 +289,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 098160c737d..6f7098532e4 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 166e52675b0..ab549315edc 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); } }); @@ -249,6 +275,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 = { @@ -270,15 +299,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); } @@ -295,6 +328,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(); @@ -315,6 +351,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 */ @@ -362,6 +404,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)) { @@ -373,6 +416,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 5aa3c1d3eab..895bde13ecb 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 d255ae767ebb97a1459e50f13c9d4eb7e6c92c7f Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Thu, 19 Nov 2020 15:51:52 +0100 Subject: [PATCH 06/14] [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 4f733396902..4b11420b32f 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -60,9 +60,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 b48f4eb5f139d0300ce9acde722ff62c1c175fd4 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Fri, 20 Nov 2020 15:02:56 +0100 Subject: [PATCH 07/14] [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 | 21 ++------- .../submission/sections/sections.service.ts | 13 +++++ 10 files changed, 125 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 256e6574743..f0baa3f8a3d 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 7a5d3932c8a..43f9bdfa901 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 8a5c2a57b8f..2a2b3cd2dea 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 6d208b08d27..8983bade442 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 6fe42a149a9..962f216c7d5 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 34ce657dbc9..2958b99c2c2 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)))); @@ -362,6 +366,7 @@ export class SubmissionObjectEffects { currentState: SubmissionObjectEntry, response: SubmissionObject[], submissionId: string, + forms, notify: boolean = true): SubmissionObjectAction[] { const mappedActions = []; @@ -401,10 +406,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 6f7098532e4..91caeb1e77b 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 ab549315edc..894c8b1d17c 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), tap((config: SubmissionFormsModel) => this.formConfig = config), @@ -252,7 +247,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); } }); @@ -275,7 +270,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) { @@ -328,9 +322,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(); @@ -351,12 +342,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 895bde13ecb..7b9701671ce 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 e70c202bcb88a485b0e0da46761a14b618d0d86b Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Fri, 20 Nov 2020 17:25:20 +0100 Subject: [PATCH 08/14] [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 f7a0564191c..e7193246fb7 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 01e3e6b1bad..978d4822d5b 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 ee90e377ee9..031c83e457b 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 2110d71d8e8..3b311c5e19b 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 ae0fa04ed56..90c4e6284d7 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 2958b99c2c2..af719760853 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -407,9 +407,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)); } }); @@ -418,6 +417,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 7602ad80902..d800a89aa0a 100644 --- a/src/app/submission/sections/form/section-form.component.spec.ts +++ b/src/app/submission/sections/form/section-form.component.spec.ts @@ -289,6 +289,7 @@ describe('SubmissionSectionformComponent test suite', () => { 'dc.title': [new FormFieldMetadataValueObject('test')] }; compAsAny.formData = {}; + compAsAny.sectionMetadata = ['dc.title']; expect(comp.hasMetadataEnrichment(newSectionData)).toBeTruthy(); }); @@ -298,7 +299,7 @@ describe('SubmissionSectionformComponent test suite', () => { 'dc.title': [new FormFieldMetadataValueObject('test')] }; compAsAny.formData = newSectionData; - + compAsAny.sectionMetadata = ['dc.title']; expect(comp.hasMetadataEnrichment(newSectionData)).toBeFalsy(); }); @@ -312,6 +313,7 @@ describe('SubmissionSectionformComponent test suite', () => { comp.sectionData.data = {}; comp.sectionData.errors = []; compAsAny.formData = {}; + compAsAny.sectionMetadata = ['dc.title']; comp.updateForm(sectionData, sectionError); @@ -331,10 +333,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 a3757bf44e2de3ce36665f44da3dd3ee4c1dca5c Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Fri, 20 Nov 2020 18:17:17 +0100 Subject: [PATCH 09/14] [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 f0baa3f8a3d..a6100e7ce7a 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 8ac2ca2d237..97879cc0250 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 7550201c9fa..35c3ddfee08 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 8f278fe17ae..c8860e35411 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 96179430b80..0636c3f6d3d 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 894c8b1d17c..166e52675b0 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 ef92c29adc9..d8dd5eaea2f 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -118,12 +118,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 * @@ -247,6 +241,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); } @@ -389,7 +384,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)) { @@ -401,17 +395,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 ee9575574c3..9553545d95c 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -489,16 +489,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 8de2df67f57..8a0097c5a15 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -210,26 +210,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 69dbf0aab3c37d17c39f1394c50dbc1cfcac0b28 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 23 Nov 2020 10:55:39 +0100 Subject: [PATCH 10/14] [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 af719760853..f948f6065c4 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -425,7 +425,6 @@ function getForm(forms, currentState, sectionId) { return forms[formId]; } - /** * Filter sectionErrors accordingly to this rules: * 1. if notifications are enabled return all errors From 3bc8240956738d49c330a223bb59bec921c4fae5 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 23 Nov 2020 12:58:16 +0100 Subject: [PATCH 11/14] [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 d800a89aa0a..a7ba64ba1e0 100644 --- a/src/app/submission/sections/form/section-form.component.spec.ts +++ b/src/app/submission/sections/form/section-form.component.spec.ts @@ -303,6 +303,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 d8dd5eaea2f..c2f68d04e20 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -265,7 +265,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(); @@ -347,6 +348,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 db9d612b48882355b324a0ddf9826af7a0857915 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 23 Nov 2020 12:59:22 +0100 Subject: [PATCH 12/14] [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 978d4822d5b..ff89ae27cb3 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 0644eceae9842b4e7091739783c19b3499594570 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Mon, 23 Nov 2020 14:29:10 +0100 Subject: [PATCH 13/14] [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 8965a17eef3..70e5bc3cd1a 100644 --- a/src/app/shared/mocks/submission.mock.ts +++ b/src/app/shared/mocks/submission.mock.ts @@ -59,6 +59,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: [ { @@ -1032,6 +1047,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 90c4e6284d7..0f0f2aa7d79 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 0c585e4bca7..0431cdff79d 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 4a1c1bf6c920af34d08a96df9506b24081f131ee Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Fri, 27 Nov 2020 16:25:41 +0100 Subject: [PATCH 14/14] [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 4ada78172e6..47a83375c61 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'; @@ -21,6 +20,7 @@ import { StartTransactionPatchOperationsAction } from './json-patch-operations.actions'; import { RequestEntry } from '../data/request.reducer'; +import { _deepClone } from 'fast-json-patch/lib/helpers'; class TestService extends JsonPatchOperationsService { protected linkPath = ''; @@ -179,6 +179,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 c1b37bf9049..00561e54bab 100644 --- a/src/app/core/json-patch/json-patch-operations.service.ts +++ b/src/app/core/json-patch/json-patch-operations.service.ts @@ -144,6 +144,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 35c3ddfee08..93192371c6c 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 938c81a33f0..29bfff26609 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 c8860e35411..aa5e7f996bc 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 0636c3f6d3d..be1faf57ec5 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 9553545d95c..82db47d2a4d 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -48,6 +48,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'; @@ -347,6 +349,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; @@ -373,6 +376,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, @@ -755,6 +759,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 8a0097c5a15..72507e535df 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -46,6 +46,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. @@ -83,7 +84,8 @@ export class SubmissionService { protected store: Store, protected translate: TranslateService, protected searchService: SearchService, - protected requestService: RequestService) { + protected requestService: RequestService, + protected jsonPatchOperationService: SubmissionJsonPatchOperationsService) { } /** @@ -430,6 +432,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 *