mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'main' into CST-3620
# Conflicts: # src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.spec.ts # src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.ts
This commit is contained in:
@@ -1,12 +1,7 @@
|
||||
<ng-template dsListableObject>
|
||||
</ng-template>
|
||||
<div #badges class="position-absolute ml-1">
|
||||
<div *ngIf="dso && !dso.isDiscoverable" class="private-badge">
|
||||
<span class="badge badge-danger">{{ "admin.search.item.private" | translate }}</span>
|
||||
</div>
|
||||
<div *ngIf="dso && dso.isWithdrawn" class="withdrawn-badge">
|
||||
<span class="badge badge-warning">{{ "admin.search.item.withdrawn" | translate }}</span>
|
||||
</div>
|
||||
<div #badges>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<ul #buttons class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
@@ -13,7 +12,6 @@ import { SharedModule } from '../../../../../shared/shared.module';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-result-grid-element.component';
|
||||
@@ -71,51 +69,4 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when the item is not withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isWithdrawn = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the withdrawn badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||
expect(badge).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isWithdrawn = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the withdrawn badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||
expect(badge).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is not private', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isDiscoverable = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should not show the private badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||
expect(badge).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is private', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isDiscoverable = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the private badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||
expect(badge).not.toBeNull();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
@@ -1,12 +1,7 @@
|
||||
<div *ngIf="dso && !dso.isDiscoverable" class="private-badge">
|
||||
<span class="badge badge-danger">{{ "admin.search.item.private" | translate }}</span>
|
||||
</div>
|
||||
<div *ngIf="dso && dso.isWithdrawn" class="withdrawn-badge">
|
||||
<span class="badge badge-warning">{{ "admin.search.item.withdrawn" | translate }}</span>
|
||||
</div>
|
||||
<ds-listable-object-component-loader [object]="object"
|
||||
[viewMode]="viewModes.ListElement"
|
||||
[index]="index"
|
||||
[linkType]="linkType"
|
||||
[listID]="listID"></ds-listable-object-component-loader>
|
||||
[listID]="listID"
|
||||
[hideBadges]="true"></ds-listable-object-component-loader>
|
||||
<ds-item-admin-search-result-actions-element [item]="dso" [small]="false"></ds-item-admin-search-result-actions-element>
|
||||
|
@@ -1,11 +1,9 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { ItemAdminSearchResultListElementComponent } from './item-admin-search-result-list-element.component';
|
||||
@@ -51,51 +49,4 @@ describe('ItemAdminSearchResultListElementComponent', () => {
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when the item is not withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isWithdrawn = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the withdrawn badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||
expect(badge).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isWithdrawn = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the withdrawn badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||
expect(badge).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is not private', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isDiscoverable = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should not show the private badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||
expect(badge).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is private', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isDiscoverable = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the private badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||
expect(badge).not.toBeNull();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-item-alerts [item]="item"></ds-item-alerts>
|
||||
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||
<div class="d-flex flex-row">
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-item-alerts [item]="item"></ds-item-alerts>
|
||||
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { getTestScheduler, hot } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { SubmissionPatchRequest } from '../data/request.models';
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
} from './json-patch-operations.actions';
|
||||
import { RequestEntry } from '../data/request.reducer';
|
||||
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { _deepClone } from 'fast-json-patch/lib/helpers';
|
||||
|
||||
class TestService extends JsonPatchOperationsService<SubmitDataResponseDefinitionObject, SubmissionPatchRequest> {
|
||||
protected linkPath = '';
|
||||
@@ -196,6 +196,32 @@ describe('JsonPatchOperationsService test suite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPendingOperations', () => {
|
||||
|
||||
it('should return true when there are pending operations', () => {
|
||||
|
||||
const expected = hot('(x|)', { x: true });
|
||||
|
||||
const result = service.hasPendingOperations(testJsonPatchResourceType);
|
||||
expect(result).toBeObservable(expected);
|
||||
|
||||
});
|
||||
|
||||
it('should return false when there are not pending operations', () => {
|
||||
|
||||
const mockStateNoOp = _deepClone(mockState);
|
||||
mockStateNoOp['json/patch'][testJsonPatchResourceType].children = [];
|
||||
store.select.and.returnValue(observableOf(mockStateNoOp['json/patch'][testJsonPatchResourceType]));
|
||||
|
||||
const expected = hot('(x|)', { x: false });
|
||||
|
||||
const result = service.hasPendingOperations(testJsonPatchResourceType);
|
||||
expect(result).toBeObservable(expected);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('jsonPatchByResourceID', () => {
|
||||
|
||||
it('should call submitJsonPatchOperations method', () => {
|
||||
|
@@ -161,6 +161,18 @@ export abstract class JsonPatchOperationsService<ResponseDefinitionDomain, Patch
|
||||
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
|
||||
*
|
||||
|
@@ -1,6 +1,8 @@
|
||||
<div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'">
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<div class="position-absolute ml-1">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<a *ngIf="linkType != linkTypes.None"
|
||||
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]"
|
||||
|
@@ -1,6 +1,8 @@
|
||||
<div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'">
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<div class="position-absolute ml-1">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<a *ngIf="linkType != linkTypes.None"
|
||||
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]"
|
||||
|
@@ -1,6 +1,8 @@
|
||||
<div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'">
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<div class="position-absolute ml-1">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<a *ngIf="linkType != linkTypes.None"
|
||||
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]"
|
||||
|
@@ -1,6 +1,8 @@
|
||||
<div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'">
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<div class="position-absolute ml-1">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<a *ngIf="linkType != linkTypes.None"
|
||||
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]"
|
||||
|
@@ -1,6 +1,8 @@
|
||||
<div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'">
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<div class="position-absolute ml-1">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<a *ngIf="linkType != linkTypes.None"
|
||||
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]"
|
||||
|
@@ -1,6 +1,8 @@
|
||||
<div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'">
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<div class="position-absolute ml-1">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<a *ngIf="linkType != linkTypes.None"
|
||||
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]"
|
||||
|
@@ -7,7 +7,7 @@ import {
|
||||
DYNAMIC_FORM_CONTROL_TYPE_GROUP,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_INPUT,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP,
|
||||
DynamicFormArrayModel,
|
||||
DynamicFormArrayModel, DynamicFormControlEvent,
|
||||
DynamicFormControlModel,
|
||||
DynamicFormGroupModel,
|
||||
DynamicFormService, DynamicFormValidationService,
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { isObject, isString, mergeWith } from 'lodash';
|
||||
|
||||
import { hasValue, isEmpty, isNotEmpty, isNotNull, isNotUndefined, isNull } from '../../empty.util';
|
||||
import { DynamicQualdropModel } from './ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model';
|
||||
import {DynamicQualdropModel} from './ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model';
|
||||
import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model';
|
||||
import { RowParser } from './parsers/row-parser';
|
||||
@@ -26,6 +26,7 @@ import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-inpu
|
||||
import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model';
|
||||
import { isNgbDateStruct } from '../../date.util';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-ui/ds-dynamic-form-constants';
|
||||
import { CONCAT_GROUP_SUFFIX, DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model';
|
||||
|
||||
@Injectable()
|
||||
export class FormBuilderService extends DynamicFormService {
|
||||
@@ -54,6 +55,13 @@ export class FormBuilderService extends DynamicFormService {
|
||||
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)) {
|
||||
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);
|
||||
}
|
||||
|
||||
isConcatGroup(model: DynamicFormControlModel): boolean {
|
||||
return this.isCustomGroup(model) && (model.id.indexOf(CONCAT_GROUP_SUFFIX) !== -1);
|
||||
}
|
||||
|
||||
isRowGroup(model: DynamicFormControlModel): boolean {
|
||||
return model && ((model as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && (model as any).isRowGroup === true);
|
||||
}
|
||||
@@ -303,4 +315,76 @@ export class FormBuilderService extends DynamicFormService {
|
||||
return (tempModel.id !== tempModel.name) ? tempModel.name : tempModel.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the metadata list related to the event.
|
||||
* @param event
|
||||
*/
|
||||
getMetadataIdsFromEvent(event: DynamicFormControlEvent): string[] {
|
||||
|
||||
let model = event.model;
|
||||
while (model.parent) {
|
||||
model = model.parent as any;
|
||||
}
|
||||
|
||||
const iterateControlModels = (findGroupModel: DynamicFormControlModel[], controlModelIndex: number = 0): 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 = {
|
||||
FORM_INIT: type('dspace/form/FORM_INIT'),
|
||||
FORM_CHANGE: type('dspace/form/FORM_CHANGE'),
|
||||
FORM_ADD_TOUCHED: type('dspace/form/FORM_ADD_TOUCHED'),
|
||||
FORM_REMOVE: type('dspace/form/FORM_REMOVE'),
|
||||
FORM_STATUS_CHANGE: type('dspace/form/FORM_STATUS_CHANGE'),
|
||||
FORM_ADD_ERROR: type('dspace/form/FORM_ADD_ERROR'),
|
||||
@@ -52,7 +53,7 @@ export class FormChangeAction implements Action {
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new FormInitAction
|
||||
* Create a new FormChangeAction
|
||||
*
|
||||
* @param formId
|
||||
* 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 {
|
||||
type = FormActionTypes.FORM_REMOVE;
|
||||
payload: {
|
||||
@@ -147,6 +168,7 @@ export class FormClearErrorsAction implements Action {
|
||||
*/
|
||||
export type FormAction = FormInitAction
|
||||
| FormChangeAction
|
||||
| FormAddTouchedAction
|
||||
| FormRemoveAction
|
||||
| FormStatusChangeAction
|
||||
| FormAddError
|
||||
|
@@ -18,7 +18,7 @@
|
||||
<button type="button" class="btn btn-secondary"
|
||||
[disabled]="isItemReadOnly(context, 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>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -119,7 +119,8 @@ function init() {
|
||||
dc_identifier_issn: null
|
||||
},
|
||||
valid: false,
|
||||
errors: []
|
||||
errors: [],
|
||||
touched: {}
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -253,6 +253,7 @@ export class FormComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
onFocus(event: DynamicFormControlEvent): void {
|
||||
this.formService.setTouched(this.formId, this.formModel, event);
|
||||
this.focus.emit(event);
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
FormInitAction,
|
||||
FormRemoveAction,
|
||||
FormRemoveErrorAction,
|
||||
FormAddTouchedAction,
|
||||
FormStatusChangeAction
|
||||
} from './form.actions';
|
||||
|
||||
@@ -21,7 +22,8 @@ describe('formReducer', () => {
|
||||
description: null
|
||||
},
|
||||
valid: false,
|
||||
errors: []
|
||||
errors: [],
|
||||
touched: {}
|
||||
}
|
||||
};
|
||||
const formId = 'testForm';
|
||||
@@ -48,7 +50,8 @@ describe('formReducer', () => {
|
||||
description: null
|
||||
},
|
||||
valid: false,
|
||||
errors: []
|
||||
errors: [],
|
||||
touched: {}
|
||||
}
|
||||
};
|
||||
const formId = 'testForm';
|
||||
@@ -67,7 +70,8 @@ describe('formReducer', () => {
|
||||
description: null
|
||||
},
|
||||
valid: false,
|
||||
errors: []
|
||||
errors: [],
|
||||
touched: {}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,7 +92,8 @@ describe('formReducer', () => {
|
||||
description: null
|
||||
},
|
||||
valid: false,
|
||||
errors: []
|
||||
errors: [],
|
||||
touched: {}
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
@@ -100,7 +105,8 @@ describe('formReducer', () => {
|
||||
description: null
|
||||
},
|
||||
valid: false,
|
||||
errors: []
|
||||
errors: [],
|
||||
touched: {}
|
||||
}
|
||||
};
|
||||
const formId = 'testForm';
|
||||
@@ -127,7 +133,8 @@ describe('formReducer', () => {
|
||||
description: null
|
||||
},
|
||||
valid: false,
|
||||
errors: []
|
||||
errors: [],
|
||||
touched: {}
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
@@ -139,7 +146,8 @@ describe('formReducer', () => {
|
||||
description: null
|
||||
},
|
||||
valid: true,
|
||||
errors: []
|
||||
errors: [],
|
||||
touched: {}
|
||||
}
|
||||
};
|
||||
const formId = 'testForm';
|
||||
@@ -160,7 +168,8 @@ describe('formReducer', () => {
|
||||
description: null
|
||||
},
|
||||
valid: true,
|
||||
errors: []
|
||||
errors: [],
|
||||
touched: {}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -204,7 +213,8 @@ describe('formReducer', () => {
|
||||
fieldIndex: 0,
|
||||
message: 'error.validation.required'
|
||||
}
|
||||
]
|
||||
],
|
||||
touched: {}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -236,7 +246,8 @@ describe('formReducer', () => {
|
||||
description: null
|
||||
},
|
||||
valid: true,
|
||||
errors: []
|
||||
errors: [],
|
||||
touched: {}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -264,7 +275,8 @@ describe('formReducer', () => {
|
||||
fieldIndex: 0,
|
||||
message: 'error.validation.required'
|
||||
}
|
||||
]
|
||||
],
|
||||
touched: {}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -275,4 +287,84 @@ describe('formReducer', () => {
|
||||
|
||||
expect(newState.testForm.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should set new touched field to the form state', () => {
|
||||
const initState = {
|
||||
testForm: {
|
||||
data: {
|
||||
author: null,
|
||||
title: ['test'],
|
||||
date: null,
|
||||
description: null
|
||||
},
|
||||
valid: false,
|
||||
errors: [],
|
||||
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,
|
||||
FormChangeAction, FormClearErrorsAction,
|
||||
FormInitAction,
|
||||
FormRemoveAction,
|
||||
FormRemoveErrorAction,
|
||||
FormStatusChangeAction
|
||||
FormRemoveAction, FormRemoveErrorAction,
|
||||
FormStatusChangeAction, FormAddTouchedAction
|
||||
} from './form.actions';
|
||||
import { hasValue } from '../empty.util';
|
||||
import { isEqual, uniqWith } from 'lodash';
|
||||
@@ -17,10 +16,15 @@ export interface FormError {
|
||||
fieldIndex: number;
|
||||
}
|
||||
|
||||
export interface FormTouchedState {
|
||||
[key: string]: boolean
|
||||
}
|
||||
|
||||
export interface FormEntry {
|
||||
data: any;
|
||||
valid: boolean;
|
||||
errors: FormError[];
|
||||
touched: FormTouchedState;
|
||||
}
|
||||
|
||||
export interface FormState {
|
||||
@@ -40,6 +44,10 @@ export function formReducer(state = initialState, action: FormAction): FormState
|
||||
return changeDataForm(state, action as FormChangeAction);
|
||||
}
|
||||
|
||||
case FormActionTypes.FORM_ADD_TOUCHED: {
|
||||
return changeTouchedState(state, action as FormAddTouchedAction);
|
||||
}
|
||||
|
||||
case FormActionTypes.FORM_REMOVE: {
|
||||
return removeForm(state, action as FormRemoveAction);
|
||||
}
|
||||
@@ -127,7 +135,8 @@ function initForm(state: FormState, action: FormInitAction): FormState {
|
||||
const formState = {
|
||||
data: action.payload.formData,
|
||||
valid: action.payload.valid,
|
||||
errors: []
|
||||
touched: {},
|
||||
errors: [],
|
||||
};
|
||||
if (!hasValue(state[action.payload.formId])) {
|
||||
return Object.assign({}, state, {
|
||||
@@ -212,3 +221,24 @@ function removeForm(state: FormState, action: FormRemoveAction): FormState {
|
||||
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: {
|
||||
data: formData,
|
||||
valid: false,
|
||||
errors: []
|
||||
errors: [],
|
||||
touched: {}
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { map, distinctUntilChanged, filter } from 'rxjs/operators';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
|
||||
import { Observable } from 'rxjs';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
@@ -7,16 +7,16 @@ import { select, Store } from '@ngrx/store';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { formObjectFromIdSelector } from './selectors';
|
||||
import { FormBuilderService } from './builder/form-builder.service';
|
||||
import { DynamicFormControlModel } from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormControlEvent, DynamicFormControlModel } from '@ng-dynamic-forms/core';
|
||||
import { isEmpty, isNotUndefined } from '../empty.util';
|
||||
import { uniqueId } from 'lodash';
|
||||
import {
|
||||
FormChangeAction,
|
||||
FormInitAction,
|
||||
FormRemoveAction, FormRemoveErrorAction,
|
||||
FormRemoveAction, FormRemoveErrorAction, FormAddTouchedAction,
|
||||
FormStatusChangeAction
|
||||
} from './form.actions';
|
||||
import { FormEntry } from './form.reducer';
|
||||
import { FormEntry, FormTouchedState } from './form.reducer';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@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
|
||||
*/
|
||||
@@ -169,6 +181,11 @@ export class FormService {
|
||||
this.store.dispatch(new FormChangeAction(formId, this.formBuilderService.getValueFromModel(model)));
|
||||
}
|
||||
|
||||
public setTouched(formId: string, model: DynamicFormControlModel[], event: DynamicFormControlEvent) {
|
||||
const ids = this.formBuilderService.getMetadataIdsFromEvent(event);
|
||||
this.store.dispatch(new FormAddTouchedAction(formId, ids));
|
||||
}
|
||||
|
||||
public removeError(formId: string, eventModelId: string, fieldIndex: number) {
|
||||
this.store.dispatch(new FormRemoveErrorAction(formId, eventModelId, fieldIndex));
|
||||
}
|
||||
|
@@ -0,0 +1,8 @@
|
||||
<div>
|
||||
<div *ngIf="item && !item.isDiscoverable" class="private-warning">
|
||||
<ds-alert [type]="AlertTypeEnum.Warning" [content]="'item.alerts.private' | translate"></ds-alert>
|
||||
</div>
|
||||
<div *ngIf="item && item.isWithdrawn" class="withdrawn-warning">
|
||||
<ds-alert [type]="AlertTypeEnum.Warning" [content]="'item.alerts.withdrawn' | translate"></ds-alert>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,87 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ItemAlertsComponent } from './item-alerts.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('ItemAlertsComponent', () => {
|
||||
let component: ItemAlertsComponent;
|
||||
let fixture: ComponentFixture<ItemAlertsComponent>;
|
||||
let item: Item;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ItemAlertsComponent],
|
||||
imports: [TranslateModule.forRoot()],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemAlertsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when the item is discoverable', () => {
|
||||
beforeEach(() => {
|
||||
item = Object.assign(new Item(), {
|
||||
isDiscoverable: true
|
||||
});
|
||||
component.item = item;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not display the private alert', () => {
|
||||
const privateWarning = fixture.debugElement.query(By.css('.private-warning'));
|
||||
expect(privateWarning).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is not discoverable', () => {
|
||||
beforeEach(() => {
|
||||
item = Object.assign(new Item(), {
|
||||
isDiscoverable: false
|
||||
});
|
||||
component.item = item;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should display the private alert', () => {
|
||||
const privateWarning = fixture.debugElement.query(By.css('.private-warning'));
|
||||
expect(privateWarning).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
item = Object.assign(new Item(), {
|
||||
isWithdrawn: true
|
||||
});
|
||||
component.item = item;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should display the withdrawn alert', () => {
|
||||
const privateWarning = fixture.debugElement.query(By.css('.withdrawn-warning'));
|
||||
expect(privateWarning).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is not withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
item = Object.assign(new Item(), {
|
||||
isWithdrawn: false
|
||||
});
|
||||
component.item = item;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not display the withdrawn alert', () => {
|
||||
const privateWarning = fixture.debugElement.query(By.css('.withdrawn-warning'));
|
||||
expect(privateWarning).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
24
src/app/shared/item/item-alerts/item-alerts.component.ts
Normal file
24
src/app/shared/item/item-alerts/item-alerts.component.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { AlertType } from '../../alert/aletr-type';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-alerts',
|
||||
templateUrl: './item-alerts.component.html',
|
||||
styleUrls: ['./item-alerts.component.scss']
|
||||
})
|
||||
/**
|
||||
* Component displaying alerts for an item
|
||||
*/
|
||||
export class ItemAlertsComponent {
|
||||
/**
|
||||
* The Item to display alerts for
|
||||
*/
|
||||
@Input() item: Item;
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {AlertType}
|
||||
*/
|
||||
public AlertTypeEnum = AlertType;
|
||||
}
|
@@ -60,6 +60,21 @@ export const mockSectionsErrors = [
|
||||
}
|
||||
];
|
||||
|
||||
export const mockSectionsErrorsTwo = [
|
||||
{
|
||||
message: 'error.validation.required',
|
||||
paths: [
|
||||
'/sections/traditionalpageone/dc.title',
|
||||
]
|
||||
},
|
||||
{
|
||||
message: 'error.validation.license.notgranted',
|
||||
paths: [
|
||||
'/sections/license'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const mockUploadResponse1Errors = {
|
||||
errors: [
|
||||
{
|
||||
@@ -1033,6 +1048,7 @@ export const mockSubmissionState: SubmissionObjectState = Object.assign({}, {
|
||||
enabled: true,
|
||||
data: {},
|
||||
errors: [],
|
||||
formId: '2_traditionalpageone',
|
||||
isLoading: false,
|
||||
isValid: false
|
||||
} as any,
|
||||
|
@@ -1 +1,9 @@
|
||||
<div [ngClass]="{'d-none' : hideBadges}" #badges>
|
||||
<div *ngIf="privateBadge" class="private-badge">
|
||||
<span class="badge badge-danger">{{ "item.badge.private" | translate }}</span>
|
||||
</div>
|
||||
<div *ngIf="withdrawnBadge" class="withdrawn-badge">
|
||||
<span class="badge badge-warning">{{ "item.badge.withdrawn" | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template dsListableObject></ng-template>
|
@@ -9,6 +9,9 @@ import * as listableObjectDecorators from './listable-object.decorator';
|
||||
import { ItemListElementComponent } from '../../../object-list/item-list-element/item-types/item/item-list-element.component';
|
||||
import { ListableObjectDirective } from './listable-object.directive';
|
||||
import { spyOnExported } from '../../../testing/utils.test';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
const testType = 'TestType';
|
||||
@@ -27,7 +30,7 @@ describe('ListableObjectComponentLoaderComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ListableObjectComponentLoaderComponent, ItemListElementComponent, ListableObjectDirective],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
providers: [ComponentFactoryResolver]
|
||||
@@ -57,6 +60,64 @@ describe('ListableObjectComponentLoaderComponent', () => {
|
||||
})
|
||||
});
|
||||
|
||||
describe('when the object is an item and viewMode is a list', () => {
|
||||
beforeEach(() => {
|
||||
comp.object = Object.assign(new Item());
|
||||
comp.viewMode = ViewMode.ListElement;
|
||||
});
|
||||
|
||||
describe('when the item is not withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
(comp.object as any).isWithdrawn = false;
|
||||
comp.initBadges();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the withdrawn badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||
expect(badge).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
(comp.object as any).isWithdrawn = true;
|
||||
comp.initBadges();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the withdrawn badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||
expect(badge).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is not private', () => {
|
||||
beforeEach(() => {
|
||||
(comp.object as any).isDiscoverable = true;
|
||||
comp.initBadges();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should not show the private badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||
expect(badge).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is private', () => {
|
||||
beforeEach(() => {
|
||||
(comp.object as any).isDiscoverable = false;
|
||||
comp.initBadges();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the private badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||
expect(badge).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a reloadedObject is emitted', () => {
|
||||
|
||||
it('should re-instantiate the listable component ', fakeAsync(() => {
|
||||
@@ -72,4 +133,5 @@ describe('ListableObjectComponentLoaderComponent', () => {
|
||||
}))
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, ComponentFactoryResolver, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, ComponentFactoryResolver, Input, OnDestroy, OnInit, ViewChild, ElementRef } from '@angular/core';
|
||||
import { ListableObject } from '../listable-object.model';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
@@ -59,11 +59,32 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
|
||||
*/
|
||||
@Input() value: string;
|
||||
|
||||
/**
|
||||
* Whether or not informational badges (e.g. Private, Withdrawn) should be hidden
|
||||
*/
|
||||
@Input() hideBadges = false;
|
||||
|
||||
/**
|
||||
* Directive hook used to place the dynamic child component
|
||||
*/
|
||||
@ViewChild(ListableObjectDirective, {static: true}) listableObjectDirective: ListableObjectDirective;
|
||||
|
||||
/**
|
||||
* View on the badges template, to be passed on to the loaded component (which will place the badges in the desired
|
||||
* location, or on top if not specified)
|
||||
*/
|
||||
@ViewChild('badges', { static: true }) badges: ElementRef;
|
||||
|
||||
/**
|
||||
* Whether or not the "Private" badge should be displayed for this listable object
|
||||
*/
|
||||
privateBadge = false;
|
||||
|
||||
/**
|
||||
* Whether or not the "Withdrawn" badge should be displayed for this listable object
|
||||
*/
|
||||
withdrawnBadge = false;
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
@@ -87,12 +108,21 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
|
||||
}
|
||||
|
||||
private instantiateComponent(object) {
|
||||
|
||||
this.initBadges();
|
||||
|
||||
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(object));
|
||||
|
||||
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
const componentRef = viewContainerRef.createComponent(componentFactory);
|
||||
const componentRef = viewContainerRef.createComponent(
|
||||
componentFactory,
|
||||
0,
|
||||
undefined,
|
||||
[
|
||||
[this.badges.nativeElement],
|
||||
]);
|
||||
(componentRef.instance as any).object = object;
|
||||
(componentRef.instance as any).index = this.index;
|
||||
(componentRef.instance as any).linkType = this.linkType;
|
||||
@@ -111,6 +141,19 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize which badges should be visible in the listable component
|
||||
*/
|
||||
initBadges() {
|
||||
let objectAsAny = this.object as any;
|
||||
if (hasValue(objectAsAny.indexableObject)) {
|
||||
objectAsAny = objectAsAny.indexableObject;
|
||||
}
|
||||
const objectExistsAndValidViewMode = hasValue(objectAsAny) && this.viewMode !== ViewMode.StandalonePage;
|
||||
this.privateBadge = objectExistsAndValidViewMode && hasValue(objectAsAny.isDiscoverable) && !objectAsAny.isDiscoverable;
|
||||
this.withdrawnBadge = objectExistsAndValidViewMode && hasValue(objectAsAny.isWithdrawn) && objectAsAny.isWithdrawn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the component depending on the item's relationship type, view mode and context
|
||||
* @returns {GenericConstructor<Component>}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
<div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'">
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<div class="position-absolute ml-1">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]"
|
||||
class="card-img-top full-width">
|
||||
<div>
|
||||
|
@@ -217,6 +217,7 @@ import { AuthorizedCollectionSelectorComponent } from './dso-selector/dso-select
|
||||
import { DsoPageEditButtonComponent } from './dso-page/dso-page-edit-button/dso-page-edit-button.component';
|
||||
import { HoverClassDirective } from './hover-class.directive';
|
||||
import { ValidationSuggestionsComponent } from './input-suggestions/validation-suggestions/validation-suggestions.component';
|
||||
import { ItemAlertsComponent } from './item/item-alerts/item-alerts.component';
|
||||
import { ItemSearchResultGridElementComponent } from './object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component';
|
||||
import { ResourcePolicyEditComponent } from './resource-policies/edit/resource-policy-edit.component';
|
||||
import { ResourcePolicyCreateComponent } from './resource-policies/create/resource-policy-create.component';
|
||||
@@ -522,7 +523,8 @@ const ENTRY_COMPONENTS = [
|
||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||
MetadataFieldWrapperComponent,
|
||||
MetadataValuesComponent,
|
||||
DsoPageEditButtonComponent
|
||||
DsoPageEditButtonComponent,
|
||||
ItemAlertsComponent,
|
||||
];
|
||||
|
||||
const PROVIDERS = [
|
||||
|
@@ -2,6 +2,7 @@ export class SectionsServiceStub {
|
||||
|
||||
checkSectionErrors = jasmine.createSpy('checkSectionErrors');
|
||||
dispatchRemoveSectionErrors = jasmine.createSpy('dispatchRemoveSectionErrors');
|
||||
dispatchSetSectionFormId = jasmine.createSpy('dispatchSetSectionFormId');
|
||||
getSectionData = jasmine.createSpy('getSectionData');
|
||||
getSectionErrors = jasmine.createSpy('getSectionErrors');
|
||||
getSectionState = jasmine.createSpy('getSectionState');
|
||||
@@ -14,5 +15,5 @@ export class SectionsServiceStub {
|
||||
updateSectionData = jasmine.createSpy('updateSectionData');
|
||||
setSectionError = jasmine.createSpy('setSectionError');
|
||||
setSectionStatus = jasmine.createSpy('setSectionStatus');
|
||||
|
||||
computeSectionConfiguredMetadata = jasmine.createSpy('computeSectionConfiguredMetadata');
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ export class SubmissionServiceStub {
|
||||
getSubmissionStatus = jasmine.createSpy('getSubmissionStatus');
|
||||
getSubmissionSaveProcessingStatus = jasmine.createSpy('getSubmissionSaveProcessingStatus');
|
||||
getSubmissionDepositProcessingStatus = jasmine.createSpy('getSubmissionDepositProcessingStatus');
|
||||
hasUnsavedModification = jasmine.createSpy('hasUnsavedModification');
|
||||
isSectionHidden = jasmine.createSpy('isSectionHidden');
|
||||
isSubmissionLoading = jasmine.createSpy('isSubmissionLoading');
|
||||
notifyNewSection = jasmine.createSpy('notifyNewSection');
|
||||
|
@@ -12,7 +12,7 @@
|
||||
<button type="button"
|
||||
class="btn btn-info"
|
||||
id="save"
|
||||
[disabled]="(processingSaveStatus | async)"
|
||||
[disabled]="(processingSaveStatus | async) || !(hasUnsavedModification | async)"
|
||||
(click)="save($event)">
|
||||
<span>{{'submission.general.save' | translate}}</span>
|
||||
</button>
|
||||
|
@@ -169,7 +169,7 @@ describe('SubmissionFormFooterComponent Component', () => {
|
||||
comp.save(null);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(submissionServiceStub.dispatchSave).toHaveBeenCalledWith(submissionId);
|
||||
expect(submissionServiceStub.dispatchSave).toHaveBeenCalledWith(submissionId, true);
|
||||
});
|
||||
|
||||
it('should call dispatchSaveForLater on save for later', () => {
|
||||
@@ -224,6 +224,22 @@ describe('SubmissionFormFooterComponent Component', () => {
|
||||
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);
|
||||
|
||||
/**
|
||||
* A boolean representing if submission form has unsaved modifications
|
||||
*/
|
||||
public hasUnsavedModification: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
@@ -73,6 +78,7 @@ export class SubmissionFormFooterComponent implements OnChanges {
|
||||
this.processingSaveStatus = this.submissionService.getSubmissionSaveProcessingStatus(this.submissionId);
|
||||
this.processingDepositStatus = this.submissionService.getSubmissionDepositProcessingStatus(this.submissionId);
|
||||
this.showDepositAndDiscard = observableOf(this.submissionService.getSubmissionScope() === SubmissionScopeType.WorkspaceItem);
|
||||
this.hasUnsavedModification = this.submissionService.hasUnsavedModification();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +86,7 @@ export class SubmissionFormFooterComponent implements OnChanges {
|
||||
* Dispatch a submission save action
|
||||
*/
|
||||
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'),
|
||||
ENABLE_SECTION: type('dspace/submission/ENABLE_SECTION'),
|
||||
DISABLE_SECTION: type('dspace/submission/DISABLE_SECTION'),
|
||||
SET_SECTION_FORM_ID: type('dspace/submission/SET_SECTION_FORM_ID'),
|
||||
SECTION_STATUS_CHANGE: type('dspace/submission/SECTION_STATUS_CHANGE'),
|
||||
SECTION_LOADING_STATUS_CHANGE: type('dspace/submission/SECTION_LOADING_STATUS_CHANGE'),
|
||||
UPDATE_SECTION_DATA: type('dspace/submission/UPDATE_SECTION_DATA'),
|
||||
@@ -206,6 +207,7 @@ export class UpdateSectionDataAction implements Action {
|
||||
sectionId: string;
|
||||
data: WorkspaceitemSectionDataType;
|
||||
errors: SubmissionSectionError[];
|
||||
metadata: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -219,12 +221,15 @@ export class UpdateSectionDataAction implements Action {
|
||||
* the section's data
|
||||
* @param errors
|
||||
* the section's errors
|
||||
* @param metadata
|
||||
* the section's metadata
|
||||
*/
|
||||
constructor(submissionId: string,
|
||||
sectionId: string,
|
||||
data: WorkspaceitemSectionDataType,
|
||||
errors: SubmissionSectionError[]) {
|
||||
this.payload = { submissionId, sectionId, data, errors };
|
||||
errors: SubmissionSectionError[],
|
||||
metadata?: string[]) {
|
||||
this.payload = { submissionId, sectionId, data, errors, metadata };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
export class CompleteInitSubmissionFormAction implements Action {
|
||||
@@ -368,6 +396,7 @@ export class SaveSubmissionFormAction implements Action {
|
||||
type = SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM;
|
||||
payload: {
|
||||
submissionId: string;
|
||||
isManual?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -376,8 +405,8 @@ export class SaveSubmissionFormAction implements Action {
|
||||
* @param submissionId
|
||||
* the submission's ID
|
||||
*/
|
||||
constructor(submissionId: string) {
|
||||
this.payload = { submissionId };
|
||||
constructor(submissionId: string, isManual: boolean = false) {
|
||||
this.payload = { submissionId, isManual };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -777,6 +806,7 @@ export class DeleteUploadedFileAction implements Action {
|
||||
*/
|
||||
export type SubmissionObjectAction = DisableSectionAction
|
||||
| InitSectionAction
|
||||
| SetSectionFormId
|
||||
| EnableSectionAction
|
||||
| InitSubmissionFormAction
|
||||
| ResetSubmissionFormAction
|
||||
|
@@ -32,7 +32,7 @@ import {
|
||||
mockSubmissionId,
|
||||
mockSubmissionSelfUrl,
|
||||
mockSubmissionState,
|
||||
mockSubmissionRestResponse
|
||||
mockSubmissionRestResponse, mockSectionsErrorsTwo
|
||||
} from '../../shared/mocks/submission.mock';
|
||||
import { SubmissionSectionModel } from '../../core/config/models/config-submission-section.model';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||
@@ -51,15 +51,16 @@ import { Item } from '../../core/shared/item.model';
|
||||
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service';
|
||||
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
|
||||
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||
import {formStateSelector} from '../../shared/form/selectors';
|
||||
|
||||
describe('SubmissionObjectEffects test suite', () => {
|
||||
let submissionObjectEffects: SubmissionObjectEffects;
|
||||
let actions: Observable<any>;
|
||||
let store: StoreMock<AppState>;
|
||||
|
||||
const notificationsServiceStub = new NotificationsServiceStub();
|
||||
const submissionServiceStub = new SubmissionServiceStub();
|
||||
const submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub();
|
||||
let notificationsServiceStub;
|
||||
let submissionServiceStub;
|
||||
let submissionJsonPatchOperationsServiceStub;
|
||||
const collectionId: string = mockSubmissionCollectionId;
|
||||
const submissionId: string = mockSubmissionId;
|
||||
const submissionDefinitionResponse: any = mockSubmissionDefinitionResponse;
|
||||
@@ -68,6 +69,11 @@ describe('SubmissionObjectEffects test suite', () => {
|
||||
const submissionState: any = Object.assign({}, mockSubmissionState);
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
notificationsServiceStub = new NotificationsServiceStub();
|
||||
submissionServiceStub = new SubmissionServiceStub();
|
||||
submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot({}, storeModuleConfig),
|
||||
@@ -206,6 +212,52 @@ describe('SubmissionObjectEffects test suite', () => {
|
||||
expect(submissionObjectEffects.saveSubmission$).toBeObservable(expected);
|
||||
});
|
||||
|
||||
it('should enable notifications if is manual', () => {
|
||||
actions = hot('--a-', {
|
||||
a: {
|
||||
type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM,
|
||||
payload: {
|
||||
submissionId: submissionId,
|
||||
isManual: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
submissionJsonPatchOperationsServiceStub.jsonPatchByResourceType.and.returnValue(observableOf(mockSubmissionRestResponse));
|
||||
const expected = cold('--b-', {
|
||||
b: new SaveSubmissionFormSuccessAction(
|
||||
submissionId,
|
||||
mockSubmissionRestResponse as any,
|
||||
true
|
||||
)
|
||||
});
|
||||
|
||||
expect(submissionObjectEffects.saveSubmission$).toBeObservable(expected);
|
||||
});
|
||||
|
||||
it('should disable notifications if is not manual', () => {
|
||||
actions = hot('--a-', {
|
||||
a: {
|
||||
type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM,
|
||||
payload: {
|
||||
submissionId: submissionId,
|
||||
isManual: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
submissionJsonPatchOperationsServiceStub.jsonPatchByResourceType.and.returnValue(observableOf(mockSubmissionRestResponse));
|
||||
const expected = cold('--b-', {
|
||||
b: new SaveSubmissionFormSuccessAction(
|
||||
submissionId,
|
||||
mockSubmissionRestResponse as any,
|
||||
false
|
||||
)
|
||||
});
|
||||
|
||||
expect(submissionObjectEffects.saveSubmission$).toBeObservable(expected);
|
||||
});
|
||||
|
||||
it('should return a SAVE_SUBMISSION_FORM_ERROR action on error', () => {
|
||||
actions = hot('--a-', {
|
||||
a: {
|
||||
@@ -292,7 +344,8 @@ describe('SubmissionObjectEffects test suite', () => {
|
||||
type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS,
|
||||
payload: {
|
||||
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', () => {
|
||||
store.nextState({
|
||||
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$', () => {
|
||||
it('should return a SAVE_SUBMISSION_SECTION_FORM_SUCCESS action on success', () => {
|
||||
actions = hot('--a-', {
|
||||
|
@@ -52,12 +52,14 @@ import {
|
||||
UpdateSectionDataAction,
|
||||
UpdateSectionDataSuccessAction
|
||||
} from './submission-objects.actions';
|
||||
import { SubmissionObjectEntry, SubmissionSectionObject } from './submission-objects.reducer';
|
||||
import {SubmissionObjectEntry, SubmissionSectionError, SubmissionSectionObject} from './submission-objects.reducer';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
import parseSectionErrorPaths, {SectionErrorPath} from '../utils/parseSectionErrorPaths';
|
||||
import { FormState } from '../../shared/form/form.reducer';
|
||||
|
||||
@Injectable()
|
||||
export class SubmissionObjectEffects {
|
||||
@@ -132,7 +134,7 @@ export class SubmissionObjectEffects {
|
||||
this.submissionService.getSubmissionObjectLinkName(),
|
||||
action.payload.submissionId,
|
||||
'sections').pipe(
|
||||
map((response: SubmissionObject[]) => new SaveSubmissionFormSuccessAction(action.payload.submissionId, response)),
|
||||
map((response: SubmissionObject[]) => new SaveSubmissionFormSuccessAction(action.payload.submissionId, response, action.payload.isManual)),
|
||||
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
|
||||
}));
|
||||
|
||||
@@ -154,10 +156,24 @@ export class SubmissionObjectEffects {
|
||||
* Call parseSaveResponse and dispatch actions
|
||||
*/
|
||||
@Effect() saveSubmissionSuccess$ = this.actions$.pipe(
|
||||
ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS, SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS),
|
||||
ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS),
|
||||
withLatestFrom(this.store$),
|
||||
map(([action, currentState]: [SaveSubmissionFormSuccessAction | SaveSubmissionSectionFormSuccessAction, any]) => {
|
||||
return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], action.payload.submissionObject, action.payload.submissionId, action.payload.notify);
|
||||
map(([action, currentState]: [SaveSubmissionFormSuccessAction, any]) => {
|
||||
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)));
|
||||
|
||||
@@ -200,7 +216,8 @@ export class SubmissionObjectEffects {
|
||||
return new DepositSubmissionAction(action.payload.submissionId);
|
||||
} else {
|
||||
this.notificationsService.warning(null, this.translate.get('submission.sections.general.sections_not_valid'));
|
||||
return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], response, action.payload.submissionId);
|
||||
return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId],
|
||||
response, action.payload.submissionId, currentState.forms);
|
||||
}
|
||||
}),
|
||||
catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId))));
|
||||
@@ -280,7 +297,7 @@ export class SubmissionObjectEffects {
|
||||
return item$.pipe(
|
||||
map((item: Item) => item.metadata),
|
||||
filter((metadata) => !isEqual(action.payload.data, metadata)),
|
||||
map((metadata: any) => new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, metadata, action.payload.errors))
|
||||
map((metadata: any) => new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, metadata, action.payload.errors, action.payload.metadata))
|
||||
);
|
||||
} else {
|
||||
return observableOf(new UpdateSectionDataSuccessAction());
|
||||
@@ -353,6 +370,7 @@ export class SubmissionObjectEffects {
|
||||
currentState: SubmissionObjectEntry,
|
||||
response: SubmissionObject[],
|
||||
submissionId: string,
|
||||
forms,
|
||||
notify: boolean = true): SubmissionObjectAction[] {
|
||||
|
||||
const mappedActions = [];
|
||||
@@ -392,10 +410,54 @@ export class SubmissionObjectEffects {
|
||||
if (notify && !currentState.sections[sectionId].enabled) {
|
||||
this.submissionService.notifyNewSection(submissionId, sectionId, currentState.sections[sectionId].sectionType);
|
||||
}
|
||||
mappedActions.push(new UpdateSectionDataAction(submissionId, sectionId, sectionData, sectionErrors));
|
||||
|
||||
const sectionForm = getForm(forms, currentState, sectionId);
|
||||
const filteredErrors = filterErrors(sectionForm, sectionErrors, currentState.sections[sectionId].sectionType, notify);
|
||||
mappedActions.push(new UpdateSectionDataAction(submissionId, sectionId, sectionData, filteredErrors));
|
||||
}
|
||||
});
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
||||
it('should update submission section metadata properly', () => {
|
||||
const data = {
|
||||
} as any;
|
||||
const metadata = ['dc.title', 'dc.contributor.author'];
|
||||
|
||||
const action = new UpdateSectionDataAction(submissionId, 'traditionalpageone', data, [], metadata);
|
||||
const newState = submissionObjectReducer(initState, action);
|
||||
|
||||
expect(newState[826].sections.traditionalpageone.metadata).toEqual(metadata);
|
||||
});
|
||||
|
||||
it('should add submission section errors properly', () => {
|
||||
const errors = [
|
||||
{
|
||||
|
@@ -30,6 +30,7 @@ import {
|
||||
SaveSubmissionSectionFormSuccessAction,
|
||||
SectionStatusChangeAction,
|
||||
SetActiveSectionAction,
|
||||
SetSectionFormId,
|
||||
SubmissionObjectAction,
|
||||
SubmissionObjectActionTypes,
|
||||
UpdateSectionDataAction
|
||||
@@ -85,6 +86,11 @@ export interface SubmissionSectionObject {
|
||||
*/
|
||||
enabled: boolean;
|
||||
|
||||
/**
|
||||
* The list of the metadata ids of the section.
|
||||
*/
|
||||
metadata: string[];
|
||||
|
||||
/**
|
||||
* The section data object
|
||||
*/
|
||||
@@ -104,6 +110,11 @@ export interface SubmissionSectionObject {
|
||||
* A boolean representing if this section is valid
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
case SubmissionObjectActionTypes.SET_SECTION_FORM_ID: {
|
||||
return setSectionFormId(state, action as SetSectionFormId);
|
||||
}
|
||||
|
||||
case SubmissionObjectActionTypes.ENABLE_SECTION: {
|
||||
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.
|
||||
*
|
||||
@@ -660,7 +702,8 @@ function updateSectionData(state: SubmissionObjectState, action: UpdateSectionDa
|
||||
[ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], {
|
||||
enabled: true,
|
||||
data: action.payload.data,
|
||||
errors: action.payload.errors
|
||||
errors: action.payload.errors,
|
||||
metadata: reduceSectionMetadata(action.payload.metadata, state[ action.payload.submissionId ].sections [ action.payload.sectionId ].metadata)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -670,6 +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.
|
||||
*
|
||||
|
@@ -287,6 +287,7 @@ describe('SubmissionSectionformComponent test suite', () => {
|
||||
'dc.title': [new FormFieldMetadataValueObject('test')]
|
||||
};
|
||||
compAsAny.formData = {};
|
||||
compAsAny.sectionMetadata = ['dc.title'];
|
||||
|
||||
expect(comp.hasMetadataEnrichment(newSectionData)).toBeTruthy();
|
||||
});
|
||||
@@ -296,7 +297,16 @@ describe('SubmissionSectionformComponent test suite', () => {
|
||||
'dc.title': [new FormFieldMetadataValueObject('test')]
|
||||
};
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -310,6 +320,7 @@ describe('SubmissionSectionformComponent test suite', () => {
|
||||
comp.sectionData.data = {};
|
||||
comp.sectionData.errors = [];
|
||||
compAsAny.formData = {};
|
||||
compAsAny.sectionMetadata = ['dc.title'];
|
||||
|
||||
comp.updateForm(sectionData, sectionError);
|
||||
|
||||
@@ -329,10 +340,11 @@ describe('SubmissionSectionformComponent test suite', () => {
|
||||
comp.sectionData.data = {};
|
||||
comp.sectionData.errors = [];
|
||||
compAsAny.formData = sectionData;
|
||||
compAsAny.sectionMetadata = ['dc.title'];
|
||||
|
||||
comp.updateForm(sectionData, parsedSectionErrors);
|
||||
|
||||
expect(comp.initForm).toHaveBeenCalled();
|
||||
expect(comp.initForm).not.toHaveBeenCalled();
|
||||
expect(comp.checksForErrors).toHaveBeenCalled();
|
||||
expect(comp.sectionData.data).toEqual(sectionData);
|
||||
});
|
||||
|
@@ -13,7 +13,7 @@ import {
|
||||
tap
|
||||
} from 'rxjs/operators';
|
||||
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 { FormComponent } from '../../../shared/form/form.component';
|
||||
@@ -101,6 +101,12 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
|
||||
*/
|
||||
protected formData: any = Object.create({});
|
||||
|
||||
/**
|
||||
* Store the
|
||||
* @protected
|
||||
*/
|
||||
protected sectionMetadata: string[];
|
||||
|
||||
/**
|
||||
* The [JsonPatchOperationPathCombiner] object
|
||||
* @type {JsonPatchOperationPathCombiner}
|
||||
@@ -168,6 +174,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
|
||||
onSectionInit() {
|
||||
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', 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(
|
||||
map((configData: RemoteData<ConfigObject>) => configData.payload),
|
||||
tap((config: SubmissionFormsModel) => this.formConfig = config),
|
||||
@@ -230,16 +237,25 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
|
||||
* the section data retrieved from the server
|
||||
*/
|
||||
hasMetadataEnrichment(sectionData: WorkspaceitemSectionFormObject): boolean {
|
||||
|
||||
const sectionDataToCheck = {};
|
||||
Object.keys(sectionData).forEach((key) => {
|
||||
if (this.sectionMetadata && this.sectionMetadata.includes(key)) {
|
||||
sectionDataToCheck[key] = sectionData[key];
|
||||
}
|
||||
})
|
||||
|
||||
const diffResult = [];
|
||||
|
||||
// compare current form data state with section data retrieved from store
|
||||
const diffObj = difference(sectionData, this.formData);
|
||||
const diffObj = difference(sectionDataToCheck, this.formData);
|
||||
|
||||
// iterate over differences to check whether they are actually different
|
||||
Object.keys(diffObj)
|
||||
.forEach((key) => {
|
||||
diffObj[key].forEach((value) => {
|
||||
if (value.hasOwnProperty('value')) {
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
@@ -262,6 +278,9 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
|
||||
sectionData,
|
||||
this.submissionService.getSubmissionScope()
|
||||
);
|
||||
const sectionMetadata = this.sectionService.computeSectionConfiguredMetadata(this.formConfig);
|
||||
this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, sectionData, [], sectionMetadata);
|
||||
|
||||
} catch (e) {
|
||||
const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString();
|
||||
const sectionError: SubmissionSectionError = {
|
||||
@@ -283,8 +302,9 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
|
||||
*/
|
||||
updateForm(sectionData: WorkspaceitemSectionFormObject, errors: SubmissionSectionError[]): void {
|
||||
|
||||
if (hasValue(sectionData) && !isEqual(sectionData, this.sectionData.data)) {
|
||||
if (isNotEmpty(sectionData) && !isEqual(sectionData, this.sectionData.data)) {
|
||||
this.sectionData.data = sectionData;
|
||||
if (this.hasMetadataEnrichment(sectionData)) {
|
||||
this.isUpdating = true;
|
||||
this.formModel = null;
|
||||
this.cdr.detectChanges();
|
||||
@@ -295,6 +315,9 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
|
||||
} else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) {
|
||||
this.checksForErrors(errors);
|
||||
}
|
||||
} else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) {
|
||||
this.checksForErrors(errors);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -338,6 +361,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
|
||||
distinctUntilChanged())
|
||||
.subscribe((sectionState: SubmissionSectionObject) => {
|
||||
this.fieldsOnTheirWayToBeRemoved = new Map();
|
||||
this.sectionMetadata = sectionState.metadata;
|
||||
this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errors);
|
||||
})
|
||||
)
|
||||
|
@@ -380,4 +380,25 @@ describe('SectionsService test suite', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new UpdateSectionDataAction(submissionId, sectionId, data, []));
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeSectionConfiguredMetadata', () => {
|
||||
it('should return the configured metadata of the section from the form configuration', () => {
|
||||
|
||||
const formConfig = {
|
||||
rows: [{
|
||||
fields: [{
|
||||
selectableMetadata: [{
|
||||
metadata: 'dc.contributor.author'
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
const expectedConfiguredMetadata = [ 'dc.contributor.author' ];
|
||||
|
||||
const configuredMetadata = service.computeSectionConfiguredMetadata(formConfig as any);
|
||||
|
||||
expect(configuredMetadata).toEqual(expectedConfiguredMetadata);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -15,6 +15,7 @@ import {
|
||||
InertSectionErrorsAction,
|
||||
RemoveSectionErrorsAction,
|
||||
SectionStatusChangeAction,
|
||||
SetSectionFormId,
|
||||
UpdateSectionDataAction
|
||||
} from '../objects/submission-objects.actions';
|
||||
import {
|
||||
@@ -36,6 +37,8 @@ import { SubmissionService } from '../submission.service';
|
||||
import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model';
|
||||
import { SectionsType } from './sections-type';
|
||||
import { normalizeSectionData } from '../../core/submission/submission-response-parsing.service';
|
||||
import { SubmissionFormsModel } from '../../core/config/models/config-submission-forms.model';
|
||||
import { parseReviver } from '@ng-dynamic-forms/core';
|
||||
|
||||
/**
|
||||
* A service that provides methods used in submission process.
|
||||
@@ -133,6 +136,18 @@ export class SectionsService {
|
||||
this.store.dispatch(new RemoveSectionErrorsAction(submissionId, sectionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a new [SetSectionFormId]
|
||||
* The submission id
|
||||
* @param sectionId
|
||||
* The section id
|
||||
* @param formId
|
||||
* The form id
|
||||
*/
|
||||
public dispatchSetSectionFormId(submissionId, sectionId, formId) {
|
||||
this.store.dispatch(new SetSectionFormId(submissionId, sectionId, formId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the data object for the specified section
|
||||
*
|
||||
@@ -335,8 +350,10 @@ export class SectionsService {
|
||||
* The section data
|
||||
* @param errors
|
||||
* The list of section errors
|
||||
* @param metadata
|
||||
* The section metadata
|
||||
*/
|
||||
public updateSectionData(submissionId: string, sectionId: string, data: WorkspaceitemSectionDataType, errors: SubmissionSectionError[] = []) {
|
||||
public updateSectionData(submissionId: string, sectionId: string, data: WorkspaceitemSectionDataType, errors: SubmissionSectionError[] = [], metadata?: string[]) {
|
||||
if (isNotEmpty(data)) {
|
||||
const isAvailable$ = this.isSectionAvailable(submissionId, sectionId);
|
||||
const isEnabled$ = this.isSectionEnabled(submissionId, sectionId);
|
||||
@@ -345,7 +362,7 @@ export class SectionsService {
|
||||
take(1),
|
||||
filter(([available, enabled]: [boolean, boolean]) => available))
|
||||
.subscribe(([available, enabled]: [boolean, boolean]) => {
|
||||
this.store.dispatch(new UpdateSectionDataAction(submissionId, sectionId, data, errors));
|
||||
this.store.dispatch(new UpdateSectionDataAction(submissionId, sectionId, data, errors, metadata));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -377,4 +394,30 @@ export class SectionsService {
|
||||
public setSectionStatus(submissionId: string, sectionId: string, status: boolean) {
|
||||
this.store.dispatch(new SectionStatusChangeAction(submissionId, sectionId, status));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the list of selectable metadata for the section configuration.
|
||||
* @param formConfig
|
||||
*/
|
||||
public computeSectionConfiguredMetadata(formConfig: string | SubmissionFormsModel): string[] {
|
||||
const metadata = [];
|
||||
const rawData = typeof formConfig === 'string' ? JSON.parse(formConfig, parseReviver) : formConfig;
|
||||
if (rawData.rows && !isEmpty(rawData.rows)) {
|
||||
rawData.rows.forEach((currentRow) => {
|
||||
if (currentRow.fields && !isEmpty(currentRow.fields)) {
|
||||
currentRow.fields.forEach((field) => {
|
||||
if (field.selectableMetadata && !isEmpty(field.selectableMetadata)) {
|
||||
field.selectableMetadata.forEach((selectableMetadata) => {
|
||||
if (!metadata.includes(selectableMetadata.metadata)) {
|
||||
metadata.push(selectableMetadata.metadata);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -46,6 +46,8 @@ import { SearchService } from '../core/shared/search/search.service';
|
||||
import { Item } from '../core/shared/item.model';
|
||||
import { storeModuleConfig } from '../app.reducer';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { SubmissionJsonPatchOperationsService } from '../core/submission/submission-json-patch-operations.service';
|
||||
import { SubmissionJsonPatchOperationsServiceStub } from '../shared/testing/submission-json-patch-operations-service.stub';
|
||||
|
||||
describe('SubmissionService test suite', () => {
|
||||
const collectionId = '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f';
|
||||
@@ -345,6 +347,7 @@ describe('SubmissionService test suite', () => {
|
||||
const router = new RouterMock();
|
||||
const selfUrl = 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826';
|
||||
const submissionDefinition: any = mockSubmissionDefinition;
|
||||
const submissionJsonPatchOperationsService = new SubmissionJsonPatchOperationsServiceStub();
|
||||
|
||||
let scheduler: TestScheduler;
|
||||
let service: SubmissionService;
|
||||
@@ -371,6 +374,7 @@ describe('SubmissionService test suite', () => {
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
{ provide: SearchService, useValue: searchService },
|
||||
{ provide: RequestService, useValue: requestServce },
|
||||
{ provide: SubmissionJsonPatchOperationsService, useValue: submissionJsonPatchOperationsService },
|
||||
NotificationsService,
|
||||
RouteService,
|
||||
SubmissionService,
|
||||
@@ -487,11 +491,18 @@ describe('SubmissionService test suite', () => {
|
||||
|
||||
describe('dispatchSave', () => {
|
||||
it('should dispatch a new SaveSubmissionFormAction', () => {
|
||||
service.dispatchSave(submissionId,);
|
||||
service.dispatchSave(submissionId);
|
||||
const expected = new SaveSubmissionFormAction(submissionId);
|
||||
|
||||
expect((service as any).store.dispatch).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
@@ -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', () => {
|
||||
it('should return true/false when section is hidden/visible', () => {
|
||||
let section: any = {
|
||||
@@ -915,8 +940,15 @@ describe('SubmissionService test suite', () => {
|
||||
});
|
||||
|
||||
describe('startAutoSave', () => {
|
||||
|
||||
let environmentAutoSaveTimerOriginalValue;
|
||||
|
||||
beforeEach(() => {
|
||||
environmentAutoSaveTimerOriginalValue = environment.submission.autosave.timer;
|
||||
});
|
||||
|
||||
it('should start Auto Save', fakeAsync(() => {
|
||||
const duration = environment.submission.autosave.timer * (1000 * 60);
|
||||
const duration = environment.submission.autosave.timer;
|
||||
|
||||
service.startAutoSave('826');
|
||||
const sub = (service as any).timer$.subscribe();
|
||||
@@ -930,6 +962,19 @@ describe('SubmissionService test suite', () => {
|
||||
sub.unsubscribe();
|
||||
(service as any).autoSaveSub.unsubscribe();
|
||||
}));
|
||||
|
||||
it('should not start Auto Save if timer is 0', fakeAsync(() => {
|
||||
environment.submission.autosave.timer = 0;
|
||||
|
||||
service.startAutoSave('826');
|
||||
|
||||
expect((service as any).autoSaveSub).toBeUndefined();
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
environment.submission.autosave.timer = environmentAutoSaveTimerOriginalValue;
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
describe('stopAutoSave', () => {
|
||||
|
@@ -45,6 +45,7 @@ import { RequestService } from '../core/data/request.service';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { Item } from '../core/shared/item.model';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { SubmissionJsonPatchOperationsService } from '../core/submission/submission-json-patch-operations.service';
|
||||
|
||||
/**
|
||||
* A service that provides methods used in submission process.
|
||||
@@ -82,7 +83,8 @@ export class SubmissionService {
|
||||
protected store: Store<SubmissionState>,
|
||||
protected translate: TranslateService,
|
||||
protected searchService: SearchService,
|
||||
protected requestService: RequestService) {
|
||||
protected requestService: RequestService,
|
||||
protected jsonPatchOperationService: SubmissionJsonPatchOperationsService) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,12 +211,14 @@ export class SubmissionService {
|
||||
*
|
||||
* @param submissionId
|
||||
* The submission id
|
||||
* @param manual
|
||||
* whether is a manual save, default false
|
||||
*/
|
||||
dispatchSave(submissionId) {
|
||||
dispatchSave(submissionId, manual?: boolean) {
|
||||
this.getSubmissionSaveProcessingStatus(submissionId).pipe(
|
||||
find((isPending: boolean) => !isPending)
|
||||
).subscribe(() => {
|
||||
this.store.dispatch(new SaveSubmissionFormAction(submissionId));
|
||||
this.store.dispatch(new SaveSubmissionFormAction(submissionId, manual));
|
||||
})
|
||||
}
|
||||
|
||||
@@ -427,6 +431,16 @@ export class SubmissionService {
|
||||
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
|
||||
*
|
||||
@@ -562,9 +576,12 @@ export class SubmissionService {
|
||||
*/
|
||||
startAutoSave(submissionId) {
|
||||
this.stopAutoSave();
|
||||
if (environment.submission.autosave.timer === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// AUTOSAVE submission
|
||||
// Retrieve interval from config and convert to milliseconds
|
||||
const duration = environment.submission.autosave.timer * (1000 * 60);
|
||||
const duration = environment.submission.autosave.timer;
|
||||
// Dispatch save action after given duration
|
||||
this.timer$ = observableTimer(duration, duration);
|
||||
this.autoSaveSub = this.timer$
|
||||
|
@@ -462,14 +462,10 @@
|
||||
|
||||
"admin.search.item.move": "Move",
|
||||
|
||||
"admin.search.item.private": "Private",
|
||||
|
||||
"admin.search.item.reinstate": "Reinstate",
|
||||
|
||||
"admin.search.item.withdraw": "Withdraw",
|
||||
|
||||
"admin.search.item.withdrawn": "Withdrawn",
|
||||
|
||||
"admin.search.title": "Administrative Search",
|
||||
|
||||
|
||||
@@ -1345,12 +1341,24 @@
|
||||
|
||||
|
||||
|
||||
"item.alerts.private": "This item is private",
|
||||
|
||||
"item.alerts.withdrawn": "This item has been withdrawn",
|
||||
|
||||
|
||||
|
||||
"item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.",
|
||||
|
||||
"item.edit.authorizations.title": "Edit item's Policies",
|
||||
|
||||
|
||||
|
||||
"item.badge.private": "Private",
|
||||
|
||||
"item.badge.withdrawn": "Withdrawn",
|
||||
|
||||
|
||||
|
||||
"item.bitstreams.upload.bundle": "Bundle",
|
||||
|
||||
"item.bitstreams.upload.bundle.placeholder": "Select a bundle",
|
||||
|
@@ -65,9 +65,9 @@ export const environment: GlobalConfig = {
|
||||
submission: {
|
||||
autosave: {
|
||||
// NOTE: which metadata trigger an autosave
|
||||
metadata: ['dc.title', 'dc.identifier.doi', 'dc.identifier.pmid', 'dc.identifier.arxiv'],
|
||||
metadata: [],
|
||||
// NOTE: every how many minutes submission is saved automatically
|
||||
timer: 5
|
||||
timer: 0
|
||||
},
|
||||
icons: {
|
||||
metadata: [
|
||||
|
Reference in New Issue
Block a user