diff --git a/config/environment.default.js b/config/environment.default.js
index 24386d6cf7..58193d31bc 100644
--- a/config/environment.default.js
+++ b/config/environment.default.js
@@ -185,6 +185,11 @@ module.exports = {
undoTimeout: 10000 // 10 seconds
}
},
+ collection: {
+ edit: {
+ undoTimeout: 10000 // 10 seconds
+ }
+ },
theme: {
name: 'default',
}
diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5
index 50a53eb7e6..39d30859bc 100644
--- a/resources/i18n/en.json5
+++ b/resources/i18n/en.json5
@@ -338,8 +338,40 @@
"collection.edit.tabs.roles.title": "Collection Edit - Roles",
+ "collection.edit.tabs.source.external": "This collection harvests its content from an external source",
+
+ "collection.edit.tabs.source.form.errors.oaiSource.required": "You must provide a set id of the target collection.",
+
+ "collection.edit.tabs.source.form.harvestType": "Content being harvested",
+
+ "collection.edit.tabs.source.form.head": "Configure an external source",
+
+ "collection.edit.tabs.source.form.metadataConfigId": "Metadata Format",
+
+ "collection.edit.tabs.source.form.oaiSetId": "OAI specific set id",
+
+ "collection.edit.tabs.source.form.oaiSource": "OAI Provider",
+
+ "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_BITSTREAMS": "Harvest metadata and bitstreams (requires ORE support)",
+
+ "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_REF": "Harvest metadata and references to bitstreams (requires ORE support)",
+
+ "collection.edit.tabs.source.form.options.harvestType.METADATA_ONLY": "Harvest metadata only",
+
"collection.edit.tabs.source.head": "Content Source",
+ "collection.edit.tabs.source.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button",
+
+ "collection.edit.tabs.source.notifications.discarded.title": "Changed discarded",
+
+ "collection.edit.tabs.source.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.",
+
+ "collection.edit.tabs.source.notifications.invalid.title": "Metadata invalid",
+
+ "collection.edit.tabs.source.notifications.saved.content": "Your changes to this collection's content source were saved.",
+
+ "collection.edit.tabs.source.notifications.saved.title": "Content Source saved",
+
"collection.edit.tabs.source.title": "Collection Edit - Content Source",
@@ -382,6 +414,12 @@
+ "collection.source.update.notifications.error.content": "The provided settings have been tested and didn't work.",
+
+ "collection.source.update.notifications.error.title": "Server Error",
+
+
+
"communityList.tabTitle": "DSpace - Community List",
"communityList.title": "List of Communities",
@@ -997,6 +1035,8 @@
"loading.collections": "Loading collections...",
+ "loading.content-source": "Loading content source...",
+
"loading.community": "Loading community...",
"loading.default": "Loading...",
diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html
index e69de29bb2..4192922c7e 100644
--- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html
+++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
{{ 'collection.edit.tabs.source.head' | translate }}
+
+
+
+
+
+
{{ 'collection.edit.tabs.source.form.head' | translate }}
+
+
+
+
+
+
+
+
+
diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts
new file mode 100644
index 0000000000..11ec9b1f6a
--- /dev/null
+++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts
@@ -0,0 +1,222 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ActivatedRoute, Router } from '@angular/router';
+import { of as observableOf } from 'rxjs/internal/observable/of';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { CollectionSourceComponent } from './collection-source.component';
+import { ContentSource, ContentSourceHarvestType } from '../../../core/shared/content-source.model';
+import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
+import { INotification, Notification } from '../../../shared/notifications/models/notification.model';
+import { NotificationType } from '../../../shared/notifications/models/notification-type';
+import { FieldUpdate } from '../../../core/data/object-updates/object-updates.reducer';
+import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { DynamicFormControlModel, DynamicFormService } from '@ng-dynamic-forms/core';
+import { hasValue } from '../../../shared/empty.util';
+import { FormControl, FormGroup } from '@angular/forms';
+import { RouterStub } from '../../../shared/testing/router-stub';
+import { GLOBAL_CONFIG } from '../../../../config';
+import { By } from '@angular/platform-browser';
+import { Collection } from '../../../core/shared/collection.model';
+import { RemoteData } from '../../../core/data/remote-data';
+import { CollectionDataService } from '../../../core/data/collection-data.service';
+import { RequestService } from '../../../core/data/request.service';
+
+const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
+const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
+const successNotification: INotification = new Notification('id', NotificationType.Success, 'success');
+
+const uuid = '29481ed7-ae6b-409a-8c51-34dd347a0ce4';
+let date: Date;
+let contentSource: ContentSource;
+let fieldUpdate: FieldUpdate;
+let objectUpdatesService: ObjectUpdatesService;
+let notificationsService: NotificationsService;
+let location: Location;
+let formService: DynamicFormService;
+let router: any;
+let collection: Collection;
+let collectionService: CollectionDataService;
+let requestService: RequestService;
+
+describe('CollectionSourceComponent', () => {
+ let comp: CollectionSourceComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ date = new Date();
+ contentSource = Object.assign(new ContentSource(), {
+ uuid: uuid,
+ metadataConfigs: [
+ {
+ id: 'dc',
+ label: 'Simple Dublin Core',
+ nameSpace: 'http://www.openarchives.org/OAI/2.0/oai_dc/'
+ },
+ {
+ id: 'qdc',
+ label: 'Qualified Dublin Core',
+ nameSpace: 'http://purl.org/dc/terms/'
+ },
+ {
+ id: 'dim',
+ label: 'DSpace Intermediate Metadata',
+ nameSpace: 'http://www.dspace.org/xmlns/dspace/dim'
+ }
+ ]
+ });
+ fieldUpdate = {
+ field: contentSource,
+ changeType: undefined
+ };
+ objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
+ {
+ getFieldUpdates: observableOf({
+ [contentSource.uuid]: fieldUpdate
+ }),
+ saveAddFieldUpdate: {},
+ discardFieldUpdates: {},
+ reinstateFieldUpdates: observableOf(true),
+ initialize: {},
+ getUpdatedFields: observableOf([contentSource]),
+ getLastModified: observableOf(date),
+ hasUpdates: observableOf(true),
+ isReinstatable: observableOf(false),
+ isValidPage: observableOf(true)
+ }
+ );
+ notificationsService = jasmine.createSpyObj('notificationsService',
+ {
+ info: infoNotification,
+ warning: warningNotification,
+ success: successNotification
+ }
+ );
+ location = jasmine.createSpyObj('location', ['back']);
+ formService = Object.assign({
+ createFormGroup: (fModel: DynamicFormControlModel[]) => {
+ const controls = {};
+ if (hasValue(fModel)) {
+ fModel.forEach((controlModel) => {
+ controls[controlModel.id] = new FormControl((controlModel as any).value);
+ });
+ return new FormGroup(controls);
+ }
+ return undefined;
+ }
+ });
+ router = Object.assign(new RouterStub(), {
+ url: 'http://test-url.com/test-url'
+ });
+ collection = Object.assign(new Collection(), {
+ uuid: 'fake-collection-id'
+ });
+ collectionService = jasmine.createSpyObj('collectionService', {
+ getContentSource: observableOf(contentSource),
+ updateContentSource: observableOf(contentSource),
+ getHarvesterEndpoint: observableOf('harvester-endpoint')
+ });
+ requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring']);
+
+ TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot(), RouterTestingModule],
+ declarations: [CollectionSourceComponent],
+ providers: [
+ { provide: ObjectUpdatesService, useValue: objectUpdatesService },
+ { provide: NotificationsService, useValue: notificationsService },
+ { provide: Location, useValue: location },
+ { provide: DynamicFormService, useValue: formService },
+ { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: new RemoteData(false, false, true, null, collection) }) } } },
+ { provide: Router, useValue: router },
+ { provide: GLOBAL_CONFIG, useValue: { collection: { edit: { undoTimeout: 10 } } } as any },
+ { provide: CollectionDataService, useValue: collectionService },
+ { provide: RequestService, useValue: requestService }
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CollectionSourceComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ describe('on startup', () => {
+ let form;
+
+ beforeEach(() => {
+ form = fixture.debugElement.query(By.css('ds-form'));
+ });
+
+ it('ContentSource should be disabled', () => {
+ expect(comp.contentSource.harvestType).toEqual(ContentSourceHarvestType.None);
+ });
+
+ it('the input-form should be hidden', () => {
+ expect(form).toBeNull();
+ });
+ });
+
+ describe('when selecting the checkbox', () => {
+ let input;
+ let form;
+
+ beforeEach(() => {
+ input = fixture.debugElement.query(By.css('#externalSourceCheck')).nativeElement;
+ input.click();
+ fixture.detectChanges();
+ form = fixture.debugElement.query(By.css('ds-form'));
+ });
+
+ it('should enable ContentSource', () => {
+ expect(comp.contentSource.harvestType).not.toEqual(ContentSourceHarvestType.None);
+ });
+
+ it('should send a field update', () => {
+ expect(objectUpdatesService.saveAddFieldUpdate).toHaveBeenCalledWith(router.url, comp.contentSource)
+ });
+
+ it('should display the form', () => {
+ expect(form).not.toBeNull();
+ });
+ });
+
+ describe('isValid', () => {
+ it('should return true when ContentSource is disabled but the form invalid', () => {
+ spyOnProperty(comp.formGroup, 'valid').and.returnValue(false);
+ comp.contentSource.harvestType = ContentSourceHarvestType.None;
+ expect(comp.isValid()).toBe(true);
+ });
+
+ it('should return false when ContentSource is enabled but the form is invalid', () => {
+ spyOnProperty(comp.formGroup, 'valid').and.returnValue(false);
+ comp.contentSource.harvestType = ContentSourceHarvestType.Metadata;
+ expect(comp.isValid()).toBe(false);
+ });
+
+ it('should return true when ContentSource is enabled and the form is valid', () => {
+ spyOnProperty(comp.formGroup, 'valid').and.returnValue(true);
+ comp.contentSource.harvestType = ContentSourceHarvestType.Metadata;
+ expect(comp.isValid()).toBe(true);
+ });
+ });
+
+ describe('onSubmit', () => {
+ beforeEach(() => {
+ comp.onSubmit();
+ });
+
+ it('should re-initialize the field updates', () => {
+ expect(objectUpdatesService.initialize).toHaveBeenCalled();
+ });
+
+ it('should display a success notification', () => {
+ expect(notificationsService.success).toHaveBeenCalled();
+ });
+
+ it('should call updateContentSource on the collectionService', () => {
+ expect(collectionService.updateContentSource).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts
index 6ec5be884d..5fcc740663 100644
--- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts
+++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts
@@ -1,4 +1,37 @@
-import { Component } from '@angular/core';
+import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
+import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component';
+import {
+ DynamicFormControlModel,
+ DynamicFormGroupModel,
+ DynamicFormLayout,
+ DynamicFormService,
+ DynamicInputModel,
+ DynamicOptionControlModel,
+ DynamicRadioGroupModel,
+ DynamicSelectModel,
+ DynamicTextAreaModel
+} from '@ng-dynamic-forms/core';
+import { Location } from '@angular/common';
+import { TranslateService } from '@ngx-translate/core';
+import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
+import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { FormGroup } from '@angular/forms';
+import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
+import { ContentSource, ContentSourceHarvestType } from '../../../core/shared/content-source.model';
+import { Observable } from 'rxjs/internal/Observable';
+import { RemoteData } from '../../../core/data/remote-data';
+import { Collection } from '../../../core/shared/collection.model';
+import { first, map, switchMap, take } from 'rxjs/operators';
+import { ActivatedRoute, Router } from '@angular/router';
+import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
+import { Subscription } from 'rxjs/internal/Subscription';
+import { cloneDeep } from 'lodash';
+import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config';
+import { CollectionDataService } from '../../../core/data/collection-data.service';
+import { getSucceededRemoteData } from '../../../core/shared/operators';
+import { MetadataConfig } from '../../../core/shared/metadata-config.model';
+import { INotification } from '../../../shared/notifications/models/notification.model';
+import { RequestService } from '../../../core/data/request.service';
/**
* Component for managing the content source of the collection
@@ -7,6 +40,440 @@ import { Component } from '@angular/core';
selector: 'ds-collection-source',
templateUrl: './collection-source.component.html',
})
-export class CollectionSourceComponent {
- /* TODO: Implement Collection Edit - Content Source */
+export class CollectionSourceComponent extends AbstractTrackableComponent implements OnInit, OnDestroy {
+ /**
+ * The current collection's remote data
+ */
+ collectionRD$: Observable>;
+
+ /**
+ * The collection's content source
+ */
+ contentSource: ContentSource;
+
+ /**
+ * The current update to the content source
+ */
+ update$: Observable;
+
+ /**
+ * The initial harvest type we started off with
+ * Used to compare changes
+ */
+ initialHarvestType: ContentSourceHarvestType;
+
+ /**
+ * @type {string} Key prefix used to generate form labels
+ */
+ LABEL_KEY_PREFIX = 'collection.edit.tabs.source.form.';
+
+ /**
+ * @type {string} Key prefix used to generate form error messages
+ */
+ ERROR_KEY_PREFIX = 'collection.edit.tabs.source.form.errors.';
+
+ /**
+ * @type {string} Key prefix used to generate form option labels
+ */
+ OPTIONS_KEY_PREFIX = 'collection.edit.tabs.source.form.options.';
+
+ /**
+ * The Dynamic Input Model for the OAI Provider
+ */
+ oaiSourceModel = new DynamicInputModel({
+ id: 'oaiSource',
+ name: 'oaiSource',
+ required: true,
+ validators: {
+ required: null
+ },
+ errorMessages: {
+ required: 'You must provide a set id of the target collection.'
+ }
+ });
+
+ /**
+ * The Dynamic Input Model for the OAI Set
+ */
+ oaiSetIdModel = new DynamicInputModel({
+ id: 'oaiSetId',
+ name: 'oaiSetId'
+ });
+
+ /**
+ * The Dynamic Input Model for the Metadata Format used
+ */
+ metadataConfigIdModel = new DynamicSelectModel({
+ id: 'metadataConfigId',
+ name: 'metadataConfigId'
+ });
+
+ /**
+ * The Dynamic Input Model for the type of harvesting
+ */
+ harvestTypeModel = new DynamicRadioGroupModel({
+ id: 'harvestType',
+ name: 'harvestType',
+ options: [
+ {
+ value: ContentSourceHarvestType.Metadata
+ },
+ {
+ value: ContentSourceHarvestType.MetadataAndRef
+ },
+ {
+ value: ContentSourceHarvestType.MetadataAndBitstreams
+ }
+ ]
+ });
+
+ /**
+ * All input models in a simple array for easier iterations
+ */
+ inputModels = [this.oaiSourceModel, this.oaiSetIdModel, this.metadataConfigIdModel, this.harvestTypeModel];
+
+ /**
+ * The dynamic form fields used for editing the content source of a collection
+ * @type {(DynamicInputModel | DynamicTextAreaModel)[]}
+ */
+ formModel: DynamicFormControlModel[] = [
+ new DynamicFormGroupModel({
+ id: 'oaiSourceContainer',
+ group: [
+ this.oaiSourceModel
+ ]
+ }),
+ new DynamicFormGroupModel({
+ id: 'oaiSetContainer',
+ group: [
+ this.oaiSetIdModel,
+ this.metadataConfigIdModel
+ ]
+ }),
+ new DynamicFormGroupModel({
+ id: 'harvestTypeContainer',
+ group: [
+ this.harvestTypeModel
+ ]
+ })
+ ];
+
+ /**
+ * Layout used for structuring the form inputs
+ */
+ formLayout: DynamicFormLayout = {
+ oaiSource: {
+ grid: {
+ host: 'col-12 d-inline-block'
+ }
+ },
+ oaiSetId: {
+ grid: {
+ host: 'col col-sm-6 d-inline-block'
+ }
+ },
+ metadataConfigId: {
+ grid: {
+ host: 'col col-sm-6 d-inline-block'
+ }
+ },
+ harvestType: {
+ grid: {
+ host: 'col-12',
+ option: 'btn-outline-secondary'
+ }
+ },
+ oaiSetContainer: {
+ grid: {
+ host: 'row'
+ }
+ },
+ oaiSourceContainer: {
+ grid: {
+ host: 'row'
+ }
+ },
+ harvestTypeContainer: {
+ grid: {
+ host: 'row'
+ }
+ }
+ };
+
+ /**
+ * The form group of this form
+ */
+ formGroup: FormGroup;
+
+ /**
+ * Subscription to update the current form
+ */
+ updateSub: Subscription;
+
+ /**
+ * The content harvesting type used when harvesting is disabled
+ */
+ harvestTypeNone = ContentSourceHarvestType.None;
+
+ /**
+ * The previously selected harvesting type
+ * Used for switching between ContentSourceHarvestType.None and the previously selected value when enabling / disabling harvesting
+ * Defaults to ContentSourceHarvestType.Metadata
+ */
+ previouslySelectedHarvestType = ContentSourceHarvestType.Metadata;
+
+ /**
+ * Notifications displayed after clicking submit
+ * These are cleaned up every time a user submits the form to prevent error or other notifications from staying active
+ * while they shouldn't be.
+ */
+ displayedNotifications: INotification[] = [];
+
+ public constructor(public objectUpdatesService: ObjectUpdatesService,
+ public notificationsService: NotificationsService,
+ protected location: Location,
+ protected formService: DynamicFormService,
+ protected translate: TranslateService,
+ protected route: ActivatedRoute,
+ protected router: Router,
+ @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
+ protected collectionService: CollectionDataService,
+ protected requestService: RequestService) {
+ super(objectUpdatesService, notificationsService, translate);
+ }
+
+ /**
+ * Initialize properties to setup the Field Update and Form
+ */
+ ngOnInit(): void {
+ this.notificationsPrefix = 'collection.edit.tabs.source.notifications.';
+ this.discardTimeOut = this.EnvConfig.collection.edit.undoTimeout;
+ this.url = this.router.url;
+ if (this.url.indexOf('?') > 0) {
+ this.url = this.url.substr(0, this.url.indexOf('?'));
+ }
+ this.formGroup = this.formService.createFormGroup(this.formModel);
+ this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.dso));
+
+ this.collectionRD$.pipe(
+ getSucceededRemoteData(),
+ map((col) => col.payload.uuid),
+ switchMap((uuid) => this.collectionService.getContentSource(uuid)),
+ take(1)
+ ).subscribe((contentSource: ContentSource) => {
+ this.initializeOriginalContentSource(contentSource);
+ });
+
+ this.updateFieldTranslations();
+ this.translate.onLangChange
+ .subscribe(() => {
+ this.updateFieldTranslations();
+ });
+ }
+
+ /**
+ * Initialize the Field Update and subscribe on it to fire updates to the form whenever it changes
+ */
+ initializeOriginalContentSource(contentSource: ContentSource) {
+ this.contentSource = contentSource;
+ this.initialHarvestType = contentSource.harvestType;
+ this.initializeMetadataConfigs();
+ const initialContentSource = cloneDeep(this.contentSource);
+ this.objectUpdatesService.initialize(this.url, [initialContentSource], new Date());
+ this.update$ = this.objectUpdatesService.getFieldUpdates(this.url, [initialContentSource]).pipe(
+ map((updates: FieldUpdates) => updates[initialContentSource.uuid])
+ );
+ this.updateSub = this.update$.subscribe((update: FieldUpdate) => {
+ if (update) {
+ const field = update.field as ContentSource;
+ let configId;
+ if (hasValue(this.contentSource) && isNotEmpty(this.contentSource.metadataConfigs)) {
+ configId = this.contentSource.metadataConfigs[0].id;
+ }
+ if (hasValue(field) && hasValue(field.metadataConfigId)) {
+ configId = field.metadataConfigId;
+ }
+ if (hasValue(field)) {
+ this.formGroup.patchValue({
+ oaiSourceContainer: {
+ oaiSource: field.oaiSource
+ },
+ oaiSetContainer: {
+ oaiSetId: field.oaiSetId,
+ metadataConfigId: configId
+ },
+ harvestTypeContainer: {
+ harvestType: field.harvestType
+ }
+ });
+ this.contentSource = cloneDeep(field);
+ }
+ this.contentSource.metadataConfigId = configId;
+ }
+ });
+ }
+
+ /**
+ * Fill the metadataConfigIdModel's options using the contentSource's metadataConfigs property
+ */
+ initializeMetadataConfigs() {
+ this.metadataConfigIdModel.options = this.contentSource.metadataConfigs
+ .map((metadataConfig: MetadataConfig) => Object.assign({ value: metadataConfig.id, label: metadataConfig.label }));
+ if (this.metadataConfigIdModel.options.length > 0) {
+ this.formGroup.patchValue({
+ oaiSetContainer: {
+ metadataConfigId: this.metadataConfigIdModel.options[0].value
+ }
+ });
+ }
+ }
+
+ /**
+ * Used the update translations of errors and labels on init and on language change
+ */
+ private updateFieldTranslations() {
+ this.inputModels.forEach(
+ (fieldModel: DynamicFormControlModel) => {
+ this.updateFieldTranslation(fieldModel);
+ }
+ );
+ }
+
+ /**
+ * Update the translations of a DynamicInputModel
+ * @param fieldModel
+ */
+ private updateFieldTranslation(fieldModel: DynamicFormControlModel) {
+ fieldModel.label = this.translate.instant(this.LABEL_KEY_PREFIX + fieldModel.id);
+ if (isNotEmpty(fieldModel.validators)) {
+ fieldModel.errorMessages = {};
+ Object.keys(fieldModel.validators).forEach((key) => {
+ fieldModel.errorMessages[key] = this.translate.instant(this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key);
+ });
+ }
+ if (fieldModel instanceof DynamicOptionControlModel) {
+ if (isNotEmpty(fieldModel.options)) {
+ fieldModel.options.forEach((option) => {
+ if (hasNoValue(option.label)) {
+ option.label = this.translate.instant(this.OPTIONS_KEY_PREFIX + fieldModel.id + '.' + option.value);
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Fired whenever the form receives an update and makes sure the Content Source and field update is up-to-date with the changes
+ * @param event
+ */
+ onChange(event) {
+ this.updateContentSourceField(event.model, true);
+ this.saveFieldUpdate();
+ }
+
+ /**
+ * Submit the edited Content Source to the REST API, re-initialize the field update and display a notification
+ */
+ onSubmit() {
+ // Remove cached harvester request to allow for latest harvester to be displayed when switching tabs
+ this.collectionRD$.pipe(
+ getSucceededRemoteData(),
+ map((col) => col.payload.uuid),
+ switchMap((uuid) => this.collectionService.getHarvesterEndpoint(uuid)),
+ take(1)
+ ).subscribe((endpoint) => this.requestService.removeByHrefSubstring(endpoint));
+
+ // Update harvester
+ this.collectionRD$.pipe(
+ getSucceededRemoteData(),
+ map((col) => col.payload.uuid),
+ switchMap((uuid) => this.collectionService.updateContentSource(uuid, this.contentSource)),
+ take(1)
+ ).subscribe((result: ContentSource | INotification) => {
+ if (hasValue((result as any).harvestType)) {
+ this.clearNotifications();
+ this.initializeOriginalContentSource(result as ContentSource);
+ this.displayedNotifications.push(this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')));
+ } else {
+ this.displayedNotifications.push(result as INotification);
+ }
+ });
+ }
+
+ /**
+ * Cancel the edit and return to the previous page
+ */
+ onCancel() {
+ this.location.back();
+ }
+
+ /**
+ * Is the current form valid to be submitted ?
+ */
+ isValid(): boolean {
+ return (this.contentSource.harvestType === ContentSourceHarvestType.None) || this.formGroup.valid;
+ }
+
+ /**
+ * Switch the external source on or off and fire a field update
+ */
+ changeExternalSource() {
+ if (this.contentSource.harvestType === ContentSourceHarvestType.None) {
+ this.contentSource.harvestType = this.previouslySelectedHarvestType;
+ } else {
+ this.previouslySelectedHarvestType = this.contentSource.harvestType;
+ this.contentSource.harvestType = ContentSourceHarvestType.None;
+ }
+ this.updateContentSource(false);
+ }
+
+ /**
+ * Loop over all inputs and update the Content Source with their value
+ * @param updateHarvestType When set to false, the harvestType of the contentSource will be ignored in the update
+ */
+ updateContentSource(updateHarvestType: boolean) {
+ this.inputModels.forEach(
+ (fieldModel: DynamicInputModel) => {
+ this.updateContentSourceField(fieldModel, updateHarvestType)
+ }
+ );
+ this.saveFieldUpdate();
+ }
+
+ /**
+ * Update the Content Source with the value from a DynamicInputModel
+ * @param fieldModel The fieldModel to fetch the value from and update the contentSource with
+ * @param updateHarvestType When set to false, the harvestType of the contentSource will be ignored in the update
+ */
+ updateContentSourceField(fieldModel: DynamicInputModel, updateHarvestType: boolean) {
+ if (hasValue(fieldModel.value) && !(fieldModel.id === this.harvestTypeModel.id && !updateHarvestType)) {
+ this.contentSource[fieldModel.id] = fieldModel.value;
+ }
+ }
+
+ /**
+ * Save the current Content Source to the Object Updates cache
+ */
+ saveFieldUpdate() {
+ this.objectUpdatesService.saveAddFieldUpdate(this.url, cloneDeep(this.contentSource));
+ }
+
+ /**
+ * Clear possible active notifications
+ */
+ clearNotifications() {
+ this.displayedNotifications.forEach((notification: INotification) => {
+ this.notificationsService.remove(notification);
+ });
+ this.displayedNotifications = [];
+ }
+
+ /**
+ * Make sure open subscriptions are closed
+ */
+ ngOnDestroy(): void {
+ if (this.updateSub) {
+ this.updateSub.unsubscribe();
+ }
+ }
}
diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts
index 3915eca23f..5f4e15e138 100644
--- a/src/app/core/cache/response.models.ts
+++ b/src/app/core/cache/response.models.ts
@@ -14,6 +14,7 @@ import { DSpaceObject } from '../shared/dspace-object.model';
import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model';
import { MetadataSchema } from '../metadata/metadata-schema.model';
import { MetadataField } from '../metadata/metadata-field.model';
+import { ContentSource } from '../shared/content-source.model';
/* tslint:disable:max-classes-per-file */
export class RestResponse {
@@ -288,4 +289,17 @@ export class FilteredDiscoveryQueryResponse extends RestResponse {
super(true, statusCode, statusText);
}
}
+
+/**
+ * A successful response containing exactly one MetadataSchema
+ */
+export class ContentSourceSuccessResponse extends RestResponse {
+ constructor(
+ public contentsource: ContentSource,
+ public statusCode: number,
+ public statusText: string,
+ ) {
+ super(true, statusCode, statusText);
+ }
+}
/* tslint:enable:max-classes-per-file */
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index efd83d33d5..811ecacd38 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -119,6 +119,7 @@ import { MetadatafieldParsingService } from './data/metadatafield-parsing.servic
import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model';
import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
import { BrowseDefinition } from './shared/browse-definition.model';
+import { ContentSourceResponseParsingService } from './data/content-source-response-parsing.service';
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
import { ObjectSelectService } from '../shared/object-select/object-select.service';
import { SiteDataService } from './data/site-data.service';
@@ -244,6 +245,7 @@ const PROVIDERS = [
TaskResponseParsingService,
ClaimedTaskDataService,
PoolTaskDataService,
+ ContentSourceResponseParsingService,
SearchService,
SidebarService,
SearchFilterService,
diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts
index a3e1a916e3..c8f056bf19 100644
--- a/src/app/core/data/collection-data.service.spec.ts
+++ b/src/app/core/data/collection-data.service.spec.ts
@@ -1,44 +1,132 @@
import { CollectionDataService } from './collection-data.service';
-import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
-import { getMockRequestService } from '../../shared/mocks/mock-request.service';
-import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from './request.service';
+import { TranslateService } from '@ngx-translate/core';
+import { getMockRequestService } from '../../shared/mocks/mock-request.service';
+import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
+import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
+import { getMockTranslateService } from '../../shared/mocks/mock-translate.service';
+import { fakeAsync, tick } from '@angular/core/testing';
+import { ContentSourceRequest, GetRequest, RequestError, UpdateContentSourceRequest } from './request.models';
+import { ContentSource } from '../shared/content-source.model';
+import { of as observableOf } from 'rxjs/internal/observable/of';
+import { RequestEntry } from './request.reducer';
+import { ErrorResponse, RestResponse } from '../cache/response.models';
import { ObjectCacheService } from '../cache/object-cache.service';
-import { GetRequest } from './request.models';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
+const url = 'fake-url';
+const collectionId = 'fake-collection-id';
+
describe('CollectionDataService', () => {
let service: CollectionDataService;
- let objectCache: ObjectCacheService;
+
let requestService: RequestService;
- let halService: HALEndpointService;
+ let translate: TranslateService;
+ let notificationsService: any;
let rdbService: RemoteDataBuildService;
+ let objectCache: ObjectCacheService;
+ let halService: any;
- const url = 'fake-collections-url';
-
- beforeEach(() => {
- objectCache = jasmine.createSpyObj('objectCache', {
- remove: jasmine.createSpy('remove')
+ describe('when the requests are successful', () => {
+ beforeEach(() => {
+ createService();
});
- requestService = getMockRequestService();
- halService = Object.assign(new HALEndpointServiceStub(url));
+
+ describe('when calling getContentSource', () => {
+ let contentSource$;
+
+ beforeEach(() => {
+ contentSource$ = service.getContentSource(collectionId);
+ });
+
+ it('should configure a new ContentSourceRequest', fakeAsync(() => {
+ contentSource$.subscribe();
+ tick();
+ expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(ContentSourceRequest));
+ }));
+ });
+
+ describe('when calling updateContentSource', () => {
+ let returnedContentSource$;
+ let contentSource;
+
+ beforeEach(() => {
+ contentSource = new ContentSource();
+ returnedContentSource$ = service.updateContentSource(collectionId, contentSource);
+ });
+
+ it('should configure a new UpdateContentSourceRequest', fakeAsync(() => {
+ returnedContentSource$.subscribe();
+ tick();
+ expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(UpdateContentSourceRequest));
+ }));
+ });
+
+ describe('getMappedItems', () => {
+ let result;
+
+ beforeEach(() => {
+ result = service.getMappedItems('collection-id');
+ });
+
+ it('should configure a GET request', () => {
+ expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest));
+ });
+ });
+
+ });
+
+ describe('when the requests are unsuccessful', () => {
+ beforeEach(() => {
+ createService(observableOf(Object.assign(new RequestEntry(), {
+ response: new ErrorResponse(Object.assign({
+ statusCode: 422,
+ statusText: 'Unprocessable Entity',
+ message: 'Error message'
+ }))
+ })));
+ });
+
+ describe('when calling updateContentSource', () => {
+ let returnedContentSource$;
+ let contentSource;
+
+ beforeEach(() => {
+ contentSource = new ContentSource();
+ returnedContentSource$ = service.updateContentSource(collectionId, contentSource);
+ });
+
+ it('should configure a new UpdateContentSourceRequest', fakeAsync(() => {
+ returnedContentSource$.subscribe();
+ tick();
+ expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(UpdateContentSourceRequest));
+ }));
+
+ it('should display an error notification', fakeAsync(() => {
+ returnedContentSource$.subscribe();
+ tick();
+ expect(notificationsService.error).toHaveBeenCalled();
+ }));
+ });
+ });
+
+ /**
+ * Create a CollectionDataService used for testing
+ * @param requestEntry$ Supply a requestEntry to be returned by the REST API (optional)
+ */
+ function createService(requestEntry$?) {
+ requestService = getMockRequestService(requestEntry$);
rdbService = jasmine.createSpyObj('rdbService', {
buildList: jasmine.createSpy('buildList')
});
-
- service = new CollectionDataService(requestService, rdbService, null, null, null, objectCache, halService, null, null, null);
- });
-
- describe('getMappedItems', () => {
- let result;
-
- beforeEach(() => {
- result = service.getMappedItems('collection-id');
+ objectCache = jasmine.createSpyObj('objectCache', {
+ remove: jasmine.createSpy('remove')
});
+ halService = new HALEndpointServiceStub(url);
+ notificationsService = new NotificationsServiceStub();
+ translate = getMockTranslateService();
- it('should configure a GET request', () => {
- expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest));
- });
- });
+ service = new CollectionDataService(requestService, rdbService, null, null, null, objectCache, halService, notificationsService, null, null, translate);
+ }
});
diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts
index 0c032e6766..ed05c99e27 100644
--- a/src/app/core/data/collection-data.service.ts
+++ b/src/app/core/data/collection-data.service.ts
@@ -12,25 +12,45 @@ import { CommunityDataService } from './community-data.service';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
-import { HttpClient } from '@angular/common/http';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { Observable } from 'rxjs/internal/Observable';
-import {FindListOptions, FindListRequest, GetRequest} from './request.models';
+import {
+ ContentSourceRequest,
+ RestRequest,
+ UpdateContentSourceRequest,
+ GetRequest,
+ FindListOptions
+} from './request.models';
import { RemoteData } from './remote-data';
import { PaginatedList } from './paginated-list';
-import { configureRequest } from '../shared/operators';
+import { ContentSource } from '../shared/content-source.model';
+import {
+ configureRequest,
+ filterSuccessfulResponses,
+ getRequestFromRequestHref,
+ getResponseFromEntry
+} from '../shared/operators';
+import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models';
+import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
+import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
+import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
+import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
+import { TranslateService } from '@ngx-translate/core';
+import { SearchParam } from '../cache/models/search-param.model';
import { DSOResponseParsingService } from './dso-response-parsing.service';
import { ResponseParsingService } from './parsing.service';
import { GenericConstructor } from '../shared/generic-constructor';
-import { hasValue, isNotEmptyOperator } from '../../shared/empty.util';
import { DSpaceObject } from '../shared/dspace-object.model';
-import { SearchParam } from '../cache/models/search-param.model';
+import { INotification } from '../../shared/notifications/models/notification.model';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
@Injectable()
export class CollectionDataService extends ComColDataService {
protected linkPath = 'collections';
+ protected errorTitle = 'collection.source.update.notifications.error.title';
+ protected contentSourceError = 'collection.source.update.notifications.error.content';
constructor(
protected requestService: RequestService,
@@ -42,7 +62,8 @@ export class CollectionDataService extends ComColDataService {
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
- protected comparator: DSOChangeAnalyzer
+ protected comparator: DSOChangeAnalyzer,
+ protected translate: TranslateService
) {
super();
}
@@ -97,6 +118,81 @@ export class CollectionDataService extends ComColDataService {
);
}
+ /**
+ * Get the endpoint for the collection's content harvester
+ * @param collectionId
+ */
+ getHarvesterEndpoint(collectionId: string): Observable {
+ return this.halService.getEndpoint(this.linkPath).pipe(
+ switchMap((href: string) => this.halService.getEndpoint('harvester', `${href}/${collectionId}`))
+ );
+ }
+
+ /**
+ * Get the collection's content harvester
+ * @param collectionId
+ */
+ getContentSource(collectionId: string): Observable {
+ return this.getHarvesterEndpoint(collectionId).pipe(
+ map((href: string) => new ContentSourceRequest(this.requestService.generateRequestId(), href)),
+ configureRequest(this.requestService),
+ map((request: RestRequest) => request.href),
+ getRequestFromRequestHref(this.requestService),
+ filterSuccessfulResponses(),
+ map((response: ContentSourceSuccessResponse) => response.contentsource)
+ );
+ }
+
+ /**
+ * Update the settings of the collection's content harvester
+ * @param collectionId
+ * @param contentSource
+ */
+ updateContentSource(collectionId: string, contentSource: ContentSource): Observable {
+ const requestId = this.requestService.generateRequestId();
+ const serializedContentSource = new DSpaceRESTv2Serializer(ContentSource).serialize(contentSource);
+ const request$ = this.getHarvesterEndpoint(collectionId).pipe(
+ take(1),
+ map((href: string) => {
+ const options: HttpOptions = Object.create({});
+ let headers = new HttpHeaders();
+ headers = headers.append('Content-Type', 'application/json');
+ options.headers = headers;
+ return new UpdateContentSourceRequest(requestId, href, JSON.stringify(serializedContentSource), options);
+ })
+ );
+
+ // Execute the post/put request
+ request$.pipe(
+ configureRequest(this.requestService)
+ ).subscribe();
+
+ // Return updated ContentSource
+ return this.requestService.getByUUID(requestId).pipe(
+ getResponseFromEntry(),
+ map((response: RestResponse) => {
+ if (!response.isSuccessful) {
+ if (hasValue((response as any).errorMessage)) {
+ if (response.statusCode === 422) {
+ return this.notificationsService.error(this.translate.instant(this.errorTitle), this.translate.instant(this.contentSourceError), new NotificationOptions(-1));
+ } else {
+ return this.notificationsService.error(this.translate.instant(this.errorTitle), (response as any).errorMessage, new NotificationOptions(-1));
+ }
+ }
+ } else {
+ return response;
+ }
+ }),
+ isNotEmptyOperator(),
+ map((response: ContentSourceSuccessResponse | INotification) => {
+ if (isNotEmpty((response as any).contentsource)) {
+ return (response as ContentSourceSuccessResponse).contentsource;
+ }
+ return response as INotification;
+ })
+ );
+ }
+
/**
* Fetches the endpoint used for mapping items to a collection
* @param collectionId The id of the collection to map items to
diff --git a/src/app/core/data/content-source-response-parsing.service.ts b/src/app/core/data/content-source-response-parsing.service.ts
new file mode 100644
index 0000000000..4e0490148b
--- /dev/null
+++ b/src/app/core/data/content-source-response-parsing.service.ts
@@ -0,0 +1,31 @@
+import { Injectable } from '@angular/core';
+import { ResponseParsingService } from './parsing.service';
+import { RestRequest } from './request.models';
+import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
+import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models';
+import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
+import { ContentSource } from '../shared/content-source.model';
+import { MetadataConfig } from '../shared/metadata-config.model';
+
+@Injectable()
+/**
+ * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a ContentSource object
+ * wrapped in a ContentSourceSuccessResponse
+ */
+export class ContentSourceResponseParsingService implements ResponseParsingService {
+
+ parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
+ const payload = data.payload;
+
+ const deserialized = new DSpaceRESTv2Serializer(ContentSource).deserialize(payload);
+
+ let metadataConfigs = [];
+ if (payload._embedded && payload._embedded.harvestermetadata && payload._embedded.harvestermetadata.configs) {
+ metadataConfigs = new DSpaceRESTv2Serializer(MetadataConfig).serializeArray(payload._embedded.harvestermetadata.configs);
+ }
+ deserialized.metadataConfigs = metadataConfigs;
+
+ return new ContentSourceSuccessResponse(deserialized, data.statusCode, data.statusText);
+ }
+
+}
diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts
index 08745f9223..3bbb3960b6 100644
--- a/src/app/core/data/object-updates/object-updates.service.ts
+++ b/src/app/core/data/object-updates/object-updates.service.ts
@@ -93,14 +93,16 @@ export class ObjectUpdatesService {
const objectUpdates = this.getObjectEntry(url);
return objectUpdates.pipe(map((objectEntry) => {
const fieldUpdates: FieldUpdates = {};
- Object.keys(objectEntry.fieldStates).forEach((uuid) => {
- let fieldUpdate = objectEntry.fieldUpdates[uuid];
- if (isEmpty(fieldUpdate)) {
- const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid);
- fieldUpdate = { field: identifiable, changeType: undefined };
- }
- fieldUpdates[uuid] = fieldUpdate;
- });
+ if (hasValue(objectEntry)) {
+ Object.keys(objectEntry.fieldStates).forEach((uuid) => {
+ let fieldUpdate = objectEntry.fieldUpdates[uuid];
+ if (isEmpty(fieldUpdate)) {
+ const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid);
+ fieldUpdate = {field: identifiable, changeType: undefined};
+ }
+ fieldUpdates[uuid] = fieldUpdate;
+ });
+ }
return fieldUpdates;
}))
}
diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts
index ca864f99de..2305fc2d5d 100644
--- a/src/app/core/data/request.models.ts
+++ b/src/app/core/data/request.models.ts
@@ -18,6 +18,7 @@ import { MetadataschemaParsingService } from './metadataschema-parsing.service';
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
import { URLCombiner } from '../url-combiner/url-combiner';
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
+import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
/* tslint:disable:max-classes-per-file */
@@ -378,6 +379,26 @@ export class CreateRequest extends PostRequest {
}
}
+export class ContentSourceRequest extends GetRequest {
+ constructor(uuid: string, href: string) {
+ super(uuid, href);
+ }
+
+ getResponseParser(): GenericConstructor {
+ return ContentSourceResponseParsingService;
+ }
+}
+
+export class UpdateContentSourceRequest extends PutRequest {
+ constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
+ super(uuid, href, body, options);
+ }
+
+ getResponseParser(): GenericConstructor {
+ return ContentSourceResponseParsingService;
+ }
+}
+
/**
* Request to delete an object based on its identifier
*/
diff --git a/src/app/core/shared/content-source.model.ts b/src/app/core/shared/content-source.model.ts
new file mode 100644
index 0000000000..cd53c2d81e
--- /dev/null
+++ b/src/app/core/shared/content-source.model.ts
@@ -0,0 +1,60 @@
+import { autoserialize, autoserializeAs, deserializeAs, deserialize } from 'cerialize';
+import { MetadataConfig } from './metadata-config.model';
+
+/**
+ * The type of content harvesting used
+ */
+export enum ContentSourceHarvestType {
+ None = 'NONE',
+ Metadata = 'METADATA_ONLY',
+ MetadataAndRef = 'METADATA_AND_REF',
+ MetadataAndBitstreams = 'METADATA_AND_BITSTREAMS'
+}
+
+/**
+ * A model class that holds information about the Content Source of a Collection
+ */
+export class ContentSource {
+ /**
+ * Unique identifier, this is necessary to store the ContentSource in FieldUpdates
+ * Because the ContentSource coming from the REST API doesn't have a UUID, we're using the selflink
+ */
+ @deserializeAs('self')
+ uuid: string;
+
+ /**
+ * OAI Provider / Source
+ */
+ @autoserializeAs('oai_source')
+ oaiSource: string;
+
+ /**
+ * OAI Specific set ID
+ */
+ @autoserializeAs('oai_set_id')
+ oaiSetId: string;
+
+ /**
+ * The ID of the metadata format used
+ */
+ @autoserializeAs('metadata_config_id')
+ metadataConfigId: string;
+
+ /**
+ * Type of content being harvested
+ * Defaults to 'NONE', meaning the collection doesn't harvest its content from an external source
+ */
+ @autoserializeAs('harvest_type')
+ harvestType = ContentSourceHarvestType.None;
+
+ /**
+ * The available metadata configurations
+ */
+ metadataConfigs: MetadataConfig[];
+
+ /**
+ * The REST link to itself
+ */
+ @deserialize
+ self: string;
+}
diff --git a/src/app/core/shared/metadata-config.model.ts b/src/app/core/shared/metadata-config.model.ts
new file mode 100644
index 0000000000..861d04586e
--- /dev/null
+++ b/src/app/core/shared/metadata-config.model.ts
@@ -0,0 +1,19 @@
+/**
+ * A model class that holds information about a certain metadata configuration
+ */
+export class MetadataConfig {
+ /**
+ * A unique indentifier
+ */
+ id: string;
+
+ /**
+ * The label used for display
+ */
+ label: string;
+
+ /**
+ * The namespace of the metadata
+ */
+ nameSpace: string;
+}
diff --git a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts
index 0f9d4c55b4..2fa05fa28b 100644
--- a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts
+++ b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts
@@ -2,10 +2,8 @@ import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { ActivatedRoute, Router } from '@angular/router';
import { RemoteData } from '../../../core/data/remote-data';
-import { isNotEmpty, isNotUndefined } from '../../empty.util';
+import { isNotEmpty } from '../../empty.util';
import { first, map } from 'rxjs/operators';
-import { getSucceededRemoteData } from '../../../core/shared/operators';
-import { DataService } from '../../../core/data/data.service';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
/**
diff --git a/src/app/shared/trackable/abstract-trackable.component.ts b/src/app/shared/trackable/abstract-trackable.component.ts
index cd1b425f10..bb1f4b31b4 100644
--- a/src/app/shared/trackable/abstract-trackable.component.ts
+++ b/src/app/shared/trackable/abstract-trackable.component.ts
@@ -63,7 +63,7 @@ export class AbstractTrackableComponent {
* Get translated notification title
* @param key
*/
- private getNotificationTitle(key: string) {
+ protected getNotificationTitle(key: string) {
return this.translateService.instant(this.notificationsPrefix + key + '.title');
}
@@ -71,7 +71,7 @@ export class AbstractTrackableComponent {
* Get translated notification content
* @param key
*/
- private getNotificationContent(key: string) {
+ protected getNotificationContent(key: string) {
return this.translateService.instant(this.notificationsPrefix + key + '.content');
}
diff --git a/src/config/collection-page-config.interface.ts b/src/config/collection-page-config.interface.ts
new file mode 100644
index 0000000000..b0103fd176
--- /dev/null
+++ b/src/config/collection-page-config.interface.ts
@@ -0,0 +1,7 @@
+import { Config } from './config.interface';
+
+export interface CollectionPageConfig extends Config {
+ edit: {
+ undoTimeout: number;
+ }
+}
diff --git a/src/config/global-config.interface.ts b/src/config/global-config.interface.ts
index 22b4b0500f..dec23ff676 100644
--- a/src/config/global-config.interface.ts
+++ b/src/config/global-config.interface.ts
@@ -8,6 +8,7 @@ import { FormConfig } from './form-config.interfaces';
import {LangConfig} from './lang-config.interface';
import { BrowseByConfig } from './browse-by-config.interface';
import { ItemPageConfig } from './item-page-config.interface';
+import { CollectionPageConfig } from './collection-page-config.interface';
import { Theme } from './theme.inferface';
export interface GlobalConfig extends Config {
@@ -26,5 +27,6 @@ export interface GlobalConfig extends Config {
languages: LangConfig[];
browseBy: BrowseByConfig;
item: ItemPageConfig;
+ collection: CollectionPageConfig;
themes: Theme[];
}