mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-09 19:13:08 +00:00
Merge pull request #506 from atmire/Collection-content-source-tab
Collection content source tab
This commit is contained in:
@@ -185,6 +185,11 @@ module.exports = {
|
|||||||
undoTimeout: 10000 // 10 seconds
|
undoTimeout: 10000 // 10 seconds
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
collection: {
|
||||||
|
edit: {
|
||||||
|
undoTimeout: 10000 // 10 seconds
|
||||||
|
}
|
||||||
|
},
|
||||||
theme: {
|
theme: {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
}
|
}
|
||||||
|
@@ -338,8 +338,40 @@
|
|||||||
|
|
||||||
"collection.edit.tabs.roles.title": "Collection Edit - Roles",
|
"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.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",
|
"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.tabTitle": "DSpace - Community List",
|
||||||
|
|
||||||
"communityList.title": "List of Communities",
|
"communityList.title": "List of Communities",
|
||||||
@@ -997,6 +1035,8 @@
|
|||||||
|
|
||||||
"loading.collections": "Loading collections...",
|
"loading.collections": "Loading collections...",
|
||||||
|
|
||||||
|
"loading.content-source": "Loading content source...",
|
||||||
|
|
||||||
"loading.community": "Loading community...",
|
"loading.community": "Loading community...",
|
||||||
|
|
||||||
"loading.default": "Loading...",
|
"loading.default": "Loading...",
|
||||||
|
@@ -0,0 +1,56 @@
|
|||||||
|
<div class="container-fluid">
|
||||||
|
<div class="d-inline-block float-right">
|
||||||
|
<button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||||
|
[disabled]="!(hasChanges() | async)"
|
||||||
|
(click)="discard()"><i
|
||||||
|
class="fas fa-times"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||||
|
(click)="reinstate()"><i
|
||||||
|
class="fas fa-undo-alt"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
|
||||||
|
(click)="onSubmit()"><i
|
||||||
|
class="fas fa-save"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h4>{{ 'collection.edit.tabs.source.head' | translate }}</h4>
|
||||||
|
<div *ngIf="contentSource" class="form-check mb-4">
|
||||||
|
<input type="checkbox" class="form-check-input" id="externalSourceCheck" [checked]="(contentSource?.harvestType !== harvestTypeNone)" (change)="changeExternalSource()">
|
||||||
|
<label class="form-check-label" for="externalSourceCheck">{{ 'collection.edit.tabs.source.external' | translate }}</label>
|
||||||
|
</div>
|
||||||
|
<ds-loading *ngIf="!contentSource" [message]="'loading.content-source' | translate"></ds-loading>
|
||||||
|
<h4 *ngIf="contentSource && (contentSource?.harvestType !== harvestTypeNone)">{{ 'collection.edit.tabs.source.form.head' | translate }}</h4>
|
||||||
|
</div>
|
||||||
|
<ds-form *ngIf="formGroup && contentSource && (contentSource?.harvestType !== harvestTypeNone)"
|
||||||
|
[formId]="'collection-source-form-id'"
|
||||||
|
[formGroup]="formGroup"
|
||||||
|
[formModel]="formModel"
|
||||||
|
[formLayout]="formLayout"
|
||||||
|
[displaySubmit]="false"
|
||||||
|
(dfChange)="onChange($event)"
|
||||||
|
(submitForm)="onSubmit()"
|
||||||
|
(cancel)="onCancel()"></ds-form>
|
||||||
|
<div class="container-fluid" *ngIf="(contentSource?.harvestType !== harvestTypeNone)">
|
||||||
|
<div class="d-inline-block float-right">
|
||||||
|
<button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||||
|
[disabled]="!(hasChanges() | async)"
|
||||||
|
(click)="discard()"><i
|
||||||
|
class="fas fa-times"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||||
|
(click)="reinstate()"><i
|
||||||
|
class="fas fa-undo-alt"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
|
||||||
|
(click)="onSubmit()"><i
|
||||||
|
class="fas fa-save"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@@ -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<CollectionSourceComponent>;
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -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
|
* Component for managing the content source of the collection
|
||||||
@@ -7,6 +40,440 @@ import { Component } from '@angular/core';
|
|||||||
selector: 'ds-collection-source',
|
selector: 'ds-collection-source',
|
||||||
templateUrl: './collection-source.component.html',
|
templateUrl: './collection-source.component.html',
|
||||||
})
|
})
|
||||||
export class CollectionSourceComponent {
|
export class CollectionSourceComponent extends AbstractTrackableComponent implements OnInit, OnDestroy {
|
||||||
/* TODO: Implement Collection Edit - Content Source */
|
/**
|
||||||
|
* The current collection's remote data
|
||||||
|
*/
|
||||||
|
collectionRD$: Observable<RemoteData<Collection>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The collection's content source
|
||||||
|
*/
|
||||||
|
contentSource: ContentSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current update to the content source
|
||||||
|
*/
|
||||||
|
update$: Observable<FieldUpdate>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<string>({
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
14
src/app/core/cache/response.models.ts
vendored
14
src/app/core/cache/response.models.ts
vendored
@@ -14,6 +14,7 @@ import { DSpaceObject } from '../shared/dspace-object.model';
|
|||||||
import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model';
|
import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model';
|
||||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
import { MetadataField } from '../metadata/metadata-field.model';
|
import { MetadataField } from '../metadata/metadata-field.model';
|
||||||
|
import { ContentSource } from '../shared/content-source.model';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export class RestResponse {
|
export class RestResponse {
|
||||||
@@ -288,4 +289,17 @@ export class FilteredDiscoveryQueryResponse extends RestResponse {
|
|||||||
super(true, statusCode, statusText);
|
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 */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
@@ -119,6 +119,7 @@ import { MetadatafieldParsingService } from './data/metadatafield-parsing.servic
|
|||||||
import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model';
|
import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model';
|
||||||
import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
|
import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
|
||||||
import { BrowseDefinition } from './shared/browse-definition.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 { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
||||||
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
||||||
import { SiteDataService } from './data/site-data.service';
|
import { SiteDataService } from './data/site-data.service';
|
||||||
@@ -244,6 +245,7 @@ const PROVIDERS = [
|
|||||||
TaskResponseParsingService,
|
TaskResponseParsingService,
|
||||||
ClaimedTaskDataService,
|
ClaimedTaskDataService,
|
||||||
PoolTaskDataService,
|
PoolTaskDataService,
|
||||||
|
ContentSourceResponseParsingService,
|
||||||
SearchService,
|
SearchService,
|
||||||
SidebarService,
|
SidebarService,
|
||||||
SearchFilterService,
|
SearchFilterService,
|
||||||
|
@@ -1,44 +1,132 @@
|
|||||||
import { CollectionDataService } from './collection-data.service';
|
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 { 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 { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { GetRequest } from './request.models';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
|
||||||
|
const url = 'fake-url';
|
||||||
|
const collectionId = 'fake-collection-id';
|
||||||
|
|
||||||
describe('CollectionDataService', () => {
|
describe('CollectionDataService', () => {
|
||||||
let service: CollectionDataService;
|
let service: CollectionDataService;
|
||||||
let objectCache: ObjectCacheService;
|
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let halService: HALEndpointService;
|
let translate: TranslateService;
|
||||||
|
let notificationsService: any;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let objectCache: ObjectCacheService;
|
||||||
|
let halService: any;
|
||||||
|
|
||||||
const url = 'fake-collections-url';
|
describe('when the requests are successful', () => {
|
||||||
|
beforeEach(() => {
|
||||||
beforeEach(() => {
|
createService();
|
||||||
objectCache = jasmine.createSpyObj('objectCache', {
|
|
||||||
remove: jasmine.createSpy('remove')
|
|
||||||
});
|
});
|
||||||
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', {
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
buildList: jasmine.createSpy('buildList')
|
buildList: jasmine.createSpy('buildList')
|
||||||
});
|
});
|
||||||
|
objectCache = jasmine.createSpyObj('objectCache', {
|
||||||
service = new CollectionDataService(requestService, rdbService, null, null, null, objectCache, halService, null, null, null);
|
remove: jasmine.createSpy('remove')
|
||||||
});
|
|
||||||
|
|
||||||
describe('getMappedItems', () => {
|
|
||||||
let result;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
result = service.getMappedItems('collection-id');
|
|
||||||
});
|
});
|
||||||
|
halService = new HALEndpointServiceStub(url);
|
||||||
|
notificationsService = new NotificationsServiceStub();
|
||||||
|
translate = getMockTranslateService();
|
||||||
|
|
||||||
it('should configure a GET request', () => {
|
service = new CollectionDataService(requestService, rdbService, null, null, null, objectCache, halService, notificationsService, null, null, translate);
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest));
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -12,25 +12,45 @@ import { CommunityDataService } from './community-data.service';
|
|||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.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 { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
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 { RemoteData } from './remote-data';
|
||||||
import { PaginatedList } from './paginated-list';
|
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 { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||||
import { ResponseParsingService } from './parsing.service';
|
import { ResponseParsingService } from './parsing.service';
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
import { hasValue, isNotEmptyOperator } from '../../shared/empty.util';
|
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
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';
|
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CollectionDataService extends ComColDataService<Collection> {
|
export class CollectionDataService extends ComColDataService<Collection> {
|
||||||
protected linkPath = 'collections';
|
protected linkPath = 'collections';
|
||||||
|
protected errorTitle = 'collection.source.update.notifications.error.title';
|
||||||
|
protected contentSourceError = 'collection.source.update.notifications.error.content';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -42,7 +62,8 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
|||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
protected http: HttpClient,
|
||||||
protected comparator: DSOChangeAnalyzer<Collection>
|
protected comparator: DSOChangeAnalyzer<Collection>,
|
||||||
|
protected translate: TranslateService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -97,6 +118,81 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint for the collection's content harvester
|
||||||
|
* @param collectionId
|
||||||
|
*/
|
||||||
|
getHarvesterEndpoint(collectionId: string): Observable<string> {
|
||||||
|
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<ContentSource> {
|
||||||
|
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<ContentSource | INotification> {
|
||||||
|
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
|
* Fetches the endpoint used for mapping items to a collection
|
||||||
* @param collectionId The id of the collection to map items to
|
* @param collectionId The id of the collection to map items to
|
||||||
|
31
src/app/core/data/content-source-response-parsing.service.ts
Normal file
31
src/app/core/data/content-source-response-parsing.service.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -93,14 +93,16 @@ export class ObjectUpdatesService {
|
|||||||
const objectUpdates = this.getObjectEntry(url);
|
const objectUpdates = this.getObjectEntry(url);
|
||||||
return objectUpdates.pipe(map((objectEntry) => {
|
return objectUpdates.pipe(map((objectEntry) => {
|
||||||
const fieldUpdates: FieldUpdates = {};
|
const fieldUpdates: FieldUpdates = {};
|
||||||
Object.keys(objectEntry.fieldStates).forEach((uuid) => {
|
if (hasValue(objectEntry)) {
|
||||||
let fieldUpdate = objectEntry.fieldUpdates[uuid];
|
Object.keys(objectEntry.fieldStates).forEach((uuid) => {
|
||||||
if (isEmpty(fieldUpdate)) {
|
let fieldUpdate = objectEntry.fieldUpdates[uuid];
|
||||||
const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid);
|
if (isEmpty(fieldUpdate)) {
|
||||||
fieldUpdate = { field: identifiable, changeType: undefined };
|
const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid);
|
||||||
}
|
fieldUpdate = {field: identifiable, changeType: undefined};
|
||||||
fieldUpdates[uuid] = fieldUpdate;
|
}
|
||||||
});
|
fieldUpdates[uuid] = fieldUpdate;
|
||||||
|
});
|
||||||
|
}
|
||||||
return fieldUpdates;
|
return fieldUpdates;
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,7 @@ import { MetadataschemaParsingService } from './metadataschema-parsing.service';
|
|||||||
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
|
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
||||||
|
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
||||||
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* 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<ResponseParsingService> {
|
||||||
|
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<ResponseParsingService> {
|
||||||
|
return ContentSourceResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request to delete an object based on its identifier
|
* Request to delete an object based on its identifier
|
||||||
*/
|
*/
|
||||||
|
60
src/app/core/shared/content-source.model.ts
Normal file
60
src/app/core/shared/content-source.model.ts
Normal file
@@ -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;
|
||||||
|
}
|
19
src/app/core/shared/metadata-config.model.ts
Normal file
19
src/app/core/shared/metadata-config.model.ts
Normal file
@@ -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;
|
||||||
|
}
|
@@ -2,10 +2,8 @@ import { Component, OnInit } from '@angular/core';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
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 { 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';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -63,7 +63,7 @@ export class AbstractTrackableComponent {
|
|||||||
* Get translated notification title
|
* Get translated notification title
|
||||||
* @param key
|
* @param key
|
||||||
*/
|
*/
|
||||||
private getNotificationTitle(key: string) {
|
protected getNotificationTitle(key: string) {
|
||||||
return this.translateService.instant(this.notificationsPrefix + key + '.title');
|
return this.translateService.instant(this.notificationsPrefix + key + '.title');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ export class AbstractTrackableComponent {
|
|||||||
* Get translated notification content
|
* Get translated notification content
|
||||||
* @param key
|
* @param key
|
||||||
*/
|
*/
|
||||||
private getNotificationContent(key: string) {
|
protected getNotificationContent(key: string) {
|
||||||
return this.translateService.instant(this.notificationsPrefix + key + '.content');
|
return this.translateService.instant(this.notificationsPrefix + key + '.content');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
7
src/config/collection-page-config.interface.ts
Normal file
7
src/config/collection-page-config.interface.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Config } from './config.interface';
|
||||||
|
|
||||||
|
export interface CollectionPageConfig extends Config {
|
||||||
|
edit: {
|
||||||
|
undoTimeout: number;
|
||||||
|
}
|
||||||
|
}
|
@@ -8,6 +8,7 @@ import { FormConfig } from './form-config.interfaces';
|
|||||||
import {LangConfig} from './lang-config.interface';
|
import {LangConfig} from './lang-config.interface';
|
||||||
import { BrowseByConfig } from './browse-by-config.interface';
|
import { BrowseByConfig } from './browse-by-config.interface';
|
||||||
import { ItemPageConfig } from './item-page-config.interface';
|
import { ItemPageConfig } from './item-page-config.interface';
|
||||||
|
import { CollectionPageConfig } from './collection-page-config.interface';
|
||||||
import { Theme } from './theme.inferface';
|
import { Theme } from './theme.inferface';
|
||||||
|
|
||||||
export interface GlobalConfig extends Config {
|
export interface GlobalConfig extends Config {
|
||||||
@@ -26,5 +27,6 @@ export interface GlobalConfig extends Config {
|
|||||||
languages: LangConfig[];
|
languages: LangConfig[];
|
||||||
browseBy: BrowseByConfig;
|
browseBy: BrowseByConfig;
|
||||||
item: ItemPageConfig;
|
item: ItemPageConfig;
|
||||||
|
collection: CollectionPageConfig;
|
||||||
themes: Theme[];
|
themes: Theme[];
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user