mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge pull request #960 from 4Science/CSTPER-260
Auto-save in new Item Submission form breaks the form
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
import { getTestScheduler } from 'jasmine-marbles';
|
import { getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError } from 'rxjs/operators';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { SubmissionPatchRequest } from '../data/request.models';
|
import { SubmissionPatchRequest } from '../data/request.models';
|
||||||
@@ -22,6 +21,7 @@ import {
|
|||||||
} from './json-patch-operations.actions';
|
} from './json-patch-operations.actions';
|
||||||
import { RequestEntry } from '../data/request.reducer';
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||||
|
import { _deepClone } from 'fast-json-patch/lib/helpers';
|
||||||
|
|
||||||
class TestService extends JsonPatchOperationsService<SubmitDataResponseDefinitionObject, SubmissionPatchRequest> {
|
class TestService extends JsonPatchOperationsService<SubmitDataResponseDefinitionObject, SubmissionPatchRequest> {
|
||||||
protected linkPath = '';
|
protected linkPath = '';
|
||||||
@@ -196,6 +196,32 @@ describe('JsonPatchOperationsService test suite', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('hasPendingOperations', () => {
|
||||||
|
|
||||||
|
it('should return true when there are pending operations', () => {
|
||||||
|
|
||||||
|
const expected = hot('(x|)', { x: true });
|
||||||
|
|
||||||
|
const result = service.hasPendingOperations(testJsonPatchResourceType);
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when there are not pending operations', () => {
|
||||||
|
|
||||||
|
const mockStateNoOp = _deepClone(mockState);
|
||||||
|
mockStateNoOp['json/patch'][testJsonPatchResourceType].children = [];
|
||||||
|
store.select.and.returnValue(observableOf(mockStateNoOp['json/patch'][testJsonPatchResourceType]));
|
||||||
|
|
||||||
|
const expected = hot('(x|)', { x: false });
|
||||||
|
|
||||||
|
const result = service.hasPendingOperations(testJsonPatchResourceType);
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe('jsonPatchByResourceID', () => {
|
describe('jsonPatchByResourceID', () => {
|
||||||
|
|
||||||
it('should call submitJsonPatchOperations method', () => {
|
it('should call submitJsonPatchOperations method', () => {
|
||||||
|
@@ -161,6 +161,18 @@ export abstract class JsonPatchOperationsService<ResponseDefinitionDomain, Patch
|
|||||||
return this.submitJsonPatchOperations(href$, resourceType);
|
return this.submitJsonPatchOperations(href$, resourceType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select the jsonPatch operation related to the specified resource type.
|
||||||
|
* @param resourceType
|
||||||
|
*/
|
||||||
|
public hasPendingOperations(resourceType: string): Observable<boolean> {
|
||||||
|
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
|
* Make a new JSON Patch request with all operations related to the specified resource id
|
||||||
*
|
*
|
||||||
|
@@ -7,7 +7,7 @@ import {
|
|||||||
DYNAMIC_FORM_CONTROL_TYPE_GROUP,
|
DYNAMIC_FORM_CONTROL_TYPE_GROUP,
|
||||||
DYNAMIC_FORM_CONTROL_TYPE_INPUT,
|
DYNAMIC_FORM_CONTROL_TYPE_INPUT,
|
||||||
DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP,
|
DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP,
|
||||||
DynamicFormArrayModel,
|
DynamicFormArrayModel, DynamicFormControlEvent,
|
||||||
DynamicFormControlModel,
|
DynamicFormControlModel,
|
||||||
DynamicFormGroupModel,
|
DynamicFormGroupModel,
|
||||||
DynamicFormService, DynamicFormValidationService,
|
DynamicFormService, DynamicFormValidationService,
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { isObject, isString, mergeWith } from 'lodash';
|
import { isObject, isString, mergeWith } from 'lodash';
|
||||||
|
|
||||||
import { hasValue, isEmpty, isNotEmpty, isNotNull, isNotUndefined, isNull } from '../../empty.util';
|
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 { 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 { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model';
|
||||||
import { RowParser } from './parsers/row-parser';
|
import { RowParser } from './parsers/row-parser';
|
||||||
@@ -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 { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model';
|
||||||
import { isNgbDateStruct } from '../../date.util';
|
import { isNgbDateStruct } from '../../date.util';
|
||||||
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-ui/ds-dynamic-form-constants';
|
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()
|
@Injectable()
|
||||||
export class FormBuilderService extends DynamicFormService {
|
export class FormBuilderService extends DynamicFormService {
|
||||||
@@ -54,6 +55,13 @@ export class FormBuilderService extends DynamicFormService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isConcatGroup(controlModel)) {
|
||||||
|
if (controlModel.id.match(new RegExp(findId + CONCAT_GROUP_SUFFIX + `_\\d+$`))) {
|
||||||
|
result = (controlModel as DynamicConcatModel).group[0];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isGroup(controlModel)) {
|
if (this.isGroup(controlModel)) {
|
||||||
findByIdFn(findId, (controlModel as DynamicFormGroupModel).group, findArrayIndex);
|
findByIdFn(findId, (controlModel as DynamicFormGroupModel).group, findArrayIndex);
|
||||||
}
|
}
|
||||||
@@ -247,6 +255,10 @@ export class FormBuilderService extends DynamicFormService {
|
|||||||
return model && ((model as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && (model as any).isCustomGroup === true);
|
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 {
|
isRowGroup(model: DynamicFormControlModel): boolean {
|
||||||
return model && ((model as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && (model as any).isRowGroup === true);
|
return model && ((model as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && (model as any).isRowGroup === true);
|
||||||
}
|
}
|
||||||
@@ -303,4 +315,76 @@ export class FormBuilderService extends DynamicFormService {
|
|||||||
return (tempModel.id !== tempModel.name) ? tempModel.name : tempModel.id;
|
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): string[] => {
|
||||||
|
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 Object.keys(result);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ import { type } from '../ngrx/type';
|
|||||||
export const FormActionTypes = {
|
export const FormActionTypes = {
|
||||||
FORM_INIT: type('dspace/form/FORM_INIT'),
|
FORM_INIT: type('dspace/form/FORM_INIT'),
|
||||||
FORM_CHANGE: type('dspace/form/FORM_CHANGE'),
|
FORM_CHANGE: type('dspace/form/FORM_CHANGE'),
|
||||||
|
FORM_ADD_TOUCHED: type('dspace/form/FORM_ADD_TOUCHED'),
|
||||||
FORM_REMOVE: type('dspace/form/FORM_REMOVE'),
|
FORM_REMOVE: type('dspace/form/FORM_REMOVE'),
|
||||||
FORM_STATUS_CHANGE: type('dspace/form/FORM_STATUS_CHANGE'),
|
FORM_STATUS_CHANGE: type('dspace/form/FORM_STATUS_CHANGE'),
|
||||||
FORM_ADD_ERROR: type('dspace/form/FORM_ADD_ERROR'),
|
FORM_ADD_ERROR: type('dspace/form/FORM_ADD_ERROR'),
|
||||||
@@ -52,7 +53,7 @@ export class FormChangeAction implements Action {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new FormInitAction
|
* Create a new FormChangeAction
|
||||||
*
|
*
|
||||||
* @param formId
|
* @param formId
|
||||||
* the Form's ID
|
* the Form's ID
|
||||||
@@ -64,6 +65,26 @@ export class FormChangeAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FormAddTouchedAction implements Action {
|
||||||
|
type = FormActionTypes.FORM_ADD_TOUCHED;
|
||||||
|
payload: {
|
||||||
|
formId: string;
|
||||||
|
touched: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new FormAddTouchedAction
|
||||||
|
*
|
||||||
|
* @param formId
|
||||||
|
* the Form's ID
|
||||||
|
* @param touched
|
||||||
|
* the array containing new touched fields
|
||||||
|
*/
|
||||||
|
constructor(formId: string, touched: string[]) {
|
||||||
|
this.payload = {formId, touched};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class FormRemoveAction implements Action {
|
export class FormRemoveAction implements Action {
|
||||||
type = FormActionTypes.FORM_REMOVE;
|
type = FormActionTypes.FORM_REMOVE;
|
||||||
payload: {
|
payload: {
|
||||||
@@ -147,6 +168,7 @@ export class FormClearErrorsAction implements Action {
|
|||||||
*/
|
*/
|
||||||
export type FormAction = FormInitAction
|
export type FormAction = FormInitAction
|
||||||
| FormChangeAction
|
| FormChangeAction
|
||||||
|
| FormAddTouchedAction
|
||||||
| FormRemoveAction
|
| FormRemoveAction
|
||||||
| FormStatusChangeAction
|
| FormStatusChangeAction
|
||||||
| FormAddError
|
| FormAddError
|
||||||
|
@@ -18,7 +18,7 @@
|
|||||||
<button type="button" class="btn btn-secondary"
|
<button type="button" class="btn btn-secondary"
|
||||||
[disabled]="isItemReadOnly(context, index)"
|
[disabled]="isItemReadOnly(context, index)"
|
||||||
(click)="insertItem($event, group.context, group.index)">
|
(click)="insertItem($event, group.context, group.index)">
|
||||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
<span aria-label="Add">{{'form.add' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -119,7 +119,8 @@ function init() {
|
|||||||
dc_identifier_issn: null
|
dc_identifier_issn: null
|
||||||
},
|
},
|
||||||
valid: false,
|
valid: false,
|
||||||
errors: []
|
errors: [],
|
||||||
|
touched: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -253,6 +253,7 @@ export class FormComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onFocus(event: DynamicFormControlEvent): void {
|
onFocus(event: DynamicFormControlEvent): void {
|
||||||
|
this.formService.setTouched(this.formId, this.formModel, event);
|
||||||
this.focus.emit(event);
|
this.focus.emit(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,6 +6,7 @@ import {
|
|||||||
FormInitAction,
|
FormInitAction,
|
||||||
FormRemoveAction,
|
FormRemoveAction,
|
||||||
FormRemoveErrorAction,
|
FormRemoveErrorAction,
|
||||||
|
FormAddTouchedAction,
|
||||||
FormStatusChangeAction
|
FormStatusChangeAction
|
||||||
} from './form.actions';
|
} from './form.actions';
|
||||||
|
|
||||||
@@ -21,7 +22,8 @@ describe('formReducer', () => {
|
|||||||
description: null
|
description: null
|
||||||
},
|
},
|
||||||
valid: false,
|
valid: false,
|
||||||
errors: []
|
errors: [],
|
||||||
|
touched: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const formId = 'testForm';
|
const formId = 'testForm';
|
||||||
@@ -48,7 +50,8 @@ describe('formReducer', () => {
|
|||||||
description: null
|
description: null
|
||||||
},
|
},
|
||||||
valid: false,
|
valid: false,
|
||||||
errors: []
|
errors: [],
|
||||||
|
touched: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const formId = 'testForm';
|
const formId = 'testForm';
|
||||||
@@ -67,7 +70,8 @@ describe('formReducer', () => {
|
|||||||
description: null
|
description: null
|
||||||
},
|
},
|
||||||
valid: false,
|
valid: false,
|
||||||
errors: []
|
errors: [],
|
||||||
|
touched: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,7 +92,8 @@ describe('formReducer', () => {
|
|||||||
description: null
|
description: null
|
||||||
},
|
},
|
||||||
valid: false,
|
valid: false,
|
||||||
errors: []
|
errors: [],
|
||||||
|
touched: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const state = {
|
const state = {
|
||||||
@@ -100,7 +105,8 @@ describe('formReducer', () => {
|
|||||||
description: null
|
description: null
|
||||||
},
|
},
|
||||||
valid: false,
|
valid: false,
|
||||||
errors: []
|
errors: [],
|
||||||
|
touched: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const formId = 'testForm';
|
const formId = 'testForm';
|
||||||
@@ -127,7 +133,8 @@ describe('formReducer', () => {
|
|||||||
description: null
|
description: null
|
||||||
},
|
},
|
||||||
valid: false,
|
valid: false,
|
||||||
errors: []
|
errors: [],
|
||||||
|
touched: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const state = {
|
const state = {
|
||||||
@@ -139,7 +146,8 @@ describe('formReducer', () => {
|
|||||||
description: null
|
description: null
|
||||||
},
|
},
|
||||||
valid: true,
|
valid: true,
|
||||||
errors: []
|
errors: [],
|
||||||
|
touched: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const formId = 'testForm';
|
const formId = 'testForm';
|
||||||
@@ -160,7 +168,8 @@ describe('formReducer', () => {
|
|||||||
description: null
|
description: null
|
||||||
},
|
},
|
||||||
valid: true,
|
valid: true,
|
||||||
errors: []
|
errors: [],
|
||||||
|
touched: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -204,7 +213,8 @@ describe('formReducer', () => {
|
|||||||
fieldIndex: 0,
|
fieldIndex: 0,
|
||||||
message: 'error.validation.required'
|
message: 'error.validation.required'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
touched: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -236,7 +246,8 @@ describe('formReducer', () => {
|
|||||||
description: null
|
description: null
|
||||||
},
|
},
|
||||||
valid: true,
|
valid: true,
|
||||||
errors: []
|
errors: [],
|
||||||
|
touched: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -264,7 +275,8 @@ describe('formReducer', () => {
|
|||||||
fieldIndex: 0,
|
fieldIndex: 0,
|
||||||
message: 'error.validation.required'
|
message: 'error.validation.required'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
touched: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -275,4 +287,84 @@ describe('formReducer', () => {
|
|||||||
|
|
||||||
expect(newState.testForm.errors).toEqual([]);
|
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: [],
|
||||||
|
touched: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
testForm: {
|
||||||
|
data: {
|
||||||
|
author: null,
|
||||||
|
title: ['test'],
|
||||||
|
date: null,
|
||||||
|
description: null
|
||||||
|
},
|
||||||
|
valid: false,
|
||||||
|
errors: [],
|
||||||
|
touched: {
|
||||||
|
title: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const formId = 'testForm';
|
||||||
|
const touched = ['title'];
|
||||||
|
|
||||||
|
const action = new FormAddTouchedAction(formId, touched);
|
||||||
|
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: [],
|
||||||
|
touched: {
|
||||||
|
title: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
testForm: {
|
||||||
|
data: {
|
||||||
|
author: null,
|
||||||
|
title: ['test'],
|
||||||
|
date: null,
|
||||||
|
description: null
|
||||||
|
},
|
||||||
|
valid: false,
|
||||||
|
errors: [],
|
||||||
|
touched: {
|
||||||
|
title: true,
|
||||||
|
author: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const formId = 'testForm';
|
||||||
|
const touched = ['author'];
|
||||||
|
|
||||||
|
const action = new FormAddTouchedAction(formId, touched);
|
||||||
|
const newState = formReducer(initState, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -4,9 +4,8 @@ import {
|
|||||||
FormAddError,
|
FormAddError,
|
||||||
FormChangeAction, FormClearErrorsAction,
|
FormChangeAction, FormClearErrorsAction,
|
||||||
FormInitAction,
|
FormInitAction,
|
||||||
FormRemoveAction,
|
FormRemoveAction, FormRemoveErrorAction,
|
||||||
FormRemoveErrorAction,
|
FormStatusChangeAction, FormAddTouchedAction
|
||||||
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';
|
||||||
@@ -17,10 +16,15 @@ export interface FormError {
|
|||||||
fieldIndex: number;
|
fieldIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FormTouchedState {
|
||||||
|
[key: string]: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface FormEntry {
|
export interface FormEntry {
|
||||||
data: any;
|
data: any;
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
errors: FormError[];
|
errors: FormError[];
|
||||||
|
touched: FormTouchedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FormState {
|
export interface FormState {
|
||||||
@@ -40,6 +44,10 @@ export function formReducer(state = initialState, action: FormAction): FormState
|
|||||||
return changeDataForm(state, action as FormChangeAction);
|
return changeDataForm(state, action as FormChangeAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case FormActionTypes.FORM_ADD_TOUCHED: {
|
||||||
|
return changeTouchedState(state, action as FormAddTouchedAction);
|
||||||
|
}
|
||||||
|
|
||||||
case FormActionTypes.FORM_REMOVE: {
|
case FormActionTypes.FORM_REMOVE: {
|
||||||
return removeForm(state, action as FormRemoveAction);
|
return removeForm(state, action as FormRemoveAction);
|
||||||
}
|
}
|
||||||
@@ -127,7 +135,8 @@ function initForm(state: FormState, action: FormInitAction): FormState {
|
|||||||
const formState = {
|
const formState = {
|
||||||
data: action.payload.formData,
|
data: action.payload.formData,
|
||||||
valid: action.payload.valid,
|
valid: action.payload.valid,
|
||||||
errors: []
|
touched: {},
|
||||||
|
errors: [],
|
||||||
};
|
};
|
||||||
if (!hasValue(state[action.payload.formId])) {
|
if (!hasValue(state[action.payload.formId])) {
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
@@ -212,3 +221,24 @@ function removeForm(state: FormState, action: FormRemoveAction): FormState {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the touched state of the form. New touched fields are merged with the previous ones.
|
||||||
|
* @param state
|
||||||
|
* @param action
|
||||||
|
*/
|
||||||
|
function changeTouchedState(state: FormState, action: FormAddTouchedAction): FormState {
|
||||||
|
if (hasValue(state[action.payload.formId])) {
|
||||||
|
const newState = Object.assign({}, state);
|
||||||
|
|
||||||
|
const newForm = Object.assign({}, newState[action.payload.formId]);
|
||||||
|
newState[action.payload.formId] = newForm;
|
||||||
|
|
||||||
|
newForm.touched = { ... newForm.touched};
|
||||||
|
action.payload.touched.forEach((field) => newForm.touched[field] = true);
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
} else {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -84,7 +84,8 @@ describe('FormService test suite', () => {
|
|||||||
testForm: {
|
testForm: {
|
||||||
data: formData,
|
data: formData,
|
||||||
valid: false,
|
valid: false,
|
||||||
errors: []
|
errors: [],
|
||||||
|
touched: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { map, distinctUntilChanged, filter } from 'rxjs/operators';
|
import { map, distinctUntilChanged, filter } from 'rxjs/operators';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
|
import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { select, Store } from '@ngrx/store';
|
import { select, Store } from '@ngrx/store';
|
||||||
@@ -7,16 +7,16 @@ import { select, Store } from '@ngrx/store';
|
|||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { formObjectFromIdSelector } from './selectors';
|
import { formObjectFromIdSelector } from './selectors';
|
||||||
import { FormBuilderService } from './builder/form-builder.service';
|
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 { isEmpty, isNotUndefined } from '../empty.util';
|
||||||
import { uniqueId } from 'lodash';
|
import { uniqueId } from 'lodash';
|
||||||
import {
|
import {
|
||||||
FormChangeAction,
|
FormChangeAction,
|
||||||
FormInitAction,
|
FormInitAction,
|
||||||
FormRemoveAction, FormRemoveErrorAction,
|
FormRemoveAction, FormRemoveErrorAction, FormAddTouchedAction,
|
||||||
FormStatusChangeAction
|
FormStatusChangeAction
|
||||||
} from './form.actions';
|
} from './form.actions';
|
||||||
import { FormEntry } from './form.reducer';
|
import { FormEntry, FormTouchedState } from './form.reducer';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -51,6 +51,18 @@ export class FormService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to retrieve form's touched state
|
||||||
|
*/
|
||||||
|
public getFormTouchedState(formId: string): Observable<FormTouchedState> {
|
||||||
|
return this.store.pipe(
|
||||||
|
select(formObjectFromIdSelector(formId)),
|
||||||
|
filter((state) => isNotUndefined(state)),
|
||||||
|
map((state) => state.touched),
|
||||||
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to retrieve form's errors from state
|
* Method to retrieve form's errors from state
|
||||||
*/
|
*/
|
||||||
@@ -169,6 +181,11 @@ export class FormService {
|
|||||||
this.store.dispatch(new FormChangeAction(formId, this.formBuilderService.getValueFromModel(model)));
|
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 FormAddTouchedAction(formId, ids));
|
||||||
|
}
|
||||||
|
|
||||||
public removeError(formId: string, eventModelId: string, fieldIndex: number) {
|
public removeError(formId: string, eventModelId: string, fieldIndex: number) {
|
||||||
this.store.dispatch(new FormRemoveErrorAction(formId, eventModelId, fieldIndex));
|
this.store.dispatch(new FormRemoveErrorAction(formId, eventModelId, fieldIndex));
|
||||||
}
|
}
|
||||||
|
@@ -60,6 +60,21 @@ export const mockSectionsErrors = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const mockSectionsErrorsTwo = [
|
||||||
|
{
|
||||||
|
message: 'error.validation.required',
|
||||||
|
paths: [
|
||||||
|
'/sections/traditionalpageone/dc.title',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'error.validation.license.notgranted',
|
||||||
|
paths: [
|
||||||
|
'/sections/license'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
export const mockUploadResponse1Errors = {
|
export const mockUploadResponse1Errors = {
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
@@ -1033,6 +1048,7 @@ export const mockSubmissionState: SubmissionObjectState = Object.assign({}, {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
data: {},
|
data: {},
|
||||||
errors: [],
|
errors: [],
|
||||||
|
formId: '2_traditionalpageone',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isValid: false
|
isValid: false
|
||||||
} as any,
|
} as any,
|
||||||
|
@@ -2,6 +2,7 @@ export class SectionsServiceStub {
|
|||||||
|
|
||||||
checkSectionErrors = jasmine.createSpy('checkSectionErrors');
|
checkSectionErrors = jasmine.createSpy('checkSectionErrors');
|
||||||
dispatchRemoveSectionErrors = jasmine.createSpy('dispatchRemoveSectionErrors');
|
dispatchRemoveSectionErrors = jasmine.createSpy('dispatchRemoveSectionErrors');
|
||||||
|
dispatchSetSectionFormId = jasmine.createSpy('dispatchSetSectionFormId');
|
||||||
getSectionData = jasmine.createSpy('getSectionData');
|
getSectionData = jasmine.createSpy('getSectionData');
|
||||||
getSectionErrors = jasmine.createSpy('getSectionErrors');
|
getSectionErrors = jasmine.createSpy('getSectionErrors');
|
||||||
getSectionState = jasmine.createSpy('getSectionState');
|
getSectionState = jasmine.createSpy('getSectionState');
|
||||||
@@ -14,5 +15,5 @@ export class SectionsServiceStub {
|
|||||||
updateSectionData = jasmine.createSpy('updateSectionData');
|
updateSectionData = jasmine.createSpy('updateSectionData');
|
||||||
setSectionError = jasmine.createSpy('setSectionError');
|
setSectionError = jasmine.createSpy('setSectionError');
|
||||||
setSectionStatus = jasmine.createSpy('setSectionStatus');
|
setSectionStatus = jasmine.createSpy('setSectionStatus');
|
||||||
|
computeSectionConfiguredMetadata = jasmine.createSpy('computeSectionConfiguredMetadata');
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@ export class SubmissionServiceStub {
|
|||||||
getSubmissionStatus = jasmine.createSpy('getSubmissionStatus');
|
getSubmissionStatus = jasmine.createSpy('getSubmissionStatus');
|
||||||
getSubmissionSaveProcessingStatus = jasmine.createSpy('getSubmissionSaveProcessingStatus');
|
getSubmissionSaveProcessingStatus = jasmine.createSpy('getSubmissionSaveProcessingStatus');
|
||||||
getSubmissionDepositProcessingStatus = jasmine.createSpy('getSubmissionDepositProcessingStatus');
|
getSubmissionDepositProcessingStatus = jasmine.createSpy('getSubmissionDepositProcessingStatus');
|
||||||
|
hasUnsavedModification = jasmine.createSpy('hasUnsavedModification');
|
||||||
isSectionHidden = jasmine.createSpy('isSectionHidden');
|
isSectionHidden = jasmine.createSpy('isSectionHidden');
|
||||||
isSubmissionLoading = jasmine.createSpy('isSubmissionLoading');
|
isSubmissionLoading = jasmine.createSpy('isSubmissionLoading');
|
||||||
notifyNewSection = jasmine.createSpy('notifyNewSection');
|
notifyNewSection = jasmine.createSpy('notifyNewSection');
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-info"
|
class="btn btn-info"
|
||||||
id="save"
|
id="save"
|
||||||
[disabled]="(processingSaveStatus | async)"
|
[disabled]="(processingSaveStatus | async) || !(hasUnsavedModification | async)"
|
||||||
(click)="save($event)">
|
(click)="save($event)">
|
||||||
<span>{{'submission.general.save' | translate}}</span>
|
<span>{{'submission.general.save' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -169,7 +169,7 @@ describe('SubmissionFormFooterComponent Component', () => {
|
|||||||
comp.save(null);
|
comp.save(null);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(submissionServiceStub.dispatchSave).toHaveBeenCalledWith(submissionId);
|
expect(submissionServiceStub.dispatchSave).toHaveBeenCalledWith(submissionId, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call dispatchSaveForLater on save for later', () => {
|
it('should call dispatchSaveForLater on save for later', () => {
|
||||||
@@ -224,6 +224,22 @@ describe('SubmissionFormFooterComponent Component', () => {
|
|||||||
expect(depositBtn.nativeElement.disabled).toBeFalsy();
|
expect(depositBtn.nativeElement.disabled).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should disable save button when all modifications had been saved', () => {
|
||||||
|
comp.hasUnsavedModification = 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.hasUnsavedModification = observableOf(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const saveBtn: any = fixture.debugElement.query(By.css('#save'));
|
||||||
|
expect(saveBtn.nativeElement.disabled).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -49,6 +49,11 @@ export class SubmissionFormFooterComponent implements OnChanges {
|
|||||||
*/
|
*/
|
||||||
public submissionIsInvalid: Observable<boolean> = observableOf(true);
|
public submissionIsInvalid: Observable<boolean> = observableOf(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if submission form has unsaved modifications
|
||||||
|
*/
|
||||||
|
public hasUnsavedModification: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize instance variables
|
* Initialize instance variables
|
||||||
*
|
*
|
||||||
@@ -73,6 +78,7 @@ export class SubmissionFormFooterComponent implements OnChanges {
|
|||||||
this.processingSaveStatus = this.submissionService.getSubmissionSaveProcessingStatus(this.submissionId);
|
this.processingSaveStatus = this.submissionService.getSubmissionSaveProcessingStatus(this.submissionId);
|
||||||
this.processingDepositStatus = this.submissionService.getSubmissionDepositProcessingStatus(this.submissionId);
|
this.processingDepositStatus = this.submissionService.getSubmissionDepositProcessingStatus(this.submissionId);
|
||||||
this.showDepositAndDiscard = observableOf(this.submissionService.getSubmissionScope() === SubmissionScopeType.WorkspaceItem);
|
this.showDepositAndDiscard = observableOf(this.submissionService.getSubmissionScope() === SubmissionScopeType.WorkspaceItem);
|
||||||
|
this.hasUnsavedModification = this.submissionService.hasUnsavedModification();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +86,7 @@ export class SubmissionFormFooterComponent implements OnChanges {
|
|||||||
* Dispatch a submission save action
|
* Dispatch a submission save action
|
||||||
*/
|
*/
|
||||||
save(event) {
|
save(event) {
|
||||||
this.submissionService.dispatchSave(this.submissionId);
|
this.submissionService.dispatchSave(this.submissionId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -40,6 +40,7 @@ export const SubmissionObjectActionTypes = {
|
|||||||
INIT_SECTION: type('dspace/submission/INIT_SECTION'),
|
INIT_SECTION: type('dspace/submission/INIT_SECTION'),
|
||||||
ENABLE_SECTION: type('dspace/submission/ENABLE_SECTION'),
|
ENABLE_SECTION: type('dspace/submission/ENABLE_SECTION'),
|
||||||
DISABLE_SECTION: type('dspace/submission/DISABLE_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_STATUS_CHANGE: type('dspace/submission/SECTION_STATUS_CHANGE'),
|
||||||
SECTION_LOADING_STATUS_CHANGE: type('dspace/submission/SECTION_LOADING_STATUS_CHANGE'),
|
SECTION_LOADING_STATUS_CHANGE: type('dspace/submission/SECTION_LOADING_STATUS_CHANGE'),
|
||||||
UPDATE_SECTION_DATA: type('dspace/submission/UPDATE_SECTION_DATA'),
|
UPDATE_SECTION_DATA: type('dspace/submission/UPDATE_SECTION_DATA'),
|
||||||
@@ -206,6 +207,7 @@ export class UpdateSectionDataAction implements Action {
|
|||||||
sectionId: string;
|
sectionId: string;
|
||||||
data: WorkspaceitemSectionDataType;
|
data: WorkspaceitemSectionDataType;
|
||||||
errors: SubmissionSectionError[];
|
errors: SubmissionSectionError[];
|
||||||
|
metadata: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -219,12 +221,15 @@ export class UpdateSectionDataAction implements Action {
|
|||||||
* the section's data
|
* the section's data
|
||||||
* @param errors
|
* @param errors
|
||||||
* the section's errors
|
* the section's errors
|
||||||
|
* @param metadata
|
||||||
|
* the section's metadata
|
||||||
*/
|
*/
|
||||||
constructor(submissionId: string,
|
constructor(submissionId: string,
|
||||||
sectionId: string,
|
sectionId: string,
|
||||||
data: WorkspaceitemSectionDataType,
|
data: WorkspaceitemSectionDataType,
|
||||||
errors: SubmissionSectionError[]) {
|
errors: SubmissionSectionError[],
|
||||||
this.payload = { submissionId, sectionId, data, errors };
|
metadata?: string[]) {
|
||||||
|
this.payload = { submissionId, sectionId, data, errors, metadata };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,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
|
// Submission actions
|
||||||
|
|
||||||
export class CompleteInitSubmissionFormAction implements Action {
|
export class CompleteInitSubmissionFormAction implements Action {
|
||||||
@@ -368,6 +396,7 @@ export class SaveSubmissionFormAction implements Action {
|
|||||||
type = SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM;
|
type = SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM;
|
||||||
payload: {
|
payload: {
|
||||||
submissionId: string;
|
submissionId: string;
|
||||||
|
isManual?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -376,8 +405,8 @@ export class SaveSubmissionFormAction implements Action {
|
|||||||
* @param submissionId
|
* @param submissionId
|
||||||
* the submission's ID
|
* the submission's ID
|
||||||
*/
|
*/
|
||||||
constructor(submissionId: string) {
|
constructor(submissionId: string, isManual: boolean = false) {
|
||||||
this.payload = { submissionId };
|
this.payload = { submissionId, isManual };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -777,6 +806,7 @@ export class DeleteUploadedFileAction implements Action {
|
|||||||
*/
|
*/
|
||||||
export type SubmissionObjectAction = DisableSectionAction
|
export type SubmissionObjectAction = DisableSectionAction
|
||||||
| InitSectionAction
|
| InitSectionAction
|
||||||
|
| SetSectionFormId
|
||||||
| EnableSectionAction
|
| EnableSectionAction
|
||||||
| InitSubmissionFormAction
|
| InitSubmissionFormAction
|
||||||
| ResetSubmissionFormAction
|
| ResetSubmissionFormAction
|
||||||
|
@@ -32,7 +32,7 @@ import {
|
|||||||
mockSubmissionId,
|
mockSubmissionId,
|
||||||
mockSubmissionSelfUrl,
|
mockSubmissionSelfUrl,
|
||||||
mockSubmissionState,
|
mockSubmissionState,
|
||||||
mockSubmissionRestResponse
|
mockSubmissionRestResponse, mockSectionsErrorsTwo
|
||||||
} from '../../shared/mocks/submission.mock';
|
} from '../../shared/mocks/submission.mock';
|
||||||
import { SubmissionSectionModel } from '../../core/config/models/config-submission-section.model';
|
import { SubmissionSectionModel } from '../../core/config/models/config-submission-section.model';
|
||||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
@@ -51,15 +51,16 @@ import { Item } from '../../core/shared/item.model';
|
|||||||
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service';
|
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service';
|
||||||
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
|
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
|
||||||
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import {formStateSelector} from '../../shared/form/selectors';
|
||||||
|
|
||||||
describe('SubmissionObjectEffects test suite', () => {
|
describe('SubmissionObjectEffects test suite', () => {
|
||||||
let submissionObjectEffects: SubmissionObjectEffects;
|
let submissionObjectEffects: SubmissionObjectEffects;
|
||||||
let actions: Observable<any>;
|
let actions: Observable<any>;
|
||||||
let store: StoreMock<AppState>;
|
let store: StoreMock<AppState>;
|
||||||
|
|
||||||
const notificationsServiceStub = new NotificationsServiceStub();
|
let notificationsServiceStub;
|
||||||
const submissionServiceStub = new SubmissionServiceStub();
|
let submissionServiceStub;
|
||||||
const submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub();
|
let submissionJsonPatchOperationsServiceStub;
|
||||||
const collectionId: string = mockSubmissionCollectionId;
|
const collectionId: string = mockSubmissionCollectionId;
|
||||||
const submissionId: string = mockSubmissionId;
|
const submissionId: string = mockSubmissionId;
|
||||||
const submissionDefinitionResponse: any = mockSubmissionDefinitionResponse;
|
const submissionDefinitionResponse: any = mockSubmissionDefinitionResponse;
|
||||||
@@ -68,6 +69,11 @@ describe('SubmissionObjectEffects test suite', () => {
|
|||||||
const submissionState: any = Object.assign({}, mockSubmissionState);
|
const submissionState: any = Object.assign({}, mockSubmissionState);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
||||||
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
submissionServiceStub = new SubmissionServiceStub();
|
||||||
|
submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub();
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
StoreModule.forRoot({}, storeModuleConfig),
|
StoreModule.forRoot({}, storeModuleConfig),
|
||||||
@@ -206,6 +212,52 @@ describe('SubmissionObjectEffects test suite', () => {
|
|||||||
expect(submissionObjectEffects.saveSubmission$).toBeObservable(expected);
|
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', () => {
|
it('should return a SAVE_SUBMISSION_FORM_ERROR action on error', () => {
|
||||||
actions = hot('--a-', {
|
actions = hot('--a-', {
|
||||||
a: {
|
a: {
|
||||||
@@ -292,7 +344,8 @@ describe('SubmissionObjectEffects test suite', () => {
|
|||||||
type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS,
|
type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS,
|
||||||
payload: {
|
payload: {
|
||||||
submissionId: submissionId,
|
submissionId: submissionId,
|
||||||
submissionObject: response
|
submissionObject: response,
|
||||||
|
notify: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -324,6 +377,61 @@ 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': {
|
||||||
|
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', () => {
|
it('should display a success notification', () => {
|
||||||
store.nextState({
|
store.nextState({
|
||||||
submission: {
|
submission: {
|
||||||
@@ -471,6 +579,203 @@ 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,
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
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);
|
||||||
|
console.log(errorsList);
|
||||||
|
const expected = cold('--(bcd)-', {
|
||||||
|
b: new UpdateSectionDataAction(
|
||||||
|
submissionId,
|
||||||
|
'traditionalpageone',
|
||||||
|
mockSectionsData.traditionalpageone as any,
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
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$', () => {
|
describe('saveSection$', () => {
|
||||||
it('should return a SAVE_SUBMISSION_SECTION_FORM_SUCCESS action on success', () => {
|
it('should return a SAVE_SUBMISSION_SECTION_FORM_SUCCESS action on success', () => {
|
||||||
actions = hot('--a-', {
|
actions = hot('--a-', {
|
||||||
|
@@ -52,12 +52,14 @@ import {
|
|||||||
UpdateSectionDataAction,
|
UpdateSectionDataAction,
|
||||||
UpdateSectionDataSuccessAction
|
UpdateSectionDataSuccessAction
|
||||||
} from './submission-objects.actions';
|
} 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 { 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 { FormState } from '../../shared/form/form.reducer';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SubmissionObjectEffects {
|
export class SubmissionObjectEffects {
|
||||||
@@ -132,7 +134,7 @@ export class SubmissionObjectEffects {
|
|||||||
this.submissionService.getSubmissionObjectLinkName(),
|
this.submissionService.getSubmissionObjectLinkName(),
|
||||||
action.payload.submissionId,
|
action.payload.submissionId,
|
||||||
'sections').pipe(
|
'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))));
|
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -154,10 +156,24 @@ export class SubmissionObjectEffects {
|
|||||||
* Call parseSaveResponse and dispatch actions
|
* Call parseSaveResponse and dispatch actions
|
||||||
*/
|
*/
|
||||||
@Effect() saveSubmissionSuccess$ = this.actions$.pipe(
|
@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$),
|
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);
|
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)));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, currentState.forms, false);
|
||||||
}),
|
}),
|
||||||
mergeMap((actions) => observableFrom(actions)));
|
mergeMap((actions) => observableFrom(actions)));
|
||||||
|
|
||||||
@@ -200,7 +216,8 @@ export class SubmissionObjectEffects {
|
|||||||
return new DepositSubmissionAction(action.payload.submissionId);
|
return new DepositSubmissionAction(action.payload.submissionId);
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.warning(null, this.translate.get('submission.sections.general.sections_not_valid'));
|
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))));
|
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
|
||||||
@@ -280,7 +297,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))
|
map((metadata: any) => new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, metadata, action.payload.errors, action.payload.metadata))
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return observableOf(new UpdateSectionDataSuccessAction());
|
return observableOf(new UpdateSectionDataSuccessAction());
|
||||||
@@ -353,6 +370,7 @@ export class SubmissionObjectEffects {
|
|||||||
currentState: SubmissionObjectEntry,
|
currentState: SubmissionObjectEntry,
|
||||||
response: SubmissionObject[],
|
response: SubmissionObject[],
|
||||||
submissionId: string,
|
submissionId: string,
|
||||||
|
forms,
|
||||||
notify: boolean = true): SubmissionObjectAction[] {
|
notify: boolean = true): SubmissionObjectAction[] {
|
||||||
|
|
||||||
const mappedActions = [];
|
const mappedActions = [];
|
||||||
@@ -392,10 +410,54 @@ export class SubmissionObjectEffects {
|
|||||||
if (notify && !currentState.sections[sectionId].enabled) {
|
if (notify && !currentState.sections[sectionId].enabled) {
|
||||||
this.submissionService.notifyNewSection(submissionId, sectionId, currentState.sections[sectionId].sectionType);
|
this.submissionService.notifyNewSection(submissionId, sectionId, currentState.sections[sectionId].sectionType);
|
||||||
}
|
}
|
||||||
mappedActions.push(new UpdateSectionDataAction(submissionId, sectionId, sectionData, sectionErrors));
|
|
||||||
|
const sectionForm = getForm(forms, currentState, sectionId);
|
||||||
|
const filteredErrors = filterErrors(sectionForm, sectionErrors, currentState.sections[sectionId].sectionType, notify);
|
||||||
|
mappedActions.push(new UpdateSectionDataAction(submissionId, sectionId, sectionData, filteredErrors));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return mappedActions;
|
return mappedActions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
* 2. if sectionType is different from 'submission-form' return all errors
|
||||||
|
* 3. otherwise return errors only for those fields marked as touched inside the section form
|
||||||
|
* @param sectionForm
|
||||||
|
* The form related to the section
|
||||||
|
* @param sectionErrors
|
||||||
|
* The section errors array
|
||||||
|
* @param sectionType
|
||||||
|
* The section type
|
||||||
|
* @param notify
|
||||||
|
* Whether notifications are enabled
|
||||||
|
*/
|
||||||
|
function filterErrors(sectionForm: FormState, sectionErrors: SubmissionSectionError[], sectionType: string, notify: boolean): SubmissionSectionError[] {
|
||||||
|
if (notify || sectionType !== SectionsType.SubmissionForm) {
|
||||||
|
return sectionErrors;
|
||||||
|
}
|
||||||
|
if (!sectionForm || !sectionForm.touched) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const filteredErrors = [];
|
||||||
|
sectionErrors.forEach((error: SubmissionSectionError) => {
|
||||||
|
const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path);
|
||||||
|
errorPaths.forEach((path: SectionErrorPath) => {
|
||||||
|
if (path.fieldId && sectionForm.touched[path.fieldId]) {
|
||||||
|
filteredErrors.push(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return filteredErrors;
|
||||||
|
}
|
||||||
|
@@ -335,6 +335,17 @@ describe('submissionReducer test suite', () => {
|
|||||||
expect(newState[826].sections.traditionalpageone.data).toEqual(data);
|
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', () => {
|
it('should add submission section errors properly', () => {
|
||||||
const errors = [
|
const errors = [
|
||||||
{
|
{
|
||||||
|
@@ -30,6 +30,7 @@ import {
|
|||||||
SaveSubmissionSectionFormSuccessAction,
|
SaveSubmissionSectionFormSuccessAction,
|
||||||
SectionStatusChangeAction,
|
SectionStatusChangeAction,
|
||||||
SetActiveSectionAction,
|
SetActiveSectionAction,
|
||||||
|
SetSectionFormId,
|
||||||
SubmissionObjectAction,
|
SubmissionObjectAction,
|
||||||
SubmissionObjectActionTypes,
|
SubmissionObjectActionTypes,
|
||||||
UpdateSectionDataAction
|
UpdateSectionDataAction
|
||||||
@@ -85,6 +86,11 @@ export interface SubmissionSectionObject {
|
|||||||
*/
|
*/
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of the metadata ids of the section.
|
||||||
|
*/
|
||||||
|
metadata: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The section data object
|
* The section data object
|
||||||
*/
|
*/
|
||||||
@@ -104,6 +110,11 @@ export interface SubmissionSectionObject {
|
|||||||
* A boolean representing if this section is valid
|
* A boolean representing if this section is valid
|
||||||
*/
|
*/
|
||||||
isValid: boolean;
|
isValid: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The formId related to this section
|
||||||
|
*/
|
||||||
|
formId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -258,6 +269,10 @@ export function submissionObjectReducer(state = initialState, action: Submission
|
|||||||
return initSection(state, action as InitSectionAction);
|
return initSection(state, action as InitSectionAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case SubmissionObjectActionTypes.SET_SECTION_FORM_ID: {
|
||||||
|
return setSectionFormId(state, action as SetSectionFormId);
|
||||||
|
}
|
||||||
|
|
||||||
case SubmissionObjectActionTypes.ENABLE_SECTION: {
|
case SubmissionObjectActionTypes.ENABLE_SECTION: {
|
||||||
return changeSectionState(state, action as EnableSectionAction, true);
|
return changeSectionState(state, action as EnableSectionAction, true);
|
||||||
}
|
}
|
||||||
@@ -641,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.
|
* Update section's data.
|
||||||
*
|
*
|
||||||
@@ -653,14 +695,15 @@ function initSection(state: SubmissionObjectState, action: InitSectionAction): S
|
|||||||
*/
|
*/
|
||||||
function updateSectionData(state: SubmissionObjectState, action: UpdateSectionDataAction): SubmissionObjectState {
|
function updateSectionData(state: SubmissionObjectState, action: UpdateSectionDataAction): SubmissionObjectState {
|
||||||
if (isNotEmpty(state[ action.payload.submissionId ])
|
if (isNotEmpty(state[ action.payload.submissionId ])
|
||||||
&& isNotEmpty(state[ action.payload.submissionId ].sections[ action.payload.sectionId])) {
|
&& isNotEmpty(state[ action.payload.submissionId ].sections[ action.payload.sectionId])) {
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
[ 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 ], {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
data: action.payload.data,
|
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 +713,24 @@ 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
|
||||||
|
* @return
|
||||||
|
* new sectionMetadata value
|
||||||
|
*/
|
||||||
|
function reduceSectionMetadata(newMetadata: string[], oldMetadata: string[]): string[] {
|
||||||
|
if (newMetadata) {
|
||||||
|
return newMetadata;
|
||||||
|
}
|
||||||
|
if (oldMetadata) {
|
||||||
|
return [...oldMetadata];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a section state.
|
* Set a section state.
|
||||||
*
|
*
|
||||||
|
@@ -287,6 +287,7 @@ describe('SubmissionSectionformComponent test suite', () => {
|
|||||||
'dc.title': [new FormFieldMetadataValueObject('test')]
|
'dc.title': [new FormFieldMetadataValueObject('test')]
|
||||||
};
|
};
|
||||||
compAsAny.formData = {};
|
compAsAny.formData = {};
|
||||||
|
compAsAny.sectionMetadata = ['dc.title'];
|
||||||
|
|
||||||
expect(comp.hasMetadataEnrichment(newSectionData)).toBeTruthy();
|
expect(comp.hasMetadataEnrichment(newSectionData)).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -296,7 +297,16 @@ describe('SubmissionSectionformComponent test suite', () => {
|
|||||||
'dc.title': [new FormFieldMetadataValueObject('test')]
|
'dc.title': [new FormFieldMetadataValueObject('test')]
|
||||||
};
|
};
|
||||||
compAsAny.formData = newSectionData;
|
compAsAny.formData = newSectionData;
|
||||||
|
compAsAny.sectionMetadata = ['dc.title'];
|
||||||
|
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();
|
expect(comp.hasMetadataEnrichment(newSectionData)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -310,6 +320,7 @@ describe('SubmissionSectionformComponent test suite', () => {
|
|||||||
comp.sectionData.data = {};
|
comp.sectionData.data = {};
|
||||||
comp.sectionData.errors = [];
|
comp.sectionData.errors = [];
|
||||||
compAsAny.formData = {};
|
compAsAny.formData = {};
|
||||||
|
compAsAny.sectionMetadata = ['dc.title'];
|
||||||
|
|
||||||
comp.updateForm(sectionData, sectionError);
|
comp.updateForm(sectionData, sectionError);
|
||||||
|
|
||||||
@@ -329,10 +340,11 @@ describe('SubmissionSectionformComponent test suite', () => {
|
|||||||
comp.sectionData.data = {};
|
comp.sectionData.data = {};
|
||||||
comp.sectionData.errors = [];
|
comp.sectionData.errors = [];
|
||||||
compAsAny.formData = sectionData;
|
compAsAny.formData = sectionData;
|
||||||
|
compAsAny.sectionMetadata = ['dc.title'];
|
||||||
|
|
||||||
comp.updateForm(sectionData, parsedSectionErrors);
|
comp.updateForm(sectionData, parsedSectionErrors);
|
||||||
|
|
||||||
expect(comp.initForm).toHaveBeenCalled();
|
expect(comp.initForm).not.toHaveBeenCalled();
|
||||||
expect(comp.checksForErrors).toHaveBeenCalled();
|
expect(comp.checksForErrors).toHaveBeenCalled();
|
||||||
expect(comp.sectionData.data).toEqual(sectionData);
|
expect(comp.sectionData.data).toEqual(sectionData);
|
||||||
});
|
});
|
||||||
|
@@ -13,7 +13,7 @@ import {
|
|||||||
tap
|
tap
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual, findIndex } from 'lodash';
|
||||||
|
|
||||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||||
import { FormComponent } from '../../../shared/form/form.component';
|
import { FormComponent } from '../../../shared/form/form.component';
|
||||||
@@ -101,6 +101,12 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
|
|||||||
*/
|
*/
|
||||||
protected formData: any = Object.create({});
|
protected formData: any = Object.create({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected sectionMetadata: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [JsonPatchOperationPathCombiner] object
|
* The [JsonPatchOperationPathCombiner] object
|
||||||
* @type {JsonPatchOperationPathCombiner}
|
* @type {JsonPatchOperationPathCombiner}
|
||||||
@@ -168,6 +174,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
|
|||||||
onSectionInit() {
|
onSectionInit() {
|
||||||
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id);
|
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id);
|
||||||
this.formId = this.formService.getUniqueId(this.sectionData.id);
|
this.formId = this.formService.getUniqueId(this.sectionData.id);
|
||||||
|
this.sectionService.dispatchSetSectionFormId(this.submissionId, this.sectionData.id, this.formId);
|
||||||
this.formConfigService.findByHref(this.sectionData.config).pipe(
|
this.formConfigService.findByHref(this.sectionData.config).pipe(
|
||||||
map((configData: RemoteData<ConfigObject>) => configData.payload),
|
map((configData: RemoteData<ConfigObject>) => configData.payload),
|
||||||
tap((config: SubmissionFormsModel) => this.formConfig = config),
|
tap((config: SubmissionFormsModel) => this.formConfig = config),
|
||||||
@@ -230,16 +237,25 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
|
|||||||
* the section data retrieved from the server
|
* the section data retrieved from the server
|
||||||
*/
|
*/
|
||||||
hasMetadataEnrichment(sectionData: WorkspaceitemSectionFormObject): boolean {
|
hasMetadataEnrichment(sectionData: WorkspaceitemSectionFormObject): boolean {
|
||||||
|
|
||||||
|
const sectionDataToCheck = {};
|
||||||
|
Object.keys(sectionData).forEach((key) => {
|
||||||
|
if (this.sectionMetadata && this.sectionMetadata.includes(key)) {
|
||||||
|
sectionDataToCheck[key] = sectionData[key];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const diffResult = [];
|
const diffResult = [];
|
||||||
|
|
||||||
// compare current form data state with section data retrieved from store
|
// 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
|
// iterate over differences to check whether they are actually different
|
||||||
Object.keys(diffObj)
|
Object.keys(diffObj)
|
||||||
.forEach((key) => {
|
.forEach((key) => {
|
||||||
diffObj[key].forEach((value) => {
|
diffObj[key].forEach((value) => {
|
||||||
if (value.hasOwnProperty('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);
|
diffResult.push(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -262,6 +278,9 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
|
|||||||
sectionData,
|
sectionData,
|
||||||
this.submissionService.getSubmissionScope()
|
this.submissionService.getSubmissionScope()
|
||||||
);
|
);
|
||||||
|
const sectionMetadata = this.sectionService.computeSectionConfiguredMetadata(this.formConfig);
|
||||||
|
this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, sectionData, [], 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();
|
||||||
const sectionError: SubmissionSectionError = {
|
const sectionError: SubmissionSectionError = {
|
||||||
@@ -283,15 +302,19 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
|
|||||||
*/
|
*/
|
||||||
updateForm(sectionData: WorkspaceitemSectionFormObject, errors: SubmissionSectionError[]): void {
|
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.sectionData.data = sectionData;
|
||||||
this.isUpdating = true;
|
if (this.hasMetadataEnrichment(sectionData)) {
|
||||||
this.formModel = null;
|
this.isUpdating = true;
|
||||||
this.cdr.detectChanges();
|
this.formModel = null;
|
||||||
this.initForm(sectionData);
|
this.cdr.detectChanges();
|
||||||
this.checksForErrors(errors);
|
this.initForm(sectionData);
|
||||||
this.isUpdating = false;
|
this.checksForErrors(errors);
|
||||||
this.cdr.detectChanges();
|
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)) {
|
} else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) {
|
||||||
this.checksForErrors(errors);
|
this.checksForErrors(errors);
|
||||||
}
|
}
|
||||||
@@ -338,6 +361,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
|
|||||||
distinctUntilChanged())
|
distinctUntilChanged())
|
||||||
.subscribe((sectionState: SubmissionSectionObject) => {
|
.subscribe((sectionState: SubmissionSectionObject) => {
|
||||||
this.fieldsOnTheirWayToBeRemoved = new Map();
|
this.fieldsOnTheirWayToBeRemoved = new Map();
|
||||||
|
this.sectionMetadata = sectionState.metadata;
|
||||||
this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errors);
|
this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errors);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@@ -380,4 +380,25 @@ describe('SectionsService test suite', () => {
|
|||||||
expect(store.dispatch).toHaveBeenCalledWith(new UpdateSectionDataAction(submissionId, sectionId, data, []));
|
expect(store.dispatch).toHaveBeenCalledWith(new UpdateSectionDataAction(submissionId, sectionId, data, []));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('computeSectionConfiguredMetadata', () => {
|
||||||
|
it('should return the configured metadata of the section from the form configuration', () => {
|
||||||
|
|
||||||
|
const formConfig = {
|
||||||
|
rows: [{
|
||||||
|
fields: [{
|
||||||
|
selectableMetadata: [{
|
||||||
|
metadata: 'dc.contributor.author'
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedConfiguredMetadata = [ 'dc.contributor.author' ];
|
||||||
|
|
||||||
|
const configuredMetadata = service.computeSectionConfiguredMetadata(formConfig as any);
|
||||||
|
|
||||||
|
expect(configuredMetadata).toEqual(expectedConfiguredMetadata);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -15,6 +15,7 @@ import {
|
|||||||
InertSectionErrorsAction,
|
InertSectionErrorsAction,
|
||||||
RemoveSectionErrorsAction,
|
RemoveSectionErrorsAction,
|
||||||
SectionStatusChangeAction,
|
SectionStatusChangeAction,
|
||||||
|
SetSectionFormId,
|
||||||
UpdateSectionDataAction
|
UpdateSectionDataAction
|
||||||
} from '../objects/submission-objects.actions';
|
} from '../objects/submission-objects.actions';
|
||||||
import {
|
import {
|
||||||
@@ -36,6 +37,8 @@ import { SubmissionService } from '../submission.service';
|
|||||||
import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model';
|
import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model';
|
||||||
import { SectionsType } from './sections-type';
|
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 { parseReviver } from '@ng-dynamic-forms/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that provides methods used in submission process.
|
* A service that provides methods used in submission process.
|
||||||
@@ -133,6 +136,18 @@ export class SectionsService {
|
|||||||
this.store.dispatch(new RemoveSectionErrorsAction(submissionId, sectionId));
|
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
|
* Return the data object for the specified section
|
||||||
*
|
*
|
||||||
@@ -335,8 +350,10 @@ export class SectionsService {
|
|||||||
* The section data
|
* The section data
|
||||||
* @param errors
|
* @param errors
|
||||||
* The list of section 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)) {
|
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);
|
||||||
@@ -345,7 +362,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));
|
this.store.dispatch(new UpdateSectionDataAction(submissionId, sectionId, data, errors, metadata));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,4 +394,30 @@ export class SectionsService {
|
|||||||
public setSectionStatus(submissionId: string, sectionId: string, status: boolean) {
|
public setSectionStatus(submissionId: string, sectionId: string, status: boolean) {
|
||||||
this.store.dispatch(new SectionStatusChangeAction(submissionId, sectionId, status));
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -46,6 +46,8 @@ import { SearchService } from '../core/shared/search/search.service';
|
|||||||
import { Item } from '../core/shared/item.model';
|
import { Item } from '../core/shared/item.model';
|
||||||
import { storeModuleConfig } from '../app.reducer';
|
import { storeModuleConfig } from '../app.reducer';
|
||||||
import { environment } from '../../environments/environment';
|
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', () => {
|
describe('SubmissionService test suite', () => {
|
||||||
const collectionId = '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f';
|
const collectionId = '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f';
|
||||||
@@ -345,6 +347,7 @@ describe('SubmissionService test suite', () => {
|
|||||||
const router = new RouterMock();
|
const router = new RouterMock();
|
||||||
const selfUrl = 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826';
|
const selfUrl = 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826';
|
||||||
const submissionDefinition: any = mockSubmissionDefinition;
|
const submissionDefinition: any = mockSubmissionDefinition;
|
||||||
|
const submissionJsonPatchOperationsService = new SubmissionJsonPatchOperationsServiceStub();
|
||||||
|
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: SubmissionService;
|
let service: SubmissionService;
|
||||||
@@ -371,6 +374,7 @@ describe('SubmissionService test suite', () => {
|
|||||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||||
{ provide: SearchService, useValue: searchService },
|
{ provide: SearchService, useValue: searchService },
|
||||||
{ provide: RequestService, useValue: requestServce },
|
{ provide: RequestService, useValue: requestServce },
|
||||||
|
{ provide: SubmissionJsonPatchOperationsService, useValue: submissionJsonPatchOperationsService },
|
||||||
NotificationsService,
|
NotificationsService,
|
||||||
RouteService,
|
RouteService,
|
||||||
SubmissionService,
|
SubmissionService,
|
||||||
@@ -487,11 +491,18 @@ describe('SubmissionService test suite', () => {
|
|||||||
|
|
||||||
describe('dispatchSave', () => {
|
describe('dispatchSave', () => {
|
||||||
it('should dispatch a new SaveSubmissionFormAction', () => {
|
it('should dispatch a new SaveSubmissionFormAction', () => {
|
||||||
service.dispatchSave(submissionId,);
|
service.dispatchSave(submissionId);
|
||||||
const expected = new SaveSubmissionFormAction(submissionId);
|
const expected = new SaveSubmissionFormAction(submissionId);
|
||||||
|
|
||||||
expect((service as any).store.dispatch).toHaveBeenCalledWith(expected);
|
expect((service as any).store.dispatch).toHaveBeenCalledWith(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('dispatchSaveForLater', () => {
|
describe('dispatchSaveForLater', () => {
|
||||||
@@ -746,6 +757,20 @@ describe('SubmissionService test suite', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('hasUnsavedModification', () => {
|
||||||
|
it('should call jsonPatchOperationService hasPendingOperation observable', () => {
|
||||||
|
(service as any).jsonPatchOperationService.hasPendingOperations = jasmine.createSpy('hasPendingOperations')
|
||||||
|
.and.returnValue(observableOf(true));
|
||||||
|
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
scheduler.schedule(() => service.hasUnsavedModification());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect((service as any).jsonPatchOperationService.hasPendingOperations).toHaveBeenCalledWith('sections');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('isSectionHidden', () => {
|
describe('isSectionHidden', () => {
|
||||||
it('should return true/false when section is hidden/visible', () => {
|
it('should return true/false when section is hidden/visible', () => {
|
||||||
let section: any = {
|
let section: any = {
|
||||||
@@ -915,8 +940,15 @@ describe('SubmissionService test suite', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('startAutoSave', () => {
|
describe('startAutoSave', () => {
|
||||||
|
|
||||||
|
let environmentAutoSaveTimerOriginalValue;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
environmentAutoSaveTimerOriginalValue = environment.submission.autosave.timer;
|
||||||
|
});
|
||||||
|
|
||||||
it('should start Auto Save', fakeAsync(() => {
|
it('should start Auto Save', fakeAsync(() => {
|
||||||
const duration = environment.submission.autosave.timer * (1000 * 60);
|
const duration = environment.submission.autosave.timer;
|
||||||
|
|
||||||
service.startAutoSave('826');
|
service.startAutoSave('826');
|
||||||
const sub = (service as any).timer$.subscribe();
|
const sub = (service as any).timer$.subscribe();
|
||||||
@@ -930,6 +962,19 @@ describe('SubmissionService test suite', () => {
|
|||||||
sub.unsubscribe();
|
sub.unsubscribe();
|
||||||
(service as any).autoSaveSub.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', () => {
|
describe('stopAutoSave', () => {
|
||||||
|
@@ -45,6 +45,7 @@ import { RequestService } from '../core/data/request.service';
|
|||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
import { Item } from '../core/shared/item.model';
|
import { Item } from '../core/shared/item.model';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
|
import { SubmissionJsonPatchOperationsService } from '../core/submission/submission-json-patch-operations.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that provides methods used in submission process.
|
* A service that provides methods used in submission process.
|
||||||
@@ -82,7 +83,8 @@ export class SubmissionService {
|
|||||||
protected store: Store<SubmissionState>,
|
protected store: Store<SubmissionState>,
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
protected searchService: SearchService,
|
protected searchService: SearchService,
|
||||||
protected requestService: RequestService) {
|
protected requestService: RequestService,
|
||||||
|
protected jsonPatchOperationService: SubmissionJsonPatchOperationsService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -209,12 +211,14 @@ export class SubmissionService {
|
|||||||
*
|
*
|
||||||
* @param submissionId
|
* @param submissionId
|
||||||
* The submission id
|
* The submission id
|
||||||
|
* @param manual
|
||||||
|
* whether is a manual save, default false
|
||||||
*/
|
*/
|
||||||
dispatchSave(submissionId) {
|
dispatchSave(submissionId, manual?: boolean) {
|
||||||
this.getSubmissionSaveProcessingStatus(submissionId).pipe(
|
this.getSubmissionSaveProcessingStatus(submissionId).pipe(
|
||||||
find((isPending: boolean) => !isPending)
|
find((isPending: boolean) => !isPending)
|
||||||
).subscribe(() => {
|
).subscribe(() => {
|
||||||
this.store.dispatch(new SaveSubmissionFormAction(submissionId));
|
this.store.dispatch(new SaveSubmissionFormAction(submissionId, manual));
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,6 +431,16 @@ export class SubmissionService {
|
|||||||
startWith(false));
|
startWith(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether submission unsaved modification are present
|
||||||
|
*
|
||||||
|
* @return Observable<boolean>
|
||||||
|
* observable with submission unsaved modification presence
|
||||||
|
*/
|
||||||
|
hasUnsavedModification(): Observable<boolean> {
|
||||||
|
return this.jsonPatchOperationService.hasPendingOperations('sections');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the visibility status of the specified section
|
* Return the visibility status of the specified section
|
||||||
*
|
*
|
||||||
@@ -562,9 +576,12 @@ export class SubmissionService {
|
|||||||
*/
|
*/
|
||||||
startAutoSave(submissionId) {
|
startAutoSave(submissionId) {
|
||||||
this.stopAutoSave();
|
this.stopAutoSave();
|
||||||
|
if (environment.submission.autosave.timer === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// AUTOSAVE submission
|
// AUTOSAVE submission
|
||||||
// Retrieve interval from config and convert to milliseconds
|
const duration = environment.submission.autosave.timer;
|
||||||
const duration = environment.submission.autosave.timer * (1000 * 60);
|
|
||||||
// Dispatch save action after given duration
|
// Dispatch save action after given duration
|
||||||
this.timer$ = observableTimer(duration, duration);
|
this.timer$ = observableTimer(duration, duration);
|
||||||
this.autoSaveSub = this.timer$
|
this.autoSaveSub = this.timer$
|
||||||
|
@@ -65,9 +65,9 @@ export const environment: GlobalConfig = {
|
|||||||
submission: {
|
submission: {
|
||||||
autosave: {
|
autosave: {
|
||||||
// NOTE: which metadata trigger an 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
|
// NOTE: every how many minutes submission is saved automatically
|
||||||
timer: 5
|
timer: 0
|
||||||
},
|
},
|
||||||
icons: {
|
icons: {
|
||||||
metadata: [
|
metadata: [
|
||||||
|
Reference in New Issue
Block a user