Changes in order to keep server side validation errors into the submission form state

This commit is contained in:
Giuseppe Digilio
2021-05-14 19:23:03 +02:00
parent 0724692d40
commit 44d2310cdb
20 changed files with 265 additions and 131 deletions

View File

@@ -254,6 +254,13 @@ export class FormComponent implements OnDestroy, OnInit {
onBlur(event: DynamicFormControlEvent): void { onBlur(event: DynamicFormControlEvent): void {
this.blur.emit(event); this.blur.emit(event);
const control: FormControl = event.control;
const fieldIndex: number = (event.context && event.context.index) ? event.context.index : 0;
if (control.valid) {
this.formService.removeError(this.formId, event.model.name, fieldIndex);
} else {
this.formService.addControlErrors(control, this.formId, event.model.name, fieldIndex);
}
} }
onCustomEvent(event: any) { onCustomEvent(event: any) {

View File

@@ -2,10 +2,13 @@ import {
FormAction, FormAction,
FormActionTypes, FormActionTypes,
FormAddError, FormAddError,
FormChangeAction, FormClearErrorsAction, FormAddTouchedAction,
FormChangeAction,
FormClearErrorsAction,
FormInitAction, FormInitAction,
FormRemoveAction, FormRemoveErrorAction, FormRemoveAction,
FormStatusChangeAction, FormAddTouchedAction FormRemoveErrorAction,
FormStatusChangeAction
} from './form.actions'; } from './form.actions';
import { hasValue } from '../empty.util'; import { hasValue } from '../empty.util';
import { isEqual, uniqWith } from 'lodash'; import { isEqual, uniqWith } from 'lodash';
@@ -82,12 +85,16 @@ function addFormErrors(state: FormState, action: FormAddError) {
fieldIndex: action.payload.fieldIndex, fieldIndex: action.payload.fieldIndex,
message: action.payload.errorMessage message: action.payload.errorMessage
}; };
const metadata = action.payload.fieldId.replace(/\_/g, '.');
const touched = Object.assign({}, state[formId].touched, {
[metadata]: true
});
return Object.assign({}, state, { return Object.assign({}, state, {
[formId]: { [formId]: {
data: state[formId].data, data: state[formId].data,
valid: state[formId].valid, valid: state[formId].valid,
errors: state[formId].errors ? uniqWith(state[formId].errors.concat(error), isEqual) : [].concat(error), errors: state[formId].errors ? uniqWith(state[formId].errors.concat(error), isEqual) : [].concat(error),
touched
} }
}); });
} else { } else {

View File

@@ -1,4 +1,4 @@
import { map, distinctUntilChanged, filter } from 'rxjs/operators'; import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms'; import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -11,12 +11,15 @@ import { DynamicFormControlEvent, DynamicFormControlModel } from '@ng-dynamic-fo
import { isEmpty, isNotUndefined } from '../empty.util'; import { isEmpty, isNotUndefined } from '../empty.util';
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import { import {
FormAddError,
FormAddTouchedAction,
FormChangeAction, FormChangeAction,
FormInitAction, FormInitAction,
FormRemoveAction, FormRemoveErrorAction, FormAddTouchedAction, FormRemoveAction,
FormRemoveErrorAction,
FormStatusChangeAction FormStatusChangeAction
} from './form.actions'; } from './form.actions';
import { FormEntry, FormTouchedState } from './form.reducer'; import { FormEntry, FormError, FormTouchedState } from './form.reducer';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
@Injectable() @Injectable()
@@ -66,7 +69,7 @@ export class FormService {
/** /**
* Method to retrieve form's errors from state * Method to retrieve form's errors from state
*/ */
public getFormErrors(formId: string): Observable<any> { public getFormErrors(formId: string): Observable<FormError[]> {
return this.store.pipe( return this.store.pipe(
select(formObjectFromIdSelector(formId)), select(formObjectFromIdSelector(formId)),
filter((state) => isNotUndefined(state)), filter((state) => isNotUndefined(state)),
@@ -105,6 +108,32 @@ export class FormService {
}); });
} }
public hasValidationErrors(formGroup: FormGroup | FormArray): boolean {
let hasErrors = false;
const fields: string[] = Object.keys(formGroup.controls);
for (const field of fields) {
// Object.keys(formGroup.controls).forEach((field) => {
const control = formGroup.get(field);
if (control instanceof FormControl) {
hasErrors = !control.valid && control.touched;
} else if (control instanceof FormGroup || control instanceof FormArray) {
hasErrors = this.hasValidationErrors(control);
}
if (hasErrors) {
break;
}
// });
}
return hasErrors;
}
public addControlErrors(field: AbstractControl, formId: string, fieldId: string, fieldIndex: number) {
const errors: string[] = Object.keys(field.errors)
.filter((errorKey) => field.errors[errorKey] === true)
.map((errorKey) => `error.validation.${errorKey}`);
errors.forEach((error) => this.addError(formId, fieldId, fieldIndex, error));
}
public addErrorToField(field: AbstractControl, model: DynamicFormControlModel, message: string) { public addErrorToField(field: AbstractControl, model: DynamicFormControlModel, message: string) {
const error = {}; // create the error object const error = {}; // create the error object
const errorKey = this.getValidatorNameFromMap(message); const errorKey = this.getValidatorNameFromMap(message);
@@ -186,7 +215,12 @@ export class FormService {
this.store.dispatch(new FormAddTouchedAction(formId, ids)); this.store.dispatch(new FormAddTouchedAction(formId, ids));
} }
public removeError(formId: string, eventModelId: string, fieldIndex: number) { public addError(formId: string, fieldId: string, fieldIndex: number, message: string) {
this.store.dispatch(new FormRemoveErrorAction(formId, eventModelId, fieldIndex)); const normalizedFieldId = fieldId.replace(/\./g, '_');
this.store.dispatch(new FormAddError(formId, normalizedFieldId, fieldIndex, message));
}
public removeError(formId: string, fieldId: string, fieldIndex: number) {
const normalizedFieldId = fieldId.replace(/\./g, '_');
this.store.dispatch(new FormRemoveErrorAction(formId, normalizedFieldId, fieldIndex));
} }
} }

View File

@@ -3,6 +3,7 @@
[sections]="sections" [sections]="sections"
[selfUrl]="selfUrl" [selfUrl]="selfUrl"
[submissionDefinition]="submissionDefinition" [submissionDefinition]="submissionDefinition"
[submissionErrors]="submissionErrors"
[item]="item" [item]="item"
[submissionId]="submissionId"></ds-submission-form> [submissionId]="submissionId"></ds-submission-form>
</div> </div>

View File

@@ -2,11 +2,11 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { filter, switchMap, debounceTime } from 'rxjs/operators'; import { debounceTime, filter, switchMap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model';
import { hasValue, isEmpty, isNotNull, isNotEmptyOperator } from '../../shared/empty.util'; import { hasValue, isEmpty, isNotEmptyOperator, isNotNull } from '../../shared/empty.util';
import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model';
import { SubmissionService } from '../submission.service'; import { SubmissionService } from '../submission.service';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
@@ -17,6 +17,8 @@ import { Item } from '../../core/shared/item.model';
import { getAllSucceededRemoteData } from '../../core/shared/operators'; import { getAllSucceededRemoteData } from '../../core/shared/operators';
import { ItemDataService } from '../../core/data/item-data.service'; import { ItemDataService } from '../../core/data/item-data.service';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { SubmissionError } from '../objects/submission-objects.reducer';
import parseSectionErrors from '../utils/parseSectionErrors';
/** /**
* This component allows to edit an existing workspaceitem/workflowitem. * This component allows to edit an existing workspaceitem/workflowitem.
@@ -52,6 +54,12 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
*/ */
public submissionDefinition: SubmissionDefinitionsModel; public submissionDefinition: SubmissionDefinitionsModel;
/**
* The submission errors present in the submission object
* @type {SubmissionError}
*/
public submissionErrors: SubmissionError;
/** /**
* The submission id * The submission id
* @type {string} * @type {string}
@@ -110,6 +118,8 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit')); this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit'));
this.router.navigate(['/mydspace']); this.router.navigate(['/mydspace']);
} else { } else {
const { errors } = submissionObjectRD.payload;
this.submissionErrors = parseSectionErrors(errors);
this.submissionId = submissionObjectRD.payload.id.toString(); this.submissionId = submissionObjectRD.payload.id.toString();
this.collectionId = (submissionObjectRD.payload.collection as Collection).id; this.collectionId = (submissionObjectRD.payload.collection as Collection).id;
this.selfUrl = submissionObjectRD.payload._links.self.href; this.selfUrl = submissionObjectRD.payload._links.self.href;

View File

@@ -11,7 +11,7 @@ import { WorkspaceitemSectionsObject } from '../../core/submission/models/worksp
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { UploaderOptions } from '../../shared/uploader/uploader-options.model'; import { UploaderOptions } from '../../shared/uploader/uploader-options.model';
import { SubmissionObjectEntry } from '../objects/submission-objects.reducer'; import { SubmissionError, SubmissionObjectEntry } from '../objects/submission-objects.reducer';
import { SectionDataObject } from '../sections/models/section-data.model'; import { SectionDataObject } from '../sections/models/section-data.model';
import { SubmissionService } from '../submission.service'; import { SubmissionService } from '../submission.service';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
@@ -41,6 +41,12 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
*/ */
@Input() sections: WorkspaceitemSectionsObject; @Input() sections: WorkspaceitemSectionsObject;
/**
* The submission errors present in the submission object
* @type {SubmissionError}
*/
@Input() submissionErrors: SubmissionError;
/** /**
* The submission self url * The submission self url
* @type {string} * @type {string}
@@ -156,6 +162,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
this.uploadFilesOptions.authToken = this.authService.buildAuthHeader(); this.uploadFilesOptions.authToken = this.authService.buildAuthHeader();
this.uploadFilesOptions.url = endpointURL.concat(`/${this.submissionId}`); this.uploadFilesOptions.url = endpointURL.concat(`/${this.submissionId}`);
this.definitionId = this.submissionDefinition.name; this.definitionId = this.submissionDefinition.name;
// const { errors } = item;
this.submissionService.dispatchInit( this.submissionService.dispatchInit(
this.collectionId, this.collectionId,
this.submissionId, this.submissionId,
@@ -163,7 +170,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
this.submissionDefinition, this.submissionDefinition,
this.sections, this.sections,
this.item, this.item,
null); this.submissionErrors);
this.changeDetectorRef.detectChanges(); this.changeDetectorRef.detectChanges();
}) })
); );

View File

@@ -144,7 +144,7 @@ export class SubmissionUploadFilesComponent implements OnChanges {
this.notificationsService.error(null, this.translate.get('submission.sections.upload.upload-failed')); this.notificationsService.error(null, this.translate.get('submission.sections.upload.upload-failed'));
} }
} }
this.sectionService.updateSectionData(this.submissionId, sectionId, sectionData, sectionErrors); this.sectionService.updateSectionData(this.submissionId, sectionId, sectionData, sectionErrors, sectionErrors);
}); });
} }

View File

@@ -1,7 +1,7 @@
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type'; import { type } from '../../shared/ngrx/type';
import { SectionVisibility, SubmissionSectionError } from './submission-objects.reducer'; import { SectionVisibility, SubmissionError, SubmissionSectionError } from './submission-objects.reducer';
import { WorkspaceitemSectionUploadFileObject } from '../../core/submission/models/workspaceitem-section-upload-file.model'; import { WorkspaceitemSectionUploadFileObject } from '../../core/submission/models/workspaceitem-section-upload-file.model';
import { import {
WorkspaceitemSectionDataType, WorkspaceitemSectionDataType,
@@ -206,7 +206,8 @@ export class UpdateSectionDataAction implements Action {
submissionId: string; submissionId: string;
sectionId: string; sectionId: string;
data: WorkspaceitemSectionDataType; data: WorkspaceitemSectionDataType;
errors: SubmissionSectionError[]; errorsToShow: SubmissionSectionError[];
serverValidationErrors: SubmissionSectionError[];
metadata: string[]; metadata: string[];
}; };
@@ -219,17 +220,20 @@ export class UpdateSectionDataAction implements Action {
* the section's ID to add * the section's ID to add
* @param data * @param data
* the section's data * the section's data
* @param errors * @param errorsToShow
* the section's errors * the list of the section's errors to show
* @param serverValidationErrors
* the list of the section errors detected by the server
* @param metadata * @param metadata
* the section's metadata * the section's metadata
*/ */
constructor(submissionId: string, constructor(submissionId: string,
sectionId: string, sectionId: string,
data: WorkspaceitemSectionDataType, data: WorkspaceitemSectionDataType,
errors: SubmissionSectionError[], errorsToShow: SubmissionSectionError[],
serverValidationErrors: SubmissionSectionError[],
metadata?: string[]) { metadata?: string[]) {
this.payload = { submissionId, sectionId, data, errors, metadata }; this.payload = { submissionId, sectionId, data, errorsToShow, serverValidationErrors, metadata };
} }
} }
@@ -308,7 +312,7 @@ export class InitSubmissionFormAction implements Action {
submissionDefinition: SubmissionDefinitionsModel; submissionDefinition: SubmissionDefinitionsModel;
sections: WorkspaceitemSectionsObject; sections: WorkspaceitemSectionsObject;
item: Item; item: Item;
errors: SubmissionSectionError[]; errors: SubmissionError;
}; };
/** /**
@@ -333,7 +337,7 @@ export class InitSubmissionFormAction implements Action {
submissionDefinition: SubmissionDefinitionsModel, submissionDefinition: SubmissionDefinitionsModel,
sections: WorkspaceitemSectionsObject, sections: WorkspaceitemSectionsObject,
item: Item, item: Item,
errors: SubmissionSectionError[]) { errors: SubmissionError) {
this.payload = { collectionId, submissionId, selfUrl, submissionDefinition, sections, item, errors }; this.payload = { collectionId, submissionId, selfUrl, submissionDefinition, sections, item, errors };
} }
} }

View File

@@ -5,16 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
import { isEqual, union } from 'lodash'; import { isEqual, union } from 'lodash';
import { from as observableFrom, Observable, of as observableOf } from 'rxjs'; import { from as observableFrom, Observable, of as observableOf } from 'rxjs';
import { import { catchError, filter, map, mergeMap, switchMap, take, tap, withLatestFrom } from 'rxjs/operators';
catchError,
filter,
map,
mergeMap,
switchMap,
take,
tap,
withLatestFrom
} from 'rxjs/operators';
import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { SubmissionObject } from '../../core/submission/models/submission-object.model';
import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; import { WorkflowItem } from '../../core/submission/models/workflowitem.model';
import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model';
@@ -52,13 +43,13 @@ import {
UpdateSectionDataAction, UpdateSectionDataAction,
UpdateSectionDataSuccessAction UpdateSectionDataSuccessAction
} from './submission-objects.actions'; } from './submission-objects.actions';
import {SubmissionObjectEntry, SubmissionSectionError, SubmissionSectionObject} from './submission-objects.reducer'; import { SubmissionObjectEntry, SubmissionSectionError, SubmissionSectionObject } from './submission-objects.reducer';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service'; import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service';
import { followLink } from '../../shared/utils/follow-link-config.model'; import { followLink } from '../../shared/utils/follow-link-config.model';
import parseSectionErrorPaths, {SectionErrorPath} from '../utils/parseSectionErrorPaths'; import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths';
import { FormState } from '../../shared/form/form.reducer'; import { FormState } from '../../shared/form/form.reducer';
@Injectable() @Injectable()
@@ -83,7 +74,7 @@ export class SubmissionObjectEffects {
} else { } else {
sectionData = action.payload.item.metadata; sectionData = action.payload.item.metadata;
} }
const sectionErrors = null; const sectionErrors = isNotEmpty(action.payload.errors) ? (action.payload.errors[sectionId] || null) : null;
mappedActions.push( mappedActions.push(
new InitSectionAction( new InitSectionAction(
action.payload.submissionId, action.payload.submissionId,
@@ -232,10 +223,7 @@ export class SubmissionObjectEffects {
switchMap(([action, state]: [DepositSubmissionAction, any]) => { switchMap(([action, state]: [DepositSubmissionAction, any]) => {
return this.submissionService.depositSubmission(state.submission.objects[action.payload.submissionId].selfUrl).pipe( return this.submissionService.depositSubmission(state.submission.objects[action.payload.submissionId].selfUrl).pipe(
map(() => new DepositSubmissionSuccessAction(action.payload.submissionId)), map(() => new DepositSubmissionSuccessAction(action.payload.submissionId)),
catchError((error) => { catchError((error) => observableOf(new DepositSubmissionErrorAction(action.payload.submissionId))));
console.log('submission error', error);
return observableOf(new DepositSubmissionErrorAction(action.payload.submissionId));
}));
})); }));
/** /**
@@ -297,7 +285,7 @@ export class SubmissionObjectEffects {
return item$.pipe( return item$.pipe(
map((item: Item) => item.metadata), map((item: Item) => item.metadata),
filter((metadata) => !isEqual(action.payload.data, metadata)), filter((metadata) => !isEqual(action.payload.data, metadata)),
map((metadata: any) => new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, metadata, action.payload.errors, action.payload.metadata)) map((metadata: any) => new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, metadata, action.payload.errorsToShow, action.payload.serverValidationErrors, action.payload.metadata))
); );
} else { } else {
return observableOf(new UpdateSectionDataSuccessAction()); return observableOf(new UpdateSectionDataSuccessAction());
@@ -413,7 +401,7 @@ export class SubmissionObjectEffects {
const sectionForm = getForm(forms, currentState, sectionId); const sectionForm = getForm(forms, currentState, sectionId);
const filteredErrors = filterErrors(sectionForm, sectionErrors, currentState.sections[sectionId].sectionType, notify); const filteredErrors = filterErrors(sectionForm, sectionErrors, currentState.sections[sectionId].sectionType, notify);
mappedActions.push(new UpdateSectionDataAction(submissionId, sectionId, sectionData, filteredErrors)); mappedActions.push(new UpdateSectionDataAction(submissionId, sectionId, sectionData, filteredErrors, sectionErrors));
} }
}); });
} }

View File

@@ -28,7 +28,8 @@ import {
SaveSubmissionSectionFormAction, SaveSubmissionSectionFormAction,
SaveSubmissionSectionFormErrorAction, SaveSubmissionSectionFormErrorAction,
SaveSubmissionSectionFormSuccessAction, SaveSubmissionSectionFormSuccessAction,
SectionStatusChangeAction, SubmissionObjectAction, SectionStatusChangeAction,
SubmissionObjectAction,
UpdateSectionDataAction UpdateSectionDataAction
} from './submission-objects.actions'; } from './submission-objects.actions';
import { SectionsType } from '../sections/sections-type'; import { SectionsType } from '../sections/sections-type';
@@ -68,7 +69,7 @@ describe('submissionReducer test suite', () => {
} }
}; };
const action = new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, {}, new Item(), []); const action = new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, {}, new Item(), null);
const newState = submissionObjectReducer({}, action); const newState = submissionObjectReducer({}, action);
expect(newState).toEqual(expectedState); expect(newState).toEqual(expectedState);
@@ -242,7 +243,7 @@ describe('submissionReducer test suite', () => {
isValid: false isValid: false
} as any; } as any;
let action: any = new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, {}, new Item(), []); let action: any = new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, {}, new Item(), null);
let newState = submissionObjectReducer({}, action); let newState = submissionObjectReducer({}, action);
action = new InitSectionAction( action = new InitSectionAction(
@@ -329,7 +330,7 @@ describe('submissionReducer test suite', () => {
] ]
} as any; } as any;
const action = new UpdateSectionDataAction(submissionId, 'traditionalpageone', data, []); const action = new UpdateSectionDataAction(submissionId, 'traditionalpageone', data, [], []);
const newState = submissionObjectReducer(initState, action); const newState = submissionObjectReducer(initState, action);
expect(newState[826].sections.traditionalpageone.data).toEqual(data); expect(newState[826].sections.traditionalpageone.data).toEqual(data);
@@ -340,7 +341,7 @@ describe('submissionReducer test suite', () => {
} as any; } as any;
const metadata = ['dc.title', 'dc.contributor.author']; const metadata = ['dc.title', 'dc.contributor.author'];
const action = new UpdateSectionDataAction(submissionId, 'traditionalpageone', data, [], metadata); const action = new UpdateSectionDataAction(submissionId, 'traditionalpageone', data, [], [], metadata);
const newState = submissionObjectReducer(initState, action); const newState = submissionObjectReducer(initState, action);
expect(newState[826].sections.traditionalpageone.metadata).toEqual(metadata); expect(newState[826].sections.traditionalpageone.metadata).toEqual(metadata);
@@ -354,10 +355,10 @@ describe('submissionReducer test suite', () => {
} }
]; ];
const action = new UpdateSectionDataAction(submissionId, 'traditionalpageone', {}, errors); const action = new UpdateSectionDataAction(submissionId, 'traditionalpageone', {}, errors, errors);
const newState = submissionObjectReducer(initState, action); const newState = submissionObjectReducer(initState, action);
expect(newState[826].sections.traditionalpageone.errors).toEqual(errors); expect(newState[826].sections.traditionalpageone.errorsToShow).toEqual(errors);
}); });
it('should remove all submission section errors properly', () => { it('should remove all submission section errors properly', () => {
@@ -366,7 +367,7 @@ describe('submissionReducer test suite', () => {
newState = submissionObjectReducer(initState, action); newState = submissionObjectReducer(initState, action);
expect(newState[826].sections.traditionalpageone.errors).toEqual([]); expect(newState[826].sections.traditionalpageone.errorsToShow).toEqual([]);
}); });
it('should add submission section error properly', () => { it('should add submission section error properly', () => {
@@ -378,7 +379,7 @@ describe('submissionReducer test suite', () => {
const action = new InertSectionErrorsAction(submissionId, 'traditionalpageone', error); const action = new InertSectionErrorsAction(submissionId, 'traditionalpageone', error);
const newState = submissionObjectReducer(initState, action); const newState = submissionObjectReducer(initState, action);
expect(newState[826].sections.traditionalpageone.errors).toEqual([error]); expect(newState[826].sections.traditionalpageone.errorsToShow).toEqual([error]);
}); });
it('should remove specified submission section error/s properly', () => { it('should remove specified submission section error/s properly', () => {
@@ -402,21 +403,21 @@ describe('submissionReducer test suite', () => {
message: 'error.validation.required' message: 'error.validation.required'
}]; }];
let action: any = new UpdateSectionDataAction(submissionId, 'traditionalpageone', {}, errors); let action: any = new UpdateSectionDataAction(submissionId, 'traditionalpageone', {}, errors, errors);
let newState = submissionObjectReducer(initState, action); let newState = submissionObjectReducer(initState, action);
action = new DeleteSectionErrorsAction(submissionId, 'traditionalpageone', error); action = new DeleteSectionErrorsAction(submissionId, 'traditionalpageone', error);
newState = submissionObjectReducer(newState, action); newState = submissionObjectReducer(newState, action);
expect(newState[826].sections.traditionalpageone.errors).toEqual(expectedErrors); expect(newState[826].sections.traditionalpageone.errorsToShow).toEqual(expectedErrors);
action = new UpdateSectionDataAction(submissionId, 'traditionalpageone', {}, errors); action = new UpdateSectionDataAction(submissionId, 'traditionalpageone', {}, errors, errors);
newState = submissionObjectReducer(initState, action); newState = submissionObjectReducer(initState, action);
action = new DeleteSectionErrorsAction(submissionId, 'traditionalpageone', errors); action = new DeleteSectionErrorsAction(submissionId, 'traditionalpageone', errors);
newState = submissionObjectReducer(newState, action); newState = submissionObjectReducer(newState, action);
expect(newState[826].sections.traditionalpageone.errors).toEqual([]); expect(newState[826].sections.traditionalpageone.errorsToShow).toEqual([]);
}); });
it('should add a new file', () => { it('should add a new file', () => {

View File

@@ -1,4 +1,4 @@
import { hasValue, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util';
import { differenceWith, findKey, isEqual, uniqWith } from 'lodash'; import { differenceWith, findKey, isEqual, uniqWith } from 'lodash';
import { import {
@@ -97,9 +97,14 @@ export interface SubmissionSectionObject {
data: WorkspaceitemSectionDataType; data: WorkspaceitemSectionDataType;
/** /**
* The list of the section errors * The list of the section's errors to show
*/ */
errors: SubmissionSectionError[]; errorsToShow: SubmissionSectionError[];
/**
* The list of the section's errors detected by the server
*/
serverValidationErrors: SubmissionSectionError[];
/** /**
* A boolean representing if this section is loading * A boolean representing if this section is loading
@@ -117,6 +122,13 @@ export interface SubmissionSectionObject {
formId: string; formId: string;
} }
/**
* An interface to represent section error
*/
export interface SubmissionError {
[submissionId: string]: SubmissionSectionError[];
}
/** /**
* An interface to represent section error * An interface to represent section error
*/ */
@@ -332,7 +344,7 @@ const removeError = (state: SubmissionObjectState, action: DeleteSectionErrorsAc
if (Array.isArray(errors)) { if (Array.isArray(errors)) {
filteredErrors = differenceWith(errors, errors, isEqual); filteredErrors = differenceWith(errors, errors, isEqual);
} else { } else {
filteredErrors = state[ submissionId ].sections[ sectionId ].errors filteredErrors = state[ submissionId ].sections[ sectionId ].errorsToShow
.filter((currentError) => currentError.path !== errors.path || !isEqual(currentError, errors)); .filter((currentError) => currentError.path !== errors.path || !isEqual(currentError, errors));
} }
@@ -340,7 +352,7 @@ const removeError = (state: SubmissionObjectState, action: DeleteSectionErrorsAc
[ submissionId ]: Object.assign({}, state[ submissionId ], { [ submissionId ]: Object.assign({}, state[ submissionId ], {
sections: Object.assign({}, state[ submissionId ].sections, { sections: Object.assign({}, state[ submissionId ].sections, {
[ sectionId ]: Object.assign({}, state[ submissionId ].sections [ sectionId ], { [ sectionId ]: Object.assign({}, state[ submissionId ].sections [ sectionId ], {
errors: filteredErrors errorsToShow: filteredErrors
}) })
}) })
}) })
@@ -354,13 +366,13 @@ const addError = (state: SubmissionObjectState, action: InertSectionErrorsAction
const { submissionId, sectionId, error } = action.payload; const { submissionId, sectionId, error } = action.payload;
if (hasValue(state[ submissionId ].sections[ sectionId ])) { if (hasValue(state[ submissionId ].sections[ sectionId ])) {
const errors = uniqWith(state[ submissionId ].sections[ sectionId ].errors.concat(error), isEqual); const errorsToShow = uniqWith(state[ submissionId ].sections[ sectionId ].errorsToShow.concat(error), isEqual);
return Object.assign({}, state, { return Object.assign({}, state, {
[ submissionId ]: Object.assign({}, state[ submissionId ], { [ submissionId ]: Object.assign({}, state[ submissionId ], {
activeSection: state[ action.payload.submissionId ].activeSection, sections: Object.assign({}, state[ submissionId ].sections, { activeSection: state[ action.payload.submissionId ].activeSection, sections: Object.assign({}, state[ submissionId ].sections, {
[ sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { [ sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], {
errors errorsToShow
}) })
}), }),
}) })
@@ -378,7 +390,7 @@ const addError = (state: SubmissionObjectState, action: InertSectionErrorsAction
* @param action * @param action
* a RemoveSectionErrorsAction * a RemoveSectionErrorsAction
* @return SubmissionObjectState * @return SubmissionObjectState
* the new state, with the section's errors updated. * the new state, with the section's errorsToShow updated.
*/ */
function removeSectionErrors(state: SubmissionObjectState, action: RemoveSectionErrorsAction): SubmissionObjectState { function removeSectionErrors(state: SubmissionObjectState, action: RemoveSectionErrorsAction): SubmissionObjectState {
if (isNotEmpty(state[ action.payload.submissionId ]) if (isNotEmpty(state[ action.payload.submissionId ])
@@ -387,7 +399,7 @@ function removeSectionErrors(state: SubmissionObjectState, action: RemoveSection
[ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], {
sections: Object.assign({}, state[ action.payload.submissionId ].sections, { sections: Object.assign({}, state[ action.payload.submissionId ].sections, {
[ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], {
errors: [] errorsToShow: []
}) })
}) })
}) })
@@ -644,9 +656,10 @@ function initSection(state: SubmissionObjectState, action: InitSectionAction): S
collapsed: false, collapsed: false,
enabled: action.payload.enabled, enabled: action.payload.enabled,
data: action.payload.data, data: action.payload.data,
errors: action.payload.errors || [], errorsToShow: [],
serverValidationErrors: action.payload.errors || [],
isLoading: false, isLoading: false,
isValid: false isValid: isEmpty(action.payload.errors)
} }
}) })
}) })
@@ -702,7 +715,8 @@ function updateSectionData(state: SubmissionObjectState, action: UpdateSectionDa
[ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], {
enabled: true, enabled: true,
data: action.payload.data, data: action.payload.data,
errors: action.payload.errors, errorsToShow: action.payload.errorsToShow,
serverValidationErrors: action.payload.serverValidationErrors,
metadata: reduceSectionMetadata(action.payload.metadata, state[ action.payload.submissionId ].sections [ action.payload.sectionId ].metadata) metadata: reduceSectionMetadata(action.payload.metadata, state[ action.payload.submissionId ].sections [ action.payload.sectionId ].metadata)
}) })
}) })

View File

@@ -1,11 +1,7 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { Observable, of as observableOf, Subscription } from 'rxjs'; import { Observable, of as observableOf, Subscription } from 'rxjs';
import { import { Field, Option, SubmissionCcLicence } from '../../../core/submission/models/submission-cc-license.model';
Field, import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../../core/shared/operators';
Option,
SubmissionCcLicence
} from '../../../core/submission/models/submission-cc-license.model';
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators';
import { distinctUntilChanged, filter, map, take } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, take } from 'rxjs/operators';
import { SubmissionCcLicenseDataService } from '../../../core/submission/submission-cc-license-data.service'; import { SubmissionCcLicenseDataService } from '../../../core/submission/submission-cc-license-data.service';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
@@ -234,7 +230,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
this.subscriptions.push( this.subscriptions.push(
this.sectionService.getSectionState(this.submissionId, this.sectionData.id, SectionsType.CcLicense).pipe( this.sectionService.getSectionState(this.submissionId, this.sectionData.id, SectionsType.CcLicense).pipe(
filter((sectionState) => { filter((sectionState) => {
return isNotEmpty(sectionState) && (isNotEmpty(sectionState.data) || isNotEmpty(sectionState.errors)); return isNotEmpty(sectionState) && (isNotEmpty(sectionState.data) || isNotEmpty(sectionState.errorsToShow));
}), }),
distinctUntilChanged(), distinctUntilChanged(),
map((sectionState) => sectionState.data as WorkspaceitemSectionCcLicenseObject), map((sectionState) => sectionState.data as WorkspaceitemSectionCcLicenseObject),

View File

@@ -300,7 +300,6 @@ export class SectionFormOperationsService {
const path = this.getFieldPathFromEvent(event); const path = this.getFieldPathFromEvent(event);
const value = this.getFieldValueFromChangeEvent(event); const value = this.getFieldValueFromChangeEvent(event);
console.log(value);
if (this.formBuilder.isQualdropGroup(event.model as DynamicFormControlModel)) { if (this.formBuilder.isQualdropGroup(event.model as DynamicFormControlModel)) {
this.dispatchOperationsFromMap(this.getQualdropValueMap(event), pathCombiner, event, previousValue); this.dispatchOperationsFromMap(this.getQualdropValueMap(event), pathCombiner, event, previousValue);
} else if (event.context && event.context instanceof DynamicFormArrayGroupModel) { } else if (event.context && event.context instanceof DynamicFormArrayGroupModel) {

View File

@@ -2,15 +2,7 @@ import { ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core';
import { DynamicFormControlEvent, DynamicFormControlModel } from '@ng-dynamic-forms/core'; import { DynamicFormControlEvent, DynamicFormControlModel } from '@ng-dynamic-forms/core';
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
import { import { distinctUntilChanged, filter, find, map, mergeMap, take, tap } from 'rxjs/operators';
distinctUntilChanged,
filter,
find,
map,
take,
tap,
mergeMap
} from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { findIndex, isEqual } from 'lodash'; import { findIndex, isEqual } from 'lodash';
@@ -19,7 +11,7 @@ import { FormComponent } from '../../../shared/form/form.component';
import { FormService } from '../../../shared/form/form.service'; import { FormService } from '../../../shared/form/form.service';
import { SectionModelComponent } from '../models/section.model'; import { SectionModelComponent } from '../models/section.model';
import { SubmissionFormsConfigService } from '../../../core/config/submission-forms-config.service'; import { SubmissionFormsConfigService } from '../../../core/config/submission-forms-config.service';
import { hasValue, isNotEmpty, isUndefined } from '../../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty, isUndefined } from '../../../shared/empty.util';
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model';
import { SubmissionSectionError, SubmissionSectionObject } from '../../objects/submission-objects.reducer'; import { SubmissionSectionError, SubmissionSectionObject } from '../../objects/submission-objects.reducer';
@@ -183,7 +175,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
take(1)) take(1))
.subscribe(([sectionData, workspaceItem]: [WorkspaceitemSectionFormObject, WorkspaceItem]) => { .subscribe(([sectionData, workspaceItem]: [WorkspaceitemSectionFormObject, WorkspaceItem]) => {
if (isUndefined(this.formModel)) { if (isUndefined(this.formModel)) {
this.sectionData.errors = []; // this.sectionData.errorsToShow = [];
this.workspaceItem = workspaceItem; this.workspaceItem = workspaceItem;
// Is the first loading so init form // Is the first loading so init form
this.initForm(sectionData); this.initForm(sectionData);
@@ -211,7 +203,14 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
* the section status * the section status
*/ */
protected getSectionStatus(): Observable<boolean> { protected getSectionStatus(): Observable<boolean> {
return this.formService.isValid(this.formId); const formStatus$ = this.formService.isValid(this.formId);
const serverValidationStatus$ = this.sectionService.getSectionServerErrors(this.submissionId, this.sectionData.id).pipe(
map((validationErrors) => isEmpty(validationErrors))
);
return observableCombineLatest([formStatus$, serverValidationStatus$]).pipe(
map(([formValidation, serverSideValidation]: [boolean, boolean]) => formValidation && serverSideValidation)
);
} }
/** /**
@@ -263,7 +262,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
this.submissionService.getSubmissionScope() this.submissionService.getSubmissionScope()
); );
const sectionMetadata = this.sectionService.computeSectionConfiguredMetadata(this.formConfig); const sectionMetadata = this.sectionService.computeSectionConfiguredMetadata(this.formConfig);
this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, sectionData, [], sectionMetadata); this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, sectionData, this.sectionData.errorsToShow, this.sectionData.serverValidationErrors, sectionMetadata);
} catch (e) { } catch (e) {
const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString(); const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString();
@@ -296,10 +295,10 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
this.checksForErrors(errors); this.checksForErrors(errors);
this.isUpdating = false; this.isUpdating = false;
this.cdr.detectChanges(); this.cdr.detectChanges();
} else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) { } else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errorsToShow)) {
this.checksForErrors(errors); this.checksForErrors(errors);
} }
} else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) { } else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errorsToShow)) {
this.checksForErrors(errors); this.checksForErrors(errors);
} }
@@ -315,8 +314,8 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
this.formService.isFormInitialized(this.formId).pipe( this.formService.isFormInitialized(this.formId).pipe(
find((status: boolean) => status === true && !this.isUpdating)) find((status: boolean) => status === true && !this.isUpdating))
.subscribe(() => { .subscribe(() => {
this.sectionService.checkSectionErrors(this.submissionId, this.sectionData.id, this.formId, errors, this.sectionData.errors); this.sectionService.checkSectionErrors(this.submissionId, this.sectionData.id, this.formId, errors, this.sectionData.errorsToShow);
this.sectionData.errors = errors; this.sectionData.errorsToShow = errors;
this.cdr.detectChanges(); this.cdr.detectChanges();
}); });
} }
@@ -340,13 +339,13 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
*/ */
this.sectionService.getSectionState(this.submissionId, this.sectionData.id, this.sectionData.sectionType).pipe( this.sectionService.getSectionState(this.submissionId, this.sectionData.id, this.sectionData.sectionType).pipe(
filter((sectionState: SubmissionSectionObject) => { filter((sectionState: SubmissionSectionObject) => {
return isNotEmpty(sectionState) && (isNotEmpty(sectionState.data) || isNotEmpty(sectionState.errors)); return isNotEmpty(sectionState) && (isNotEmpty(sectionState.data) || isNotEmpty(sectionState.errorsToShow));
}), }),
distinctUntilChanged()) distinctUntilChanged())
.subscribe((sectionState: SubmissionSectionObject) => { .subscribe((sectionState: SubmissionSectionObject) => {
this.fieldsOnTheirWayToBeRemoved = new Map(); this.fieldsOnTheirWayToBeRemoved = new Map();
this.sectionMetadata = sectionState.metadata; this.sectionMetadata = sectionState.metadata;
this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errors); this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errorsToShow);
}) })
); );
} }
@@ -367,11 +366,22 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
const metadata = this.formOperationsService.getFieldPathSegmentedFromChangeEvent(event); const metadata = this.formOperationsService.getFieldPathSegmentedFromChangeEvent(event);
const value = this.formOperationsService.getFieldValueFromChangeEvent(event); const value = this.formOperationsService.getFieldValueFromChangeEvent(event);
if (environment.submission.autosave.metadata.indexOf(metadata) !== -1 && isNotEmpty(value)) { if ((environment.submission.autosave.metadata.indexOf(metadata) !== -1 && isNotEmpty(value)) || this.hasRelatedCustomError(metadata)) {
this.submissionService.dispatchSave(this.submissionId); this.submissionService.dispatchSave(this.submissionId);
} }
} }
private hasRelatedCustomError(medatata): boolean {
const index = findIndex(this.sectionData.errorsToShow, {path: this.pathCombiner.getPath(medatata).path});
if (index !== -1) {
const error = this.sectionData.errorsToShow[index];
const validator = error.message.replace('error.validation.', '');
return !environment.form.validatorMap.hasOwnProperty(validator);
} else {
return false;
}
}
/** /**
* Method called when a form dfFocus event is fired. * Method called when a form dfFocus event is fired.
* Initialize [FormFieldPreviousValueObject] instance. * Initialize [FormFieldPreviousValueObject] instance.

View File

@@ -18,9 +18,14 @@ export interface SectionDataObject {
data: WorkspaceitemSectionDataType; data: WorkspaceitemSectionDataType;
/** /**
* The list of the section errors * The list of the section's errors to show
*/ */
errors: SubmissionSectionError[]; errorsToShow: SubmissionSectionError[];
/**
* The list of the section's errors detected by the server
*/
serverValidationErrors: SubmissionSectionError[];
/** /**
* The section header * The section header

View File

@@ -6,7 +6,7 @@ import { uniq } from 'lodash';
import { SectionsService } from './sections.service'; import { SectionsService } from './sections.service';
import { hasValue, isNotEmpty, isNotNull } from '../../shared/empty.util'; import { hasValue, isNotEmpty, isNotNull } from '../../shared/empty.util';
import { SubmissionSectionError, SubmissionSectionObject } from '../objects/submission-objects.reducer'; import { SubmissionSectionError } from '../objects/submission-objects.reducer';
import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths';
import { SubmissionService } from '../submission.service'; import { SubmissionService } from '../submission.service';
import { SectionsType } from './sections-type'; import { SectionsType } from './sections-type';
@@ -111,8 +111,7 @@ export class SectionsDirective implements OnDestroy, OnInit {
})); }));
this.subs.push( this.subs.push(
this.sectionService.getSectionState(this.submissionId, this.sectionId, this.sectionType).pipe( this.sectionService.getShownSectionErrors(this.submissionId, this.sectionId, this.sectionType)
map((state: SubmissionSectionObject) => state.errors))
.subscribe((errors: SubmissionSectionError[]) => { .subscribe((errors: SubmissionSectionError[]) => {
if (isNotEmpty(errors)) { if (isNotEmpty(errors)) {
errors.forEach((errorItem: SubmissionSectionError) => { errors.forEach((errorItem: SubmissionSectionError) => {

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, take } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, mergeMap, take } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ScrollToConfigOptions, ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; import { ScrollToConfigOptions, ScrollToService } from '@nicky-lenaers/ngx-scroll-to';
import { findKey, isEqual } from 'lodash'; import { findIndex, findKey, isEqual } from 'lodash';
import { SubmissionState } from '../submission.reducers'; import { SubmissionState } from '../submission.reducers';
import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
@@ -27,11 +27,12 @@ import {
submissionObjectFromIdSelector, submissionObjectFromIdSelector,
submissionSectionDataFromIdSelector, submissionSectionDataFromIdSelector,
submissionSectionErrorsFromIdSelector, submissionSectionErrorsFromIdSelector,
submissionSectionFromIdSelector submissionSectionFromIdSelector,
submissionSectionServerErrorsFromIdSelector
} from '../selectors'; } from '../selectors';
import { SubmissionScopeType } from '../../core/submission/submission-scope-type'; import { SubmissionScopeType } from '../../core/submission/submission-scope-type';
import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths';
import { FormAddError, FormClearErrorsAction, FormRemoveErrorAction } from '../../shared/form/form.actions'; import { FormClearErrorsAction } from '../../shared/form/form.actions';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { SubmissionService } from '../submission.service'; import { SubmissionService } from '../submission.service';
import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model'; import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model';
@@ -39,6 +40,9 @@ import { SectionsType } from './sections-type';
import { normalizeSectionData } from '../../core/submission/submission-response-parsing.service'; import { normalizeSectionData } from '../../core/submission/submission-response-parsing.service';
import { SubmissionFormsModel } from '../../core/config/models/config-submission-forms.model'; import { SubmissionFormsModel } from '../../core/config/models/config-submission-forms.model';
import { parseReviver } from '@ng-dynamic-forms/core'; import { parseReviver } from '@ng-dynamic-forms/core';
import { FormService } from '../../shared/form/form.service';
import { JsonPatchOperationPathCombiner } from '../../core/json-patch/builder/json-patch-operation-path-combiner';
import { FormError } from '../../shared/form/form.reducer';
/** /**
* A service that provides methods used in submission process. * A service that provides methods used in submission process.
@@ -48,13 +52,15 @@ export class SectionsService {
/** /**
* Initialize service variables * Initialize service variables
* @param {FormService} formService
* @param {NotificationsService} notificationsService * @param {NotificationsService} notificationsService
* @param {ScrollToService} scrollToService * @param {ScrollToService} scrollToService
* @param {SubmissionService} submissionService * @param {SubmissionService} submissionService
* @param {Store<SubmissionState>} store * @param {Store<SubmissionState>} store
* @param {TranslateService} translate * @param {TranslateService} translate
*/ */
constructor(private notificationsService: NotificationsService, constructor(private formService: FormService,
private notificationsService: NotificationsService,
private scrollToService: ScrollToService, private scrollToService: ScrollToService,
private submissionService: SubmissionService, private submissionService: SubmissionService,
private store: Store<SubmissionState>, private store: Store<SubmissionState>,
@@ -95,12 +101,9 @@ export class SectionsService {
errorPaths.forEach((path: SectionErrorPath) => { errorPaths.forEach((path: SectionErrorPath) => {
if (path.fieldId) { if (path.fieldId) {
const fieldId = path.fieldId.replace(/\./g, '_');
// Dispatch action to add form error to the state; // Dispatch action to add form error to the state;
const formAddErrorAction = new FormAddError(formId, fieldId, path.fieldIndex, error.message); this.formService.addError(formId, path.fieldId, path.fieldIndex, error.message);
this.store.dispatch(formAddErrorAction); dispatchedErrors.push(path.fieldId);
dispatchedErrors.push(fieldId);
} }
}); });
}); });
@@ -111,12 +114,9 @@ export class SectionsService {
errorPaths.forEach((path: SectionErrorPath) => { errorPaths.forEach((path: SectionErrorPath) => {
if (path.fieldId) { if (path.fieldId) {
const fieldId = path.fieldId.replace(/\./g, '_'); if (!dispatchedErrors.includes(path.fieldId)) {
if (!dispatchedErrors.includes(fieldId)) {
// Dispatch action to remove form error from the state; // Dispatch action to remove form error from the state;
const formRemoveErrorAction = new FormRemoveErrorAction(formId, fieldId, path.fieldIndex); this.formService.removeError(formId, path.fieldId, path.fieldIndex);
this.store.dispatch(formRemoveErrorAction);
} }
} }
}); });
@@ -173,8 +173,31 @@ export class SectionsService {
); );
} }
getShownSectionErrors(submissionId: string, sectionId: string, sectionType: SectionsType): Observable<SubmissionSectionError[]> {
let errorsState$: Observable<SubmissionSectionError[]>;
if (sectionType !== SectionsType.SubmissionForm) {
errorsState$ = this.getSectionErrors(submissionId, sectionId);
} else {
errorsState$ = this.getSectionState(submissionId, sectionId, sectionType).pipe(
mergeMap((state: SubmissionSectionObject) => this.formService.getFormErrors(state.formId).pipe(
map((formErrors: FormError[]) => {
const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId);
const sectionErrors = formErrors
.map((error) => ({
path: pathCombiner.getPath(error.fieldId.replace(/\_/g, '.')).path,
message: error.message
} as SubmissionSectionError))
.filter((sectionError: SubmissionSectionError) => findIndex(state.errorsToShow, {path: sectionError.path}) === -1);
return [...state.errorsToShow, ...sectionErrors];
})
))
);
}
return errorsState$;
}
/** /**
* Return the error list object data for the specified section * Return the error list to show for the specified section
* *
* @param submissionId * @param submissionId
* The submission id * The submission id
@@ -188,6 +211,21 @@ export class SectionsService {
distinctUntilChanged()); distinctUntilChanged());
} }
/**
* Return the error list detected by the server for the specified section
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @return Observable<SubmissionSectionError>
* observable of array of [SubmissionSectionError]
*/
public getSectionServerErrors(submissionId: string, sectionId: string): Observable<SubmissionSectionError[]> {
return this.store.select(submissionSectionServerErrorsFromIdSelector(submissionId, sectionId)).pipe(
distinctUntilChanged());
}
/** /**
* Return the state object for the specified section * Return the state object for the specified section
* *
@@ -367,12 +405,21 @@ export class SectionsService {
* The section id * The section id
* @param data * @param data
* The section data * The section data
* @param errors * @param errorsToShow
* The list of section errors * the list of the section's errors to show
* @param serverValidationErrors
* the list of the section errors detected by the server
* @param metadata * @param metadata
* The section metadata * The section metadata
*/ */
public updateSectionData(submissionId: string, sectionId: string, data: WorkspaceitemSectionDataType, errors: SubmissionSectionError[] = [], metadata?: string[]) { public updateSectionData(
submissionId: string,
sectionId: string,
data: WorkspaceitemSectionDataType,
errorsToShow: SubmissionSectionError[] = [],
serverValidationErrors: SubmissionSectionError[] = [],
metadata?: string[]
) {
if (isNotEmpty(data)) { if (isNotEmpty(data)) {
const isAvailable$ = this.isSectionAvailable(submissionId, sectionId); const isAvailable$ = this.isSectionAvailable(submissionId, sectionId);
const isEnabled$ = this.isSectionEnabled(submissionId, sectionId); const isEnabled$ = this.isSectionEnabled(submissionId, sectionId);
@@ -381,7 +428,7 @@ export class SectionsService {
take(1), take(1),
filter(([available, enabled]: [boolean, boolean]) => available)) filter(([available, enabled]: [boolean, boolean]) => available))
.subscribe(([available, enabled]: [boolean, boolean]) => { .subscribe(([available, enabled]: [boolean, boolean]) => {
this.store.dispatch(new UpdateSectionDataAction(submissionId, sectionId, data, errors, metadata)); this.store.dispatch(new UpdateSectionDataAction(submissionId, sectionId, data, errorsToShow, serverValidationErrors, metadata));
}); });
} }
} }

View File

@@ -61,5 +61,11 @@ export function submissionSectionDataFromIdSelector(submissionId: string, sectio
export function submissionSectionErrorsFromIdSelector(submissionId: string, sectionId: string): MemoizedSelector<SubmissionState, any> { export function submissionSectionErrorsFromIdSelector(submissionId: string, sectionId: string): MemoizedSelector<SubmissionState, any> {
const submissionIdSelector = submissionSectionFromIdSelector(submissionId, sectionId); const submissionIdSelector = submissionSectionFromIdSelector(submissionId, sectionId);
return subStateSelector<SubmissionState, SubmissionSectionObject>(submissionIdSelector, 'errors'); return subStateSelector<SubmissionState, SubmissionSectionObject>(submissionIdSelector, 'errorsToShow');
}
export function submissionSectionServerErrorsFromIdSelector(submissionId: string, sectionId: string): MemoizedSelector<SubmissionState, any> {
const submissionIdSelector = submissionSectionFromIdSelector(submissionId, sectionId);
return subStateSelector<SubmissionState, SubmissionSectionObject>(submissionIdSelector, 'serverValidationErrors');
} }

View File

@@ -22,9 +22,9 @@ import {
SetActiveSectionAction SetActiveSectionAction
} from './objects/submission-objects.actions'; } from './objects/submission-objects.actions';
import { import {
SubmissionError,
SubmissionObjectEntry, SubmissionObjectEntry,
SubmissionSectionEntry, SubmissionSectionEntry,
SubmissionSectionError,
SubmissionSectionObject SubmissionSectionObject
} from './objects/submission-objects.reducer'; } from './objects/submission-objects.reducer';
import { submissionObjectFromIdSelector } from './selectors'; import { submissionObjectFromIdSelector } from './selectors';
@@ -183,7 +183,7 @@ export class SubmissionService {
submissionDefinition: SubmissionDefinitionsModel, submissionDefinition: SubmissionDefinitionsModel,
sections: WorkspaceitemSectionsObject, sections: WorkspaceitemSectionsObject,
item: Item, item: Item,
errors: SubmissionSectionError[]) { errors: SubmissionError) {
this.store.dispatch(new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, sections, item, errors)); this.store.dispatch(new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, sections, item, errors));
} }
@@ -292,7 +292,8 @@ export class SubmissionService {
sectionObject.config = sections[sectionId].config; sectionObject.config = sections[sectionId].config;
sectionObject.mandatory = sections[sectionId].mandatory; sectionObject.mandatory = sections[sectionId].mandatory;
sectionObject.data = sections[sectionId].data; sectionObject.data = sections[sectionId].data;
sectionObject.errors = sections[sectionId].errors; sectionObject.errorsToShow = sections[sectionId].errorsToShow;
sectionObject.serverValidationErrors = sections[sectionId].serverValidationErrors;
sectionObject.header = sections[sectionId].header; sectionObject.header = sections[sectionId].header;
sectionObject.id = sectionId; sectionObject.id = sectionId;
sectionObject.sectionType = sections[sectionId].sectionType; sectionObject.sectionType = sections[sectionId].sectionType;

View File

@@ -1,19 +1,17 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewContainerRef } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewContainerRef } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { BehaviorSubject, Subscription } from 'rxjs';
import { debounceTime, switchMap } from 'rxjs/operators';
import { hasValue, isEmpty, isNotNull, isNotEmptyOperator } from '../../shared/empty.util';
import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { hasValue, isEmpty, isNotEmptyOperator, isNotNull } from '../../shared/empty.util';
import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { SubmissionService } from '../submission.service'; import { SubmissionService } from '../submission.service';
import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { SubmissionObject } from '../../core/submission/models/submission-object.model';
import { Collection } from '../../core/shared/collection.model';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { switchMap, debounceTime } from 'rxjs/operators';
import { getAllSucceededRemoteData } from '../../core/shared/operators'; import { getAllSucceededRemoteData } from '../../core/shared/operators';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { ItemDataService } from '../../core/data/item-data.service'; import { ItemDataService } from '../../core/data/item-data.service';