mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 10:34:15 +00:00
Merge remote-tracking branch 'remotes/origin/master' into shibboleth
# Conflicts: # src/app/shared/shared.module.ts
This commit is contained in:
@@ -22,6 +22,11 @@ before_install:
|
|||||||
- sudo mv docker-compose /usr/local/bin
|
- sudo mv docker-compose /usr/local/bin
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
# update chrome
|
||||||
|
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||||||
|
- sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list'
|
||||||
|
- sudo apt-get update
|
||||||
|
- sudo apt-get install google-chrome-stable
|
||||||
# Start up DSpace 7 using the entities database dump
|
# Start up DSpace 7 using the entities database dump
|
||||||
- docker-compose -f ./docker/docker-compose-travis.yml up -d
|
- docker-compose -f ./docker/docker-compose-travis.yml up -d
|
||||||
# Use the dspace-cli image to populate the assetstore. Trigger a discovery and oai update
|
# Use the dspace-cli image to populate the assetstore. Trigger a discovery and oai update
|
||||||
|
@@ -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',
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,7 @@ describe('protractor App', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should contain a news section', () => {
|
it('should contain a news section', () => {
|
||||||
page.navigateTo();
|
page.navigateTo()
|
||||||
expect<any>(page.getHomePageNewsText()).toBeDefined();
|
.then(() => expect<any>(page.getHomePageNewsText()).toBeDefined());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -11,6 +11,6 @@ export class ProtractorPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getHomePageNewsText() {
|
getHomePageNewsText() {
|
||||||
return element(by.xpath('//ds-home-news')).getText();
|
return element(by.css('ds-home-news')).getText();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,33 +11,36 @@ describe('protractor SearchPage', () => {
|
|||||||
|
|
||||||
it('should contain query value when navigating to page with query parameter', () => {
|
it('should contain query value when navigating to page with query parameter', () => {
|
||||||
const queryString = 'Interesting query string';
|
const queryString = 'Interesting query string';
|
||||||
page.navigateToSearchWithQueryParameter(queryString);
|
page.navigateToSearchWithQueryParameter(queryString)
|
||||||
page.getCurrentQuery().then((query: string) => {
|
.then(() => page.getCurrentQuery())
|
||||||
expect<string>(query).toEqual(queryString);
|
.then((query: string) => {
|
||||||
});
|
expect<string>(query).toEqual(queryString);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have right scope selected when navigating to page with scope parameter', () => {
|
it('should have right scope selected when navigating to page with scope parameter', () => {
|
||||||
const scope: promise.Promise<string> = page.getRandomScopeOption();
|
page.navigateToSearch()
|
||||||
scope.then((scopeString: string) => {
|
.then(() => page.getRandomScopeOption())
|
||||||
page.navigateToSearchWithScopeParameter(scopeString);
|
.then((scopeString: string) => {
|
||||||
page.getCurrentScope().then((s: string) => {
|
page.navigateToSearchWithScopeParameter(scopeString);
|
||||||
expect<string>(s).toEqual(scopeString);
|
page.getCurrentScope().then((s: string) => {
|
||||||
|
expect<string>(s).toEqual(scopeString);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect to the correct url when scope was set and submit button was triggered', () => {
|
it('should redirect to the correct url when scope was set and submit button was triggered', () => {
|
||||||
const scope: promise.Promise<string> = page.getRandomScopeOption();
|
page.navigateToSearch()
|
||||||
scope.then((scopeString: string) => {
|
.then(() => page.getRandomScopeOption())
|
||||||
page.setCurrentScope(scopeString);
|
.then((scopeString: string) => {
|
||||||
page.submitSearchForm();
|
page.setCurrentScope(scopeString);
|
||||||
browser.wait(() => {
|
page.submitSearchForm();
|
||||||
return browser.getCurrentUrl().then((url: string) => {
|
browser.wait(() => {
|
||||||
return url.indexOf('scope=' + encodeURI(scopeString)) !== -1;
|
return browser.getCurrentUrl().then((url: string) => {
|
||||||
|
return url.indexOf('scope=' + encodeURI(scopeString)) !== -1;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect to the correct url when query was set and submit button was triggered', () => {
|
it('should redirect to the correct url when query was set and submit button was triggered', () => {
|
||||||
|
@@ -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",
|
||||||
@@ -532,6 +570,8 @@
|
|||||||
|
|
||||||
"error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.",
|
"error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.",
|
||||||
|
|
||||||
|
"error.validation.filerequired": "The file upload is mandatory",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"footer.copyright": "copyright © 2002-{{ year }}",
|
"footer.copyright": "copyright © 2002-{{ year }}",
|
||||||
@@ -813,7 +853,7 @@
|
|||||||
|
|
||||||
"item.edit.tabs.relationships.head": "Item Relationships",
|
"item.edit.tabs.relationships.head": "Item Relationships",
|
||||||
|
|
||||||
"item.edit.tabs.relationships.title": "Item Edit - Relationships",
|
"item.edit.tabs.relationships.title": "Item Edit - Relationships",
|
||||||
|
|
||||||
"item.edit.tabs.status.buttons.authorizations.button": "Authorizations...",
|
"item.edit.tabs.status.buttons.authorizations.button": "Authorizations...",
|
||||||
|
|
||||||
@@ -997,6 +1037,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...",
|
||||||
@@ -1425,6 +1467,8 @@
|
|||||||
|
|
||||||
"relationships.isVolumeOf": "Journal Volumes",
|
"relationships.isVolumeOf": "Journal Volumes",
|
||||||
|
|
||||||
|
"relationships.isContributorOf": "Contributors",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"search.description": "",
|
"search.description": "",
|
||||||
@@ -1627,8 +1671,67 @@
|
|||||||
"submission.general.save-later": "Save for later",
|
"submission.general.save-later": "Save for later",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.close": "Close",
|
"submission.sections.describe.relationship-lookup.close": "Close",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.added": "Successfully added local entry to the selection",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-button-title.Author": "Import remote author",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal": "Import remote journal",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Issue": "Import remote journal issue",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Volume": "Import remote journal volume",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.Author.title": "Import Remote Author",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.Author.added.local-entity": "Successfully added local author to the selection",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.Author.added.new-entity": "Successfully imported and added external author to the selection",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Authority",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.authority.new": "Import as a new local authority entry",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.cancel": "Cancel",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.collection": "Select a collection to import new entries to",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.entities": "Entities",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.entities.new": "Import as a new local entity",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaPublisher": "Importing from Sherpa Publisher",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.import": "Import",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.title": "Import Remote Journal",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.local-entity": "Successfully added local journal to the selection",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.new-entity": "Successfully imported and added external journal to the selection",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.title": "Import Remote Journal Issue",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.local-entity": "Successfully added local journal issue to the selection",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.new-entity": "Successfully imported and added external journal issue to the selection",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.title": "Import Remote Journal Volume",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.local-entity": "Successfully added local journal volume to the selection",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.new-entity": "Successfully imported and added external journal volume to the selection",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.external-source.import-modal.select": "Select a local match:",
|
||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Deselect all",
|
"submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Deselect all",
|
||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Deselect page",
|
"submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Deselect page",
|
||||||
@@ -1875,11 +1978,16 @@
|
|||||||
|
|
||||||
"uploader.drag-message": "Drag & Drop your files here",
|
"uploader.drag-message": "Drag & Drop your files here",
|
||||||
|
|
||||||
"uploader.or": ", or ",
|
"uploader.or": ", or",
|
||||||
|
|
||||||
"uploader.processing": "Processing",
|
"uploader.processing": "Processing",
|
||||||
|
|
||||||
"uploader.queue-length": "Queue length",
|
"uploader.queue-length": "Queue length",
|
||||||
|
|
||||||
|
"virtual-metadata.delete-item.info": "Select the types for which you want to save the virtual metadata as real metadata",
|
||||||
|
|
||||||
|
"virtual-metadata.delete-item.modal-head": "The virtual metadata of this relation",
|
||||||
|
|
||||||
|
"virtual-metadata.delete-relationship.modal-head": "Select the items for which you want to save the virtual metadata as real metadata",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@ import { ItemRelationshipsComponent } from './item-relationships/item-relationsh
|
|||||||
import { EditRelationshipComponent } from './item-relationships/edit-relationship/edit-relationship.component';
|
import { EditRelationshipComponent } from './item-relationships/edit-relationship/edit-relationship.component';
|
||||||
import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
|
import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
|
||||||
import { ItemMoveComponent } from './item-move/item-move.component';
|
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||||
|
import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module that contains all components related to the Edit Item page administrator functionality
|
* Module that contains all components related to the Edit Item page administrator functionality
|
||||||
@@ -51,6 +52,7 @@ import { ItemMoveComponent } from './item-move/item-move.component';
|
|||||||
EditRelationshipListComponent,
|
EditRelationshipListComponent,
|
||||||
ItemCollectionMapperComponent,
|
ItemCollectionMapperComponent,
|
||||||
ItemMoveComponent,
|
ItemMoveComponent,
|
||||||
|
VirtualMetadataComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class EditItemPageModule {
|
export class EditItemPageModule {
|
||||||
|
@@ -0,0 +1,98 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
|
||||||
|
<h2>{{headerMessage | translate: {id: item.handle} }}</h2>
|
||||||
|
<p>{{descriptionMessage | translate}}</p>
|
||||||
|
<ds-modify-item-overview [item]="item"></ds-modify-item-overview>
|
||||||
|
|
||||||
|
<ng-container *ngVar="(types$ | async) as types">
|
||||||
|
|
||||||
|
<div *ngIf="types && types.length > 0" class="mb-4">
|
||||||
|
|
||||||
|
{{'virtual-metadata.delete-item.info' | translate}}
|
||||||
|
|
||||||
|
<div *ngFor="let type of types" class="mb-4">
|
||||||
|
|
||||||
|
<div *ngVar="(isSelected(type) | async) as selected"
|
||||||
|
class="d-flex flex-row">
|
||||||
|
|
||||||
|
<div class="m-2" (click)="setSelected(type, !selected)">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" [checked]="selected">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-column flex-grow-1">
|
||||||
|
<h5 (click)="setSelected(type, !selected)">
|
||||||
|
{{getRelationshipMessageKey(getLabel(type) | async) | translate}}
|
||||||
|
</h5>
|
||||||
|
<div *ngFor="let relationship of (getRelationships(type) | async)"
|
||||||
|
class="d-flex flex-row">
|
||||||
|
<ng-container *ngVar="(getRelatedItem(relationship) | async) as relatedItem">
|
||||||
|
|
||||||
|
<ds-listable-object-component-loader
|
||||||
|
*ngIf="relatedItem"
|
||||||
|
[object]="relatedItem"
|
||||||
|
[viewMode]="viewMode">
|
||||||
|
</ds-listable-object-component-loader>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-outline-info btn-sm"
|
||||||
|
(click)="openVirtualMetadataModal(virtualMetadataModal)">
|
||||||
|
<i class="fas fa-info fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #virtualMetadataModal>
|
||||||
|
<div>
|
||||||
|
<div class="modal-header">
|
||||||
|
{{'virtual-metadata.delete-item.modal-head' | translate}}
|
||||||
|
<button type="button" class="close"
|
||||||
|
(click)="closeVirtualMetadataModal()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<ds-listable-object-component-loader
|
||||||
|
*ngIf="relatedItem"
|
||||||
|
[object]="relatedItem"
|
||||||
|
[viewMode]="viewMode">
|
||||||
|
</ds-listable-object-component-loader>
|
||||||
|
<div *ngFor="let metadata of (getVirtualMetadata(relationship) | async)">
|
||||||
|
<div>
|
||||||
|
<div class="font-weight-bold">
|
||||||
|
{{metadata.metadataField}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{metadata.metadataValue.value}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<button (click)="performAction()"
|
||||||
|
class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
|
||||||
|
</button>
|
||||||
|
<button [routerLink]="['/items/', item.id, 'edit']" class="btn btn-outline-secondary cancel">
|
||||||
|
{{cancelMessage| translate}}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -1,44 +1,132 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
||||||
import { RouterStub } from '../../../shared/testing/router-stub';
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
import { of as observableOf } from 'rxjs';
|
import {Item} from '../../../core/shared/item.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import {RouterStub} from '../../../shared/testing/router-stub';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
import {of as observableOf} from 'rxjs';
|
||||||
import { CommonModule } from '@angular/common';
|
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
||||||
import { FormsModule } from '@angular/forms';
|
import {CommonModule} from '@angular/common';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import {FormsModule} from '@angular/forms';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import {ItemDataService} from '../../../core/data/item-data.service';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||||
import { By } from '@angular/platform-browser';
|
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||||
import { ItemDeleteComponent } from './item-delete.component';
|
import {By} from '@angular/platform-browser';
|
||||||
import { getItemEditPath } from '../../item-page-routing.module';
|
import {ItemDeleteComponent} from './item-delete.component';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import {getItemEditPath} from '../../item-page-routing.module';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../../shared/testing/utils';
|
import {createSuccessfulRemoteDataObject} from '../../../shared/testing/utils';
|
||||||
|
import {VarDirective} from '../../../shared/utils/var.directive';
|
||||||
|
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
import {RelationshipService} from '../../../core/data/relationship.service';
|
||||||
|
import {RelationshipType} from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import {RemoteData} from '../../../core/data/remote-data';
|
||||||
|
import {PaginatedList} from '../../../core/data/paginated-list';
|
||||||
|
import {PageInfo} from '../../../core/shared/page-info.model';
|
||||||
|
import {EntityTypeService} from '../../../core/data/entity-type.service';
|
||||||
|
|
||||||
let comp: ItemDeleteComponent;
|
let comp: ItemDeleteComponent;
|
||||||
let fixture: ComponentFixture<ItemDeleteComponent>;
|
let fixture: ComponentFixture<ItemDeleteComponent>;
|
||||||
|
|
||||||
let mockItem;
|
let mockItem;
|
||||||
|
let itemType;
|
||||||
|
let type1;
|
||||||
|
let type2;
|
||||||
|
let types;
|
||||||
|
let relationships;
|
||||||
let itemPageUrl;
|
let itemPageUrl;
|
||||||
let routerStub;
|
let routerStub;
|
||||||
let mockItemDataService: ItemDataService;
|
let mockItemDataService: ItemDataService;
|
||||||
let routeStub;
|
let routeStub;
|
||||||
|
let objectUpdatesServiceStub;
|
||||||
|
let relationshipService;
|
||||||
|
let entityTypeService;
|
||||||
let notificationsServiceStub;
|
let notificationsServiceStub;
|
||||||
|
let typesSelection;
|
||||||
|
|
||||||
describe('ItemDeleteComponent', () => {
|
describe('ItemDeleteComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
|
||||||
mockItem = Object.assign(new Item(), {
|
mockItem = Object.assign(new Item(), {
|
||||||
id: 'fake-id',
|
id: 'fake-id',
|
||||||
|
uuid: 'fake-uuid',
|
||||||
handle: 'fake/handle',
|
handle: 'fake/handle',
|
||||||
lastModified: '2018',
|
lastModified: '2018',
|
||||||
isWithdrawn: true
|
isWithdrawn: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
itemType = Object.assign(new ItemType(), {
|
||||||
|
id: 'itemType',
|
||||||
|
uuid: 'itemType',
|
||||||
|
});
|
||||||
|
|
||||||
|
type1 = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
uuid: 'type-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
type2 = Object.assign(new RelationshipType(), {
|
||||||
|
id: '2',
|
||||||
|
uuid: 'type-2',
|
||||||
|
});
|
||||||
|
|
||||||
|
types = [type1, type2];
|
||||||
|
|
||||||
|
relationships = [
|
||||||
|
Object.assign(new Relationship(), {
|
||||||
|
id: '1',
|
||||||
|
uuid: 'relationship-1',
|
||||||
|
relationshipType: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
type1
|
||||||
|
)),
|
||||||
|
leftItem: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
mockItem,
|
||||||
|
)),
|
||||||
|
rightItem: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
Object.assign(new Item(), {})
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
Object.assign(new Relationship(), {
|
||||||
|
id: '2',
|
||||||
|
uuid: 'relationship-2',
|
||||||
|
relationshipType: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
type2
|
||||||
|
)),
|
||||||
|
leftItem: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
mockItem,
|
||||||
|
)),
|
||||||
|
rightItem: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
Object.assign(new Item(), {})
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
itemPageUrl = `fake-url/${mockItem.id}`;
|
itemPageUrl = `fake-url/${mockItem.id}`;
|
||||||
routerStub = Object.assign(new RouterStub(), {
|
routerStub = Object.assign(new RouterStub(), {
|
||||||
url: `${itemPageUrl}/edit`
|
url: `${itemPageUrl}/edit`
|
||||||
@@ -54,16 +142,56 @@ describe('ItemDeleteComponent', () => {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typesSelection = {
|
||||||
|
type1: false,
|
||||||
|
type2: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
entityTypeService = jasmine.createSpyObj('entityTypeService',
|
||||||
|
{
|
||||||
|
getEntityTypeByLabel: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
itemType,
|
||||||
|
)),
|
||||||
|
getEntityTypeRelationships: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
new PaginatedList(new PageInfo(), types),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
objectUpdatesServiceStub = {
|
||||||
|
initialize: () => {
|
||||||
|
// do nothing
|
||||||
|
},
|
||||||
|
isSelectedVirtualMetadata: (type) => observableOf(typesSelection[type]),
|
||||||
|
};
|
||||||
|
|
||||||
|
relationshipService = jasmine.createSpyObj('relationshipService',
|
||||||
|
{
|
||||||
|
getItemRelationshipsArray: observableOf(relationships),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
notificationsServiceStub = new NotificationsServiceStub();
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
declarations: [ItemDeleteComponent],
|
declarations: [ItemDeleteComponent, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: routeStub },
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
{ provide: Router, useValue: routerStub },
|
{ provide: Router, useValue: routerStub },
|
||||||
{ provide: ItemDataService, useValue: mockItemDataService },
|
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||||
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesServiceStub },
|
||||||
|
{ provide: RelationshipService, useValue: relationshipService },
|
||||||
|
{ provide: EntityTypeService, useValue: entityTypeService },
|
||||||
], schemas: [
|
], schemas: [
|
||||||
CUSTOM_ELEMENTS_SCHEMA
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
]
|
]
|
||||||
@@ -91,7 +219,8 @@ describe('ItemDeleteComponent', () => {
|
|||||||
it('should call delete function from the ItemDataService', () => {
|
it('should call delete function from the ItemDataService', () => {
|
||||||
spyOn(comp, 'notify');
|
spyOn(comp, 'notify');
|
||||||
comp.performAction();
|
comp.performAction();
|
||||||
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem);
|
expect(mockItemDataService.delete)
|
||||||
|
.toHaveBeenCalledWith(mockItem, types.filter((type) => typesSelection[type]).map((type) => type.id));
|
||||||
expect(comp.notify).toHaveBeenCalled();
|
expect(comp.notify).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,29 +1,323 @@
|
|||||||
import { Component } from '@angular/core';
|
import {Component, Input, OnInit} from '@angular/core';
|
||||||
import { first } from 'rxjs/operators';
|
import {filter, first, map, switchMap, take} from 'rxjs/operators';
|
||||||
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
import { getItemEditPath } from '../../item-page-routing.module';
|
import {getItemEditPath} from '../../item-page-routing.module';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {combineLatest as observableCombineLatest, combineLatest, Observable} from 'rxjs';
|
||||||
|
import {RelationshipType} from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import {VirtualMetadata} from '../virtual-metadata/virtual-metadata.component';
|
||||||
|
import {Relationship} from '../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import {getRemoteDataPayload, getSucceededRemoteData} from '../../../core/shared/operators';
|
||||||
|
import {hasValue, isNotEmpty} from '../../../shared/empty.util';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
import {MetadataValue} from '../../../core/shared/metadata.models';
|
||||||
|
import {ViewMode} from '../../../core/shared/view-mode.model';
|
||||||
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
|
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||||
|
import {ItemDataService} from '../../../core/data/item-data.service';
|
||||||
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
|
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
import {RelationshipService} from '../../../core/data/relationship.service';
|
||||||
|
import {EntityTypeService} from '../../../core/data/entity-type.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-delete',
|
selector: 'ds-item-delete',
|
||||||
templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
|
templateUrl: '../item-delete/item-delete.component.html'
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Component responsible for rendering the item delete page
|
* Component responsible for rendering the item delete page
|
||||||
*/
|
*/
|
||||||
export class ItemDeleteComponent extends AbstractSimpleItemActionComponent {
|
export class ItemDeleteComponent
|
||||||
|
extends AbstractSimpleItemActionComponent
|
||||||
|
implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current url of this page
|
||||||
|
*/
|
||||||
|
@Input() url: string;
|
||||||
|
|
||||||
protected messageKey = 'delete';
|
protected messageKey = 'delete';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform the delete action to the item
|
* The view-mode we're currently on
|
||||||
|
*/
|
||||||
|
viewMode = ViewMode.ListElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of the relationship types for which this item has relations as an observable.
|
||||||
|
* The list doesn't contain duplicates.
|
||||||
|
*/
|
||||||
|
types$: Observable<RelationshipType[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map which stores the relationships of this item for each type as observable lists
|
||||||
|
*/
|
||||||
|
relationships$: Map<RelationshipType, Observable<Relationship[]>>
|
||||||
|
= new Map<RelationshipType, Observable<Relationship[]>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map which stores the related item of each relationship of this item as an observable
|
||||||
|
*/
|
||||||
|
relatedItems$: Map<Relationship, Observable<Item>> = new Map<Relationship, Observable<Item>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map which stores the virtual metadata (of the related) item corresponding to each relationship of this item
|
||||||
|
* as an observable list
|
||||||
|
*/
|
||||||
|
virtualMetadata$: Map<Relationship, Observable<VirtualMetadata[]>> = new Map<Relationship, Observable<VirtualMetadata[]>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to NgbModal
|
||||||
|
*/
|
||||||
|
public modalRef: NgbModalRef;
|
||||||
|
|
||||||
|
constructor(protected route: ActivatedRoute,
|
||||||
|
protected router: Router,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected itemDataService: ItemDataService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
protected modalService: NgbModal,
|
||||||
|
protected objectUpdatesService: ObjectUpdatesService,
|
||||||
|
protected relationshipService: RelationshipService,
|
||||||
|
protected entityTypeService: EntityTypeService,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
route,
|
||||||
|
router,
|
||||||
|
notificationsService,
|
||||||
|
itemDataService,
|
||||||
|
translateService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up and initialize all fields
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
|
||||||
|
super.ngOnInit();
|
||||||
|
this.url = this.router.url;
|
||||||
|
|
||||||
|
this.types$ = this.entityTypeService.getEntityTypeByLabel(
|
||||||
|
this.item.firstMetadataValue('relationship.type')
|
||||||
|
).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
switchMap((entityType) => this.entityTypeService.getEntityTypeRelationships(entityType.id)),
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((relationshipTypes) => relationshipTypes.page),
|
||||||
|
switchMap((types) =>
|
||||||
|
combineLatest(types.map((type) => this.getRelationships(type))).pipe(
|
||||||
|
map((relationships) =>
|
||||||
|
types.reduce<RelationshipType[]>((includedTypes, type, index) => {
|
||||||
|
if (!includedTypes.some((includedType) => includedType.id === type.id)
|
||||||
|
&& !(relationships[index].length === 0)) {
|
||||||
|
return [...includedTypes, type];
|
||||||
|
} else {
|
||||||
|
return includedTypes;
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.types$.pipe(
|
||||||
|
take(1),
|
||||||
|
).subscribe((types) =>
|
||||||
|
this.objectUpdatesService.initialize(this.url, types, this.item.lastModified)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the modal which lists the virtual metadata of a relation
|
||||||
|
* @param content the html content of the modal
|
||||||
|
*/
|
||||||
|
openVirtualMetadataModal(content: any) {
|
||||||
|
this.modalRef = this.modalService.open(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal which lists the virtual metadata of a relation
|
||||||
|
*/
|
||||||
|
closeVirtualMetadataModal() {
|
||||||
|
this.modalRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the i18n message key for a relationship
|
||||||
|
* @param label The relationship type's label
|
||||||
|
*/
|
||||||
|
getRelationshipMessageKey(label: string): string {
|
||||||
|
if (hasValue(label) && label.indexOf('Of') > -1) {
|
||||||
|
return `relationships.${label.substring(0, label.indexOf('Of') + 2)}`
|
||||||
|
} else {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the relationship type label relevant for this item as an observable
|
||||||
|
* @param relationshipType the relationship type to get the label for
|
||||||
|
*/
|
||||||
|
getLabel(relationshipType: RelationshipType): Observable<string> {
|
||||||
|
|
||||||
|
return this.getRelationships(relationshipType).pipe(
|
||||||
|
switchMap((relationships) =>
|
||||||
|
this.isLeftItem(relationships[0]).pipe(
|
||||||
|
map((isLeftItem) => isLeftItem ? relationshipType.leftwardType : relationshipType.rightwardType),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the relationships of this item with a given type as an observable
|
||||||
|
* @param relationshipType the relationship type to filter the item's relationships on
|
||||||
|
*/
|
||||||
|
getRelationships(relationshipType: RelationshipType): Observable<Relationship[]> {
|
||||||
|
|
||||||
|
if (!this.relationships$.has(relationshipType)) {
|
||||||
|
this.relationships$.set(
|
||||||
|
relationshipType,
|
||||||
|
this.relationshipService.getItemRelationshipsArray(this.item).pipe(
|
||||||
|
// filter on type
|
||||||
|
switchMap((relationships) =>
|
||||||
|
observableCombineLatest(
|
||||||
|
relationships.map((relationship) => this.getRelationshipType(relationship))
|
||||||
|
).pipe(
|
||||||
|
map((types) => relationships.filter(
|
||||||
|
(relationship, index) => relationshipType.id === types[index].id
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.relationships$.get(relationshipType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type of a given relationship as an observable
|
||||||
|
* @param relationship the relationship to get the type for
|
||||||
|
*/
|
||||||
|
private getRelationshipType(relationship: Relationship): Observable<RelationshipType> {
|
||||||
|
|
||||||
|
return relationship.relationshipType.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
filter((relationshipType: RelationshipType) => hasValue(relationshipType) && isNotEmpty(relationshipType.uuid))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the item this item is related to through a given relationship as an observable
|
||||||
|
* @param relationship the relationship to get the other item for
|
||||||
|
*/
|
||||||
|
getRelatedItem(relationship: Relationship): Observable<Item> {
|
||||||
|
|
||||||
|
if (!this.relatedItems$.has(relationship)) {
|
||||||
|
|
||||||
|
this.relatedItems$.set(
|
||||||
|
relationship,
|
||||||
|
this.isLeftItem(relationship).pipe(
|
||||||
|
switchMap((isLeftItem) => isLeftItem ? relationship.rightItem : relationship.leftItem),
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.relatedItems$.get(relationship);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the virtual metadata for a given relationship of the related item.
|
||||||
|
* @param relationship the relationship to get the virtual metadata for
|
||||||
|
*/
|
||||||
|
getVirtualMetadata(relationship: Relationship): Observable<VirtualMetadata[]> {
|
||||||
|
|
||||||
|
if (!this.virtualMetadata$.has(relationship)) {
|
||||||
|
|
||||||
|
this.virtualMetadata$.set(
|
||||||
|
relationship,
|
||||||
|
this.getRelatedItem(relationship).pipe(
|
||||||
|
map((relatedItem) =>
|
||||||
|
Object.entries(relatedItem.metadata)
|
||||||
|
.map(([key, value]) => value
|
||||||
|
.filter((metadata: MetadataValue) =>
|
||||||
|
metadata.authority && metadata.authority.endsWith(relationship.id))
|
||||||
|
.map((metadata: MetadataValue) => {
|
||||||
|
return {
|
||||||
|
metadataField: key,
|
||||||
|
metadataValue: metadata,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.reduce((previous, current) => previous.concat(current))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.virtualMetadata$.get(relationship);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether this item is the left item of a given relationship, as an observable boolean
|
||||||
|
* @param relationship the relationship for which to check whether this item is the left item
|
||||||
|
*/
|
||||||
|
private isLeftItem(relationship: Relationship): Observable<boolean> {
|
||||||
|
|
||||||
|
return relationship.leftItem.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)),
|
||||||
|
map((leftItem) => leftItem.uuid === this.item.uuid)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a given relationship type is selected to save the corresponding virtual metadata
|
||||||
|
* @param type the relationship type for which to check whether it is selected
|
||||||
|
*/
|
||||||
|
isSelected(type: RelationshipType): Observable<boolean> {
|
||||||
|
return this.objectUpdatesService.isSelectedVirtualMetadata(this.url, this.item.uuid, type.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select/deselect a given relationship type to save the corresponding virtual metadata
|
||||||
|
* @param type the relationship type to select/deselect
|
||||||
|
* @param selected whether the type should be selected
|
||||||
|
*/
|
||||||
|
setSelected(type: RelationshipType, selected: boolean): void {
|
||||||
|
this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.item.uuid, type.uuid, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the delete operation
|
||||||
*/
|
*/
|
||||||
performAction() {
|
performAction() {
|
||||||
this.itemDataService.delete(this.item).pipe(first()).subscribe(
|
|
||||||
(succeeded: boolean) => {
|
this.types$.pipe(
|
||||||
this.notify(succeeded);
|
switchMap((types) =>
|
||||||
}
|
combineLatest(
|
||||||
);
|
types.map((type) => this.isSelected(type))
|
||||||
|
).pipe(
|
||||||
|
map((selection) => types.filter(
|
||||||
|
(type, index) => selection[index]
|
||||||
|
)),
|
||||||
|
map((selectedTypes) => selectedTypes.map((type) => type.id)),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
).subscribe((types) => {
|
||||||
|
this.itemDataService.delete(this.item, types).pipe(first()).subscribe(
|
||||||
|
(succeeded: boolean) => {
|
||||||
|
this.notify(succeeded);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,15 +1,15 @@
|
|||||||
<ng-container *ngVar="(updates$ | async) as updates">
|
<h5>{{getRelationshipMessageKey() | async | translate}}</h5>
|
||||||
<div *ngIf="updates">
|
<ng-container *ngVar="updates$ | async as updates">
|
||||||
<h5>{{getRelationshipMessageKey(relationshipLabel) | translate}}</h5>
|
<ng-container *ngIf="updates">
|
||||||
<ng-container *ngVar="(updates | dsObjectValues) as updateValues">
|
<ng-container *ngVar="updates | dsObjectValues as updateValues">
|
||||||
<div *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
<ds-edit-relationship *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
||||||
ds-edit-relationship
|
class="relationship-row d-block"
|
||||||
class="relationship-row d-block"
|
[fieldUpdate]="updateValue"
|
||||||
[fieldUpdate]="updateValue || {}"
|
[url]="url"
|
||||||
[url]="url"
|
[editItem]="item"
|
||||||
[ngClass]="{'alert alert-danger': updateValue.changeType === 2}">
|
[ngClass]="{'alert alert-danger': updateValue?.changeType === 2}">
|
||||||
</div>
|
</ds-edit-relationship>
|
||||||
<ds-loading *ngIf="updateValues.length == 0" message="{{'loading.items' | translate}}"></ds-loading>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
<div *ngIf="!updates">no relationships</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -1,27 +1,26 @@
|
|||||||
import { EditRelationshipListComponent } from './edit-relationship-list.component';
|
import {EditRelationshipListComponent} from './edit-relationship-list.component';
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
import {RelationshipType} from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
import { ResourceType } from '../../../../core/shared/resource-type';
|
import {Relationship} from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
import {of as observableOf} from 'rxjs/internal/observable/of';
|
||||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
import {RemoteData} from '../../../../core/data/remote-data';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import {Item} from '../../../../core/shared/item.model';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import {PaginatedList} from '../../../../core/data/paginated-list';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
import {PageInfo} from '../../../../core/shared/page-info.model';
|
||||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
import {FieldChangeType} from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
import {SharedModule} from '../../../../shared/shared.module';
|
||||||
import { SharedModule } from '../../../../shared/shared.module';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import {ObjectUpdatesService} from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
import {DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';
|
||||||
import { RelationshipService } from '../../../../core/data/relationship.service';
|
import {By} from '@angular/platform-browser';
|
||||||
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
import {ItemType} from '../../../../core/shared/item-relationships/item-type.model';
|
||||||
import { By } from '@angular/platform-browser';
|
|
||||||
|
|
||||||
let comp: EditRelationshipListComponent;
|
let comp: EditRelationshipListComponent;
|
||||||
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
||||||
let de: DebugElement;
|
let de: DebugElement;
|
||||||
|
|
||||||
let objectUpdatesService;
|
let objectUpdatesService;
|
||||||
let relationshipService;
|
let entityTypeService;
|
||||||
|
|
||||||
const url = 'http://test-url.com/test-url';
|
const url = 'http://test-url.com/test-url';
|
||||||
|
|
||||||
@@ -30,42 +29,66 @@ let author1;
|
|||||||
let author2;
|
let author2;
|
||||||
let fieldUpdate1;
|
let fieldUpdate1;
|
||||||
let fieldUpdate2;
|
let fieldUpdate2;
|
||||||
let relationships;
|
let relationship1;
|
||||||
|
let relationship2;
|
||||||
let relationshipType;
|
let relationshipType;
|
||||||
|
let entityType;
|
||||||
|
let relatedEntityType;
|
||||||
|
|
||||||
describe('EditRelationshipListComponent', () => {
|
describe('EditRelationshipListComponent', () => {
|
||||||
beforeEach(async(() => {
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
entityType = Object.assign(new ItemType(), {
|
||||||
|
id: 'entityType',
|
||||||
|
});
|
||||||
|
|
||||||
|
relatedEntityType = Object.assign(new ItemType(), {
|
||||||
|
id: 'relatedEntityType',
|
||||||
|
});
|
||||||
|
|
||||||
relationshipType = Object.assign(new RelationshipType(), {
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
id: '1',
|
id: '1',
|
||||||
uuid: '1',
|
uuid: '1',
|
||||||
leftwardType: 'isAuthorOfPublication',
|
leftwardType: 'isAuthorOfPublication',
|
||||||
rightwardType: 'isPublicationOfAuthor'
|
rightwardType: 'isPublicationOfAuthor',
|
||||||
|
leftType: observableOf(new RemoteData(false, false, true, undefined, entityType)),
|
||||||
|
rightType: observableOf(new RemoteData(false, false, true, undefined, relatedEntityType)),
|
||||||
});
|
});
|
||||||
|
|
||||||
relationships = [
|
relationship1 = Object.assign(new Relationship(), {
|
||||||
Object.assign(new Relationship(), {
|
self: url + '/2',
|
||||||
self: url + '/2',
|
id: '2',
|
||||||
id: '2',
|
uuid: '2',
|
||||||
uuid: '2',
|
leftId: 'author1',
|
||||||
leftId: 'author1',
|
rightId: 'publication',
|
||||||
rightId: 'publication',
|
leftItem: observableOf(new RemoteData(false, false, true, undefined, item)),
|
||||||
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
rightItem: observableOf(new RemoteData(false, false, true, undefined, author1)),
|
||||||
}),
|
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
||||||
Object.assign(new Relationship(), {
|
});
|
||||||
self: url + '/3',
|
|
||||||
id: '3',
|
relationship2 = Object.assign(new Relationship(), {
|
||||||
uuid: '3',
|
self: url + '/3',
|
||||||
leftId: 'author2',
|
id: '3',
|
||||||
rightId: 'publication',
|
uuid: '3',
|
||||||
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
leftId: 'author2',
|
||||||
})
|
rightId: 'publication',
|
||||||
];
|
leftItem: observableOf(new RemoteData(false, false, true, undefined, item)),
|
||||||
|
rightItem: observableOf(new RemoteData(false, false, true, undefined, author2)),
|
||||||
|
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
||||||
|
});
|
||||||
|
|
||||||
item = Object.assign(new Item(), {
|
item = Object.assign(new Item(), {
|
||||||
self: 'fake-item-url/publication',
|
self: 'fake-item-url/publication',
|
||||||
id: 'publication',
|
id: 'publication',
|
||||||
uuid: 'publication',
|
uuid: 'publication',
|
||||||
relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships)))
|
relationships: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
new PaginatedList(new PageInfo(), [relationship1, relationship2])
|
||||||
|
))
|
||||||
});
|
});
|
||||||
|
|
||||||
author1 = Object.assign(new Item(), {
|
author1 = Object.assign(new Item(), {
|
||||||
@@ -88,16 +111,29 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
|
|
||||||
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
||||||
{
|
{
|
||||||
getFieldUpdatesExclusive: observableOf({
|
getFieldUpdates: observableOf({
|
||||||
[author1.uuid]: fieldUpdate1,
|
[author1.uuid]: fieldUpdate1,
|
||||||
[author2.uuid]: fieldUpdate2
|
[author2.uuid]: fieldUpdate2
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
relationshipService = jasmine.createSpyObj('relationshipService',
|
entityTypeService = jasmine.createSpyObj('entityTypeService',
|
||||||
{
|
{
|
||||||
getRelatedItemsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [author1, author2]))),
|
getEntityTypeByLabel: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
entityType,
|
||||||
|
)),
|
||||||
|
getEntityTypeRelationships: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
new PaginatedList(new PageInfo(), [relationshipType]),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -106,29 +142,27 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
declarations: [EditRelationshipListComponent],
|
declarations: [EditRelationshipListComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
{ provide: RelationshipService, useValue: relationshipService }
|
|
||||||
], schemas: [
|
], schemas: [
|
||||||
NO_ERRORS_SCHEMA
|
NO_ERRORS_SCHEMA
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(EditRelationshipListComponent);
|
fixture = TestBed.createComponent(EditRelationshipListComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
de = fixture.debugElement;
|
de = fixture.debugElement;
|
||||||
|
|
||||||
comp.item = item;
|
comp.item = item;
|
||||||
|
comp.itemType = entityType;
|
||||||
comp.url = url;
|
comp.url = url;
|
||||||
comp.relationshipLabel = relationshipType.leftwardType;
|
comp.relationshipType = relationshipType;
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('changeType is REMOVE', () => {
|
describe('changeType is REMOVE', () => {
|
||||||
beforeEach(() => {
|
|
||||||
fieldUpdate1.changeType = FieldChangeType.REMOVE;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
it('the div should have class alert-danger', () => {
|
it('the div should have class alert-danger', () => {
|
||||||
|
|
||||||
|
fieldUpdate1.changeType = FieldChangeType.REMOVE;
|
||||||
const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement;
|
const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement;
|
||||||
expect(element.classList).toContain('alert-danger');
|
expect(element.classList).toContain('alert-danger');
|
||||||
});
|
});
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { FieldUpdate, FieldUpdates } from '../../../../core/data/object-updates/object-updates.reducer';
|
import {FieldUpdate, FieldUpdates} from '../../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { RelationshipService } from '../../../../core/data/relationship.service';
|
import {Item} from '../../../../core/shared/item.model';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import {map, switchMap} from 'rxjs/operators';
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
import {hasValue} from '../../../../shared/empty.util';
|
||||||
import { hasValue } from '../../../../shared/empty.util';
|
import {Relationship} from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import {RelationshipType} from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
import {getRemoteDataPayload, getSucceededRemoteData} from '../../../../core/shared/operators';
|
||||||
|
import {combineLatest as observableCombineLatest, combineLatest} from 'rxjs';
|
||||||
|
import {ItemType} from '../../../../core/shared/item-relationships/item-type.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-edit-relationship-list',
|
selector: 'ds-edit-relationship-list',
|
||||||
@@ -18,12 +20,15 @@ import { PaginatedList } from '../../../../core/data/paginated-list';
|
|||||||
* A component creating a list of editable relationships of a certain type
|
* A component creating a list of editable relationships of a certain type
|
||||||
* The relationships are rendered as a list of related items
|
* The relationships are rendered as a list of related items
|
||||||
*/
|
*/
|
||||||
export class EditRelationshipListComponent implements OnInit, OnChanges {
|
export class EditRelationshipListComponent implements OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The item to display related items for
|
* The item to display related items for
|
||||||
*/
|
*/
|
||||||
@Input() item: Item;
|
@Input() item: Item;
|
||||||
|
|
||||||
|
@Input() itemType: ItemType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URL to the current page
|
* The URL to the current page
|
||||||
* Used to fetch updates for the current item from the store
|
* Used to fetch updates for the current item from the store
|
||||||
@@ -33,7 +38,7 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
|
|||||||
/**
|
/**
|
||||||
* The label of the relationship-type we're rendering a list for
|
* The label of the relationship-type we're rendering a list for
|
||||||
*/
|
*/
|
||||||
@Input() relationshipLabel: string;
|
@Input() relationshipType: RelationshipType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The FieldUpdates for the relationships in question
|
* The FieldUpdates for the relationships in question
|
||||||
@@ -42,53 +47,42 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected objectUpdatesService: ObjectUpdatesService,
|
protected objectUpdatesService: ObjectUpdatesService,
|
||||||
protected relationshipService: RelationshipService
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
/**
|
||||||
this.initUpdates();
|
* Get the i18n message key for this relationship type
|
||||||
}
|
*/
|
||||||
|
public getRelationshipMessageKey(): Observable<string> {
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
return this.getLabel().pipe(
|
||||||
this.initUpdates();
|
map((label) => {
|
||||||
|
if (hasValue(label) && label.indexOf('Of') > -1) {
|
||||||
|
return `relationships.${label.substring(0, label.indexOf('Of') + 2)}`
|
||||||
|
} else {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the FieldUpdates using the related items
|
* Get the relevant label for this relationship type
|
||||||
*/
|
*/
|
||||||
initUpdates() {
|
private getLabel(): Observable<string> {
|
||||||
this.updates$ = this.getUpdatesByLabel(this.relationshipLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return combineLatest([
|
||||||
* Transform the item's relationships of a specific type into related items
|
this.relationshipType.leftType,
|
||||||
* @param label The relationship type's label
|
this.relationshipType.rightType,
|
||||||
*/
|
].map((itemTypeRD) => itemTypeRD.pipe(
|
||||||
public getRelatedItemsByLabel(label: string): Observable<RemoteData<PaginatedList<Item>>> {
|
getSucceededRemoteData(),
|
||||||
return this.relationshipService.getRelatedItemsByLabel(this.item, label);
|
getRemoteDataPayload(),
|
||||||
}
|
))).pipe(
|
||||||
|
map((itemTypes) => [
|
||||||
/**
|
this.relationshipType.leftwardType,
|
||||||
* Get FieldUpdates for the relationships of a specific type
|
this.relationshipType.rightwardType,
|
||||||
* @param label The relationship type's label
|
][itemTypes.findIndex((itemType) => itemType.id === this.itemType.id)]),
|
||||||
*/
|
);
|
||||||
public getUpdatesByLabel(label: string): Observable<FieldUpdates> {
|
|
||||||
return this.getRelatedItemsByLabel(label).pipe(
|
|
||||||
switchMap((itemsRD) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, itemsRD.payload.page))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the i18n message key for a relationship
|
|
||||||
* @param label The relationship type's label
|
|
||||||
*/
|
|
||||||
public getRelationshipMessageKey(label: string): string {
|
|
||||||
if (hasValue(label) && label.indexOf('Of') > -1) {
|
|
||||||
return `relationships.${label.substring(0, label.indexOf('Of') + 2)}`
|
|
||||||
} else {
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,4 +92,26 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
|
|||||||
return update && update.field ? update.field.uuid : undefined;
|
return update && update.field ? update.field.uuid : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.updates$ = this.item.relationships.pipe(
|
||||||
|
map((relationships) => relationships.payload.page.filter((relationship) => relationship)),
|
||||||
|
switchMap((itemRelationships) =>
|
||||||
|
observableCombineLatest(
|
||||||
|
itemRelationships
|
||||||
|
.map((relationship) => relationship.relationshipType.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
))
|
||||||
|
).pipe(
|
||||||
|
map((relationshipTypes) => itemRelationships.filter(
|
||||||
|
(relationship, index) => relationshipTypes[index].id === this.relationshipType.id)
|
||||||
|
),
|
||||||
|
map((relationships) => relationships.map((relationship) =>
|
||||||
|
Object.assign(new Relationship(), relationship, {uuid: relationship.id})
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
switchMap((initialFields) => this.objectUpdatesService.getFieldUpdates(this.url, initialFields)),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
<div class="row" *ngIf="item">
|
<div class="row" *ngIf="relatedItem$ | async">
|
||||||
<div class="col-10 relationship">
|
<div class="col-10 relationship">
|
||||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
<ds-listable-object-component-loader [object]="relatedItem$ | async" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<div class="btn-group relationship-action-buttons">
|
<div class="btn-group relationship-action-buttons">
|
||||||
<button [disabled]="!canRemove()" (click)="remove()"
|
<button [disabled]="!canRemove()" (click)="openVirtualMetadataModal(virtualMetadataModal)"
|
||||||
class="btn btn-outline-danger btn-sm"
|
class="btn btn-outline-danger btn-sm"
|
||||||
title="{{'item.edit.metadata.edit.buttons.remove' | translate}}">
|
title="{{'item.edit.metadata.edit.buttons.remove' | translate}}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
@@ -17,3 +17,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ng-template #virtualMetadataModal>
|
||||||
|
<ds-virtual-metadata
|
||||||
|
[relationshipId]="relationship.id"
|
||||||
|
[leftItem]="leftItem$ | async"
|
||||||
|
[rightItem]="rightItem$ | async"
|
||||||
|
[url]="url"
|
||||||
|
(close)="closeVirtualMetadataModal()"
|
||||||
|
(save)="remove()"
|
||||||
|
>
|
||||||
|
</ds-virtual-metadata>
|
||||||
|
</ng-template>
|
||||||
|
@@ -11,11 +11,13 @@ import { Item } from '../../../../core/shared/item.model';
|
|||||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
let objectUpdatesService: ObjectUpdatesService;
|
let objectUpdatesService;
|
||||||
const url = 'http://test-url.com/test-url';
|
const url = 'http://test-url.com/test-url';
|
||||||
|
|
||||||
let item;
|
let item;
|
||||||
|
let relatedItem;
|
||||||
let author1;
|
let author1;
|
||||||
let author2;
|
let author2;
|
||||||
let fieldUpdate1;
|
let fieldUpdate1;
|
||||||
@@ -29,7 +31,9 @@ let de;
|
|||||||
let el;
|
let el;
|
||||||
|
|
||||||
describe('EditRelationshipComponent', () => {
|
describe('EditRelationshipComponent', () => {
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
|
||||||
relationshipType = Object.assign(new RelationshipType(), {
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
id: '1',
|
id: '1',
|
||||||
uuid: '1',
|
uuid: '1',
|
||||||
@@ -37,6 +41,17 @@ describe('EditRelationshipComponent', () => {
|
|||||||
rightwardType: 'isPublicationOfAuthor'
|
rightwardType: 'isPublicationOfAuthor'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
item = Object.assign(new Item(), {
|
||||||
|
self: 'fake-item-url/publication',
|
||||||
|
id: 'publication',
|
||||||
|
uuid: 'publication',
|
||||||
|
relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships)))
|
||||||
|
});
|
||||||
|
|
||||||
|
relatedItem = Object.assign(new Item(), {
|
||||||
|
uuid: 'related item id',
|
||||||
|
});
|
||||||
|
|
||||||
relationships = [
|
relationships = [
|
||||||
Object.assign(new Relationship(), {
|
Object.assign(new Relationship(), {
|
||||||
self: url + '/2',
|
self: url + '/2',
|
||||||
@@ -44,7 +59,9 @@ describe('EditRelationshipComponent', () => {
|
|||||||
uuid: '2',
|
uuid: '2',
|
||||||
leftId: 'author1',
|
leftId: 'author1',
|
||||||
rightId: 'publication',
|
rightId: 'publication',
|
||||||
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)),
|
||||||
|
leftItem: observableOf(new RemoteData(false, false, true, undefined, relatedItem)),
|
||||||
|
rightItem: observableOf(new RemoteData(false, false, true, undefined, item)),
|
||||||
}),
|
}),
|
||||||
Object.assign(new Relationship(), {
|
Object.assign(new Relationship(), {
|
||||||
self: url + '/3',
|
self: url + '/3',
|
||||||
@@ -56,13 +73,6 @@ describe('EditRelationshipComponent', () => {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
item = Object.assign(new Item(), {
|
|
||||||
self: 'fake-item-url/publication',
|
|
||||||
id: 'publication',
|
|
||||||
uuid: 'publication',
|
|
||||||
relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships)))
|
|
||||||
});
|
|
||||||
|
|
||||||
author1 = Object.assign(new Item(), {
|
author1 = Object.assign(new Item(), {
|
||||||
id: 'author1',
|
id: 'author1',
|
||||||
uuid: 'author1'
|
uuid: 'author1'
|
||||||
@@ -73,38 +83,44 @@ describe('EditRelationshipComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
fieldUpdate1 = {
|
fieldUpdate1 = {
|
||||||
field: author1,
|
field: relationships[0],
|
||||||
changeType: undefined
|
changeType: undefined
|
||||||
};
|
};
|
||||||
fieldUpdate2 = {
|
fieldUpdate2 = {
|
||||||
field: author2,
|
field: relationships[1],
|
||||||
changeType: FieldChangeType.REMOVE
|
changeType: FieldChangeType.REMOVE
|
||||||
};
|
};
|
||||||
|
|
||||||
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
const itemSelection = {};
|
||||||
{
|
itemSelection[relatedItem.uuid] = false;
|
||||||
saveChangeFieldUpdate: {},
|
itemSelection[item.uuid] = true;
|
||||||
saveRemoveFieldUpdate: {},
|
|
||||||
setEditableFieldUpdate: {},
|
objectUpdatesService = {
|
||||||
setValidFieldUpdate: {},
|
isSelectedVirtualMetadata: () => null,
|
||||||
removeSingleFieldUpdate: {},
|
removeSingleFieldUpdate: jasmine.createSpy('removeSingleFieldUpdate'),
|
||||||
isEditable: observableOf(false), // should always return something --> its in ngOnInit
|
saveRemoveFieldUpdate: jasmine.createSpy('saveRemoveFieldUpdate'),
|
||||||
isValid: observableOf(true) // should always return something --> its in ngOnInit
|
};
|
||||||
}
|
|
||||||
);
|
spyOn(objectUpdatesService, 'isSelectedVirtualMetadata').and.callFake((a, b, uuid) => observableOf(itemSelection[uuid]));
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot()],
|
imports: [TranslateModule.forRoot()],
|
||||||
declarations: [EditRelationshipComponent],
|
declarations: [EditRelationshipComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService }
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
], schemas: [
|
{ provide: NgbModal, useValue: {
|
||||||
|
open: () => {/*comment*/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
], schemas: [
|
||||||
NO_ERRORS_SCHEMA
|
NO_ERRORS_SCHEMA
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
||||||
fixture = TestBed.createComponent(EditRelationshipComponent);
|
fixture = TestBed.createComponent(EditRelationshipComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
de = fixture.debugElement;
|
de = fixture.debugElement;
|
||||||
@@ -112,7 +128,8 @@ describe('EditRelationshipComponent', () => {
|
|||||||
|
|
||||||
comp.url = url;
|
comp.url = url;
|
||||||
comp.fieldUpdate = fieldUpdate1;
|
comp.fieldUpdate = fieldUpdate1;
|
||||||
comp.item = item;
|
comp.editItem = item;
|
||||||
|
comp.relatedItem$ = observableOf(relatedItem);
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -156,23 +173,30 @@ describe('EditRelationshipComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('remove', () => {
|
describe('remove', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'closeVirtualMetadataModal');
|
||||||
|
comp.ngOnChanges();
|
||||||
comp.remove();
|
comp.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call saveRemoveFieldUpdate with the correct arguments', () => {
|
it('should close the virtual metadata modal and call saveRemoveFieldUpdate with the correct arguments', () => {
|
||||||
expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(url, item);
|
expect(comp.closeVirtualMetadataModal).toHaveBeenCalled();
|
||||||
|
expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(
|
||||||
|
url,
|
||||||
|
Object.assign({}, fieldUpdate1.field, {
|
||||||
|
keepLeftVirtualMetadata: false,
|
||||||
|
keepRightVirtualMetadata: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('undo', () => {
|
describe('undo', () => {
|
||||||
beforeEach(() => {
|
|
||||||
comp.undo();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call removeSingleFieldUpdate with the correct arguments', () => {
|
it('should call removeSingleFieldUpdate with the correct arguments', () => {
|
||||||
expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, item.uuid);
|
comp.undo();
|
||||||
|
expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, relationships[0].uuid);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,14 +1,19 @@
|
|||||||
import { Component, Input, OnChanges } from '@angular/core';
|
import { Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||||
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { cloneDeep } from 'lodash';
|
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
|
||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
|
import { DeleteRelationship, FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
|
||||||
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
|
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
// tslint:disable-next-line:component-selector
|
// tslint:disable-next-line:component-selector
|
||||||
selector: '[ds-edit-relationship]',
|
selector: 'ds-edit-relationship',
|
||||||
styleUrls: ['./edit-relationship.component.scss'],
|
styleUrls: ['./edit-relationship.component.scss'],
|
||||||
templateUrl: './edit-relationship.component.html',
|
templateUrl: './edit-relationship.component.html',
|
||||||
})
|
})
|
||||||
@@ -23,38 +28,108 @@ export class EditRelationshipComponent implements OnChanges {
|
|||||||
*/
|
*/
|
||||||
@Input() url: string;
|
@Input() url: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item being edited
|
||||||
|
*/
|
||||||
|
@Input() editItem: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship being edited
|
||||||
|
*/
|
||||||
|
get relationship(): Relationship {
|
||||||
|
return this.fieldUpdate.field as Relationship;
|
||||||
|
}
|
||||||
|
|
||||||
|
private leftItem$: Observable<Item>;
|
||||||
|
private rightItem$: Observable<Item>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The related item of this relationship
|
* The related item of this relationship
|
||||||
*/
|
*/
|
||||||
item: Item;
|
relatedItem$: Observable<Item>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The view-mode we're currently on
|
* The view-mode we're currently on
|
||||||
*/
|
*/
|
||||||
viewMode = ViewMode.ListElement;
|
viewMode = ViewMode.ListElement;
|
||||||
|
|
||||||
constructor(private objectUpdatesService: ObjectUpdatesService) {
|
/**
|
||||||
|
* Reference to NgbModal
|
||||||
|
*/
|
||||||
|
public modalRef: NgbModalRef;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private objectUpdatesService: ObjectUpdatesService,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the current relationship based on the fieldUpdate input field
|
* Sets the current relationship based on the fieldUpdate input field
|
||||||
*/
|
*/
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
this.item = cloneDeep(this.fieldUpdate.field) as Item;
|
this.leftItem$ = this.relationship.leftItem.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid))
|
||||||
|
);
|
||||||
|
this.rightItem$ = this.relationship.rightItem.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid))
|
||||||
|
);
|
||||||
|
this.relatedItem$ = observableCombineLatest(
|
||||||
|
this.leftItem$,
|
||||||
|
this.rightItem$,
|
||||||
|
).pipe(
|
||||||
|
map((items: Item[]) =>
|
||||||
|
items.find((item) => item.uuid !== this.editItem.uuid)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a new remove update for this field to the object updates service
|
* Sends a new remove update for this field to the object updates service
|
||||||
*/
|
*/
|
||||||
remove(): void {
|
remove(): void {
|
||||||
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, this.item);
|
this.closeVirtualMetadataModal();
|
||||||
|
observableCombineLatest(
|
||||||
|
this.leftItem$,
|
||||||
|
this.rightItem$,
|
||||||
|
).pipe(
|
||||||
|
map((items: Item[]) =>
|
||||||
|
items.map((item) => this.objectUpdatesService
|
||||||
|
.isSelectedVirtualMetadata(this.url, this.relationship.id, item.uuid))
|
||||||
|
),
|
||||||
|
switchMap((selection$) => observableCombineLatest(selection$)),
|
||||||
|
map((selection: boolean[]) => {
|
||||||
|
return Object.assign({},
|
||||||
|
this.fieldUpdate.field,
|
||||||
|
{
|
||||||
|
keepLeftVirtualMetadata: selection[0] === true,
|
||||||
|
keepRightVirtualMetadata: selection[1] === true,
|
||||||
|
}
|
||||||
|
) as DeleteRelationship
|
||||||
|
}),
|
||||||
|
take(1),
|
||||||
|
).subscribe((deleteRelationship: DeleteRelationship) =>
|
||||||
|
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, deleteRelationship)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
openVirtualMetadataModal(content: any) {
|
||||||
|
this.modalRef = this.modalService.open(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeVirtualMetadataModal() {
|
||||||
|
this.modalRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the current update for this field in the object updates service
|
* Cancels the current update for this field in the object updates service
|
||||||
*/
|
*/
|
||||||
undo(): void {
|
undo(): void {
|
||||||
this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.item.uuid);
|
this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.fieldUpdate.field.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -17,8 +17,13 @@
|
|||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngFor="let label of relationLabels$ | async" class="mb-4">
|
<div *ngFor="let relationshipType of relationshipTypes$ | async" class="mb-4">
|
||||||
<ds-edit-relationship-list [item]="item" [url]="url" [relationshipLabel]="label" ></ds-edit-relationship-list>
|
<ds-edit-relationship-list
|
||||||
|
[url]="url"
|
||||||
|
[item]="item"
|
||||||
|
[itemType]="entityType$ | async"
|
||||||
|
[relationshipType]="relationshipType"
|
||||||
|
></ds-edit-relationship-list>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row bottom">
|
<div class="button-row bottom">
|
||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
|
@@ -13,9 +13,8 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { GLOBAL_CONFIG } from '../../../../config';
|
import { GLOBAL_CONFIG } from '../../../../config';
|
||||||
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
import { ResourceType } from '../../../core/shared/resource-type';
|
|
||||||
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
import { of as observableOf, combineLatest as observableCombineLatest } from 'rxjs';
|
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
@@ -26,6 +25,8 @@ import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
|||||||
import { getTestScheduler } from 'jasmine-marbles';
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
import { EntityTypeService } from '../../../core/data/entity-type.service';
|
||||||
|
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
||||||
|
|
||||||
let comp: any;
|
let comp: any;
|
||||||
let fixture: ComponentFixture<ItemRelationshipsComponent>;
|
let fixture: ComponentFixture<ItemRelationshipsComponent>;
|
||||||
@@ -34,6 +35,7 @@ let el: HTMLElement;
|
|||||||
let objectUpdatesService;
|
let objectUpdatesService;
|
||||||
let relationshipService;
|
let relationshipService;
|
||||||
let requestService;
|
let requestService;
|
||||||
|
let entityTypeService;
|
||||||
let objectCache;
|
let objectCache;
|
||||||
const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
|
const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
|
||||||
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
|
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
|
||||||
@@ -58,6 +60,7 @@ let author1;
|
|||||||
let author2;
|
let author2;
|
||||||
let fieldUpdate1;
|
let fieldUpdate1;
|
||||||
let fieldUpdate2;
|
let fieldUpdate2;
|
||||||
|
let entityType;
|
||||||
let relationships;
|
let relationships;
|
||||||
let relationshipType;
|
let relationshipType;
|
||||||
|
|
||||||
@@ -95,6 +98,10 @@ describe('ItemRelationshipsComponent', () => {
|
|||||||
lastModified: date
|
lastModified: date
|
||||||
});
|
});
|
||||||
|
|
||||||
|
entityType = Object.assign(new ItemType(), {
|
||||||
|
id: 'entityType',
|
||||||
|
});
|
||||||
|
|
||||||
author1 = Object.assign(new Item(), {
|
author1 = Object.assign(new Item(), {
|
||||||
id: 'author1',
|
id: 'author1',
|
||||||
uuid: 'author1'
|
uuid: 'author1'
|
||||||
@@ -110,11 +117,14 @@ describe('ItemRelationshipsComponent', () => {
|
|||||||
relationships[1].rightItem = observableOf(new RemoteData(false, false, true, undefined, item));
|
relationships[1].rightItem = observableOf(new RemoteData(false, false, true, undefined, item));
|
||||||
|
|
||||||
fieldUpdate1 = {
|
fieldUpdate1 = {
|
||||||
field: author1,
|
field: relationships[0],
|
||||||
changeType: undefined
|
changeType: undefined
|
||||||
};
|
};
|
||||||
fieldUpdate2 = {
|
fieldUpdate2 = {
|
||||||
field: author2,
|
field: Object.assign(
|
||||||
|
relationships[1],
|
||||||
|
{keepLeftVirtualMetadata: true, keepRightVirtualMetadata: false}
|
||||||
|
),
|
||||||
changeType: FieldChangeType.REMOVE
|
changeType: FieldChangeType.REMOVE
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -130,12 +140,12 @@ describe('ItemRelationshipsComponent', () => {
|
|||||||
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
||||||
{
|
{
|
||||||
getFieldUpdates: observableOf({
|
getFieldUpdates: observableOf({
|
||||||
[author1.uuid]: fieldUpdate1,
|
[relationships[0].uuid]: fieldUpdate1,
|
||||||
[author2.uuid]: fieldUpdate2
|
[relationships[1].uuid]: fieldUpdate2
|
||||||
}),
|
}),
|
||||||
getFieldUpdatesExclusive: observableOf({
|
getFieldUpdatesExclusive: observableOf({
|
||||||
[author1.uuid]: fieldUpdate1,
|
[relationships[0].uuid]: fieldUpdate1,
|
||||||
[author2.uuid]: fieldUpdate2
|
[relationships[1].uuid]: fieldUpdate2
|
||||||
}),
|
}),
|
||||||
saveAddFieldUpdate: {},
|
saveAddFieldUpdate: {},
|
||||||
discardFieldUpdates: {},
|
discardFieldUpdates: {},
|
||||||
@@ -173,6 +183,25 @@ describe('ItemRelationshipsComponent', () => {
|
|||||||
remove: undefined
|
remove: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
entityTypeService = jasmine.createSpyObj('entityTypeService',
|
||||||
|
{
|
||||||
|
getEntityTypeByLabel: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
entityType,
|
||||||
|
)),
|
||||||
|
getEntityTypeRelationships: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
new PaginatedList(new PageInfo(), [relationshipType]),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [SharedModule, TranslateModule.forRoot()],
|
imports: [SharedModule, TranslateModule.forRoot()],
|
||||||
@@ -185,6 +214,7 @@ describe('ItemRelationshipsComponent', () => {
|
|||||||
{ provide: NotificationsService, useValue: notificationsService },
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
{ provide: GLOBAL_CONFIG, useValue: { item: { edit: { undoTimeout: 10 } } } as any },
|
{ provide: GLOBAL_CONFIG, useValue: { item: { edit: { undoTimeout: 10 } } } as any },
|
||||||
{ provide: RelationshipService, useValue: relationshipService },
|
{ provide: RelationshipService, useValue: relationshipService },
|
||||||
|
{ provide: EntityTypeService, useValue: entityTypeService },
|
||||||
{ provide: ObjectCacheService, useValue: objectCache },
|
{ provide: ObjectCacheService, useValue: objectCache },
|
||||||
{ provide: RequestService, useValue: requestService },
|
{ provide: RequestService, useValue: requestService },
|
||||||
ChangeDetectorRef
|
ChangeDetectorRef
|
||||||
@@ -229,7 +259,7 @@ describe('ItemRelationshipsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('it should delete the correct relationship', () => {
|
it('it should delete the correct relationship', () => {
|
||||||
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid);
|
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid, 'left');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core';
|
import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
import { DeleteRelationship, FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { filter, flatMap, map, switchMap, take, tap } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { zip as observableZip } from 'rxjs';
|
import { zip as observableZip } from 'rxjs';
|
||||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
@@ -12,15 +12,18 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config';
|
||||||
import { RelationshipService } from '../../../core/data/relationship.service';
|
import { RelationshipService } from '../../../core/data/relationship.service';
|
||||||
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
|
||||||
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
|
||||||
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
|
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
|
||||||
import { isNotEmptyOperator } from '../../../shared/empty.util';
|
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||||
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { Subscription } from 'rxjs/internal/Subscription';
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
|
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
||||||
|
import { EntityTypeService } from '../../../core/data/entity-type.service';
|
||||||
|
import { isNotEmptyOperator } from '../../../shared/empty.util';
|
||||||
|
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
||||||
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-relationships',
|
selector: 'ds-item-relationships',
|
||||||
@@ -35,13 +38,14 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
/**
|
/**
|
||||||
* The labels of all different relations within this item
|
* The labels of all different relations within this item
|
||||||
*/
|
*/
|
||||||
relationLabels$: Observable<string[]>;
|
relationshipTypes$: Observable<RelationshipType[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A subscription that checks when the item is deleted in cache and reloads the item by sending a new request
|
* A subscription that checks when the item is deleted in cache and reloads the item by sending a new request
|
||||||
* This is used to update the item in cache after relationships are deleted
|
* This is used to update the item in cache after relationships are deleted
|
||||||
*/
|
*/
|
||||||
itemUpdateSubscription: Subscription;
|
itemUpdateSubscription: Subscription;
|
||||||
|
entityType$: Observable<ItemType>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected itemService: ItemDataService,
|
protected itemService: ItemDataService,
|
||||||
@@ -54,7 +58,8 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
protected relationshipService: RelationshipService,
|
protected relationshipService: RelationshipService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected cdRef: ChangeDetectorRef
|
protected entityTypeService: EntityTypeService,
|
||||||
|
protected cdr: ChangeDetectorRef,
|
||||||
) {
|
) {
|
||||||
super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route);
|
super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route);
|
||||||
}
|
}
|
||||||
@@ -64,21 +69,14 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.relationLabels$ = this.relationshipService.getRelationshipTypeLabelsByItem(this.item);
|
|
||||||
this.initializeItemUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the item (and view) when it's removed in the request cache
|
|
||||||
*/
|
|
||||||
public initializeItemUpdate(): void {
|
|
||||||
this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe(
|
this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe(
|
||||||
filter((exists: boolean) => !exists),
|
filter((exists: boolean) => !exists),
|
||||||
switchMap(() => this.itemService.findById(this.item.uuid)),
|
switchMap(() => this.itemService.findById(this.item.uuid)),
|
||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
).subscribe((itemRD: RemoteData<Item>) => {
|
).subscribe((itemRD: RemoteData<Item>) => {
|
||||||
this.item = itemRD.payload;
|
this.item = itemRD.payload;
|
||||||
this.cdRef.detectChanges();
|
this.cdr.detectChanges();
|
||||||
|
this.initializeUpdates();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,8 +84,22 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
* Initialize the values and updates of the current item's relationship fields
|
* Initialize the values and updates of the current item's relationship fields
|
||||||
*/
|
*/
|
||||||
public initializeUpdates(): void {
|
public initializeUpdates(): void {
|
||||||
this.updates$ = this.relationshipService.getRelatedItems(this.item).pipe(
|
|
||||||
switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdates(this.url, items))
|
this.entityType$ = this.entityTypeService.getEntityTypeByLabel(
|
||||||
|
this.item.firstMetadataValue('relationship.type')
|
||||||
|
).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.relationshipTypes$ = this.entityType$.pipe(
|
||||||
|
switchMap((entityType) =>
|
||||||
|
this.entityTypeService.getEntityTypeRelationships(entityType.id).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((relationshipTypes) => relationshipTypes.page),
|
||||||
|
)
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,26 +115,41 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
* Make sure the lists are refreshed afterwards and notifications are sent for success and errors
|
* Make sure the lists are refreshed afterwards and notifications are sent for success and errors
|
||||||
*/
|
*/
|
||||||
public submit(): void {
|
public submit(): void {
|
||||||
// Get all IDs of related items of which their relationship with the current item is about to be removed
|
|
||||||
const removedItemIds$ = this.relationshipService.getRelatedItems(this.item).pipe(
|
|
||||||
switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items) as Observable<FieldUpdates>),
|
|
||||||
map((fieldUpdates: FieldUpdates) => Object.values(fieldUpdates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)),
|
|
||||||
map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field.uuid) as string[]),
|
|
||||||
isNotEmptyOperator()
|
|
||||||
);
|
|
||||||
// Get all the relationships that should be removed
|
// Get all the relationships that should be removed
|
||||||
const removedRelationships$ = removedItemIds$.pipe(
|
this.relationshipService.getItemRelationshipsArray(this.item).pipe(
|
||||||
flatMap((uuids) => this.relationshipService.getRelationshipsByRelatedItemIds(this.item, uuids))
|
map((relationships: Relationship[]) => relationships.map((relationship) =>
|
||||||
);
|
Object.assign(new Relationship(), relationship, {uuid: relationship.id})
|
||||||
// const removedRelationships$ = removedItemIds$.pipe(flatMap((uuids: string[]) => this.relationshipService.getRelationshipsByRelatedItemIds(this.item, uuids)));
|
)),
|
||||||
// Request a delete for every relationship found in the observable created above
|
switchMap((relationships: Relationship[]) => {
|
||||||
removedRelationships$.pipe(
|
return this.objectUpdatesService.getFieldUpdatesExclusive(this.url, relationships) as Observable<FieldUpdates>
|
||||||
|
}),
|
||||||
|
map((fieldUpdates: FieldUpdates) =>
|
||||||
|
Object.values(fieldUpdates)
|
||||||
|
.filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)
|
||||||
|
.map((fieldUpdate: FieldUpdate) => fieldUpdate.field as DeleteRelationship)
|
||||||
|
),
|
||||||
|
isNotEmptyOperator(),
|
||||||
take(1),
|
take(1),
|
||||||
map((removedRelationships: Relationship[]) => removedRelationships.map((rel: Relationship) => rel.id)),
|
switchMap((deleteRelationships: DeleteRelationship[]) =>
|
||||||
switchMap((removedIds: string[]) => observableZip(...removedIds.map((uuid: string) => this.relationshipService.deleteRelationship(uuid))))
|
observableZip(...deleteRelationships.map((deleteRelationship) => {
|
||||||
|
let copyVirtualMetadata: string;
|
||||||
|
if (deleteRelationship.keepLeftVirtualMetadata && deleteRelationship.keepRightVirtualMetadata) {
|
||||||
|
copyVirtualMetadata = 'all';
|
||||||
|
} else if (deleteRelationship.keepLeftVirtualMetadata) {
|
||||||
|
copyVirtualMetadata = 'left';
|
||||||
|
} else if (deleteRelationship.keepRightVirtualMetadata) {
|
||||||
|
copyVirtualMetadata = 'right';
|
||||||
|
} else {
|
||||||
|
copyVirtualMetadata = 'none';
|
||||||
|
}
|
||||||
|
return this.relationshipService.deleteRelationship(deleteRelationship.uuid, copyVirtualMetadata);
|
||||||
|
}
|
||||||
|
))
|
||||||
|
),
|
||||||
).subscribe((responses: RestResponse[]) => {
|
).subscribe((responses: RestResponse[]) => {
|
||||||
this.displayNotifications(responses);
|
this.itemUpdateSubscription.add(() => {
|
||||||
this.reset();
|
this.displayNotifications(responses);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,22 +171,12 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-initialize fields and subscriptions
|
|
||||||
*/
|
|
||||||
reset() {
|
|
||||||
this.initializeOriginalFields();
|
|
||||||
this.initializeUpdates();
|
|
||||||
this.initializeItemUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends all initial values of this item to the object updates service
|
* Sends all initial values of this item to the object updates service
|
||||||
*/
|
*/
|
||||||
public initializeOriginalFields() {
|
public initializeOriginalFields() {
|
||||||
this.relationshipService.getRelatedItems(this.item).pipe(take(1)).subscribe((items: Item[]) => {
|
const initialFields = [];
|
||||||
this.objectUpdatesService.initialize(this.url, items, this.item.lastModified);
|
this.objectUpdatesService.initialize(this.url, initialFields, this.item.lastModified);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,5 +185,4 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.itemUpdateSubscription.unsubscribe();
|
this.itemUpdateSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,38 @@
|
|||||||
|
<div>
|
||||||
|
<div class="modal-header">{{'virtual-metadata.delete-relationship.modal-head' | translate}}
|
||||||
|
<button type="button" class="close" (click)="close.emit()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<ng-container *ngFor="let item of items; trackBy: trackItem">
|
||||||
|
<div *ngVar="(isSelectedVirtualMetadataItem(item) | async) as selected"
|
||||||
|
(click)="setSelectedVirtualMetadataItem(item, !selected)"
|
||||||
|
class="item d-flex flex-row">
|
||||||
|
<div class="m-2">
|
||||||
|
<label>
|
||||||
|
<input class="select" type="checkbox" [checked]="selected">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex-column">
|
||||||
|
<ds-listable-object-component-loader [object]="item">
|
||||||
|
</ds-listable-object-component-loader>
|
||||||
|
<div *ngFor="let metadata of virtualMetadata.get(item.uuid)">
|
||||||
|
<div class="font-weight-bold">
|
||||||
|
{{metadata.metadataField}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{metadata.metadataValue.value}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<div class="d-flex flex-row-reverse m-2">
|
||||||
|
<button class="btn btn-primary save"
|
||||||
|
(click)="save.emit()">
|
||||||
|
<i class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,102 @@
|
|||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {of as observableOf} from 'rxjs/internal/observable/of';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';
|
||||||
|
import {By} from '@angular/platform-browser';
|
||||||
|
import {VirtualMetadataComponent} from './virtual-metadata.component';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
import {VarDirective} from '../../../shared/utils/var.directive';
|
||||||
|
|
||||||
|
describe('VirtualMetadataComponent', () => {
|
||||||
|
|
||||||
|
let comp: VirtualMetadataComponent;
|
||||||
|
let fixture: ComponentFixture<VirtualMetadataComponent>;
|
||||||
|
let de: DebugElement;
|
||||||
|
|
||||||
|
let objectUpdatesService;
|
||||||
|
|
||||||
|
const url = 'http://test-url.com/test-url';
|
||||||
|
|
||||||
|
let item;
|
||||||
|
let relatedItem;
|
||||||
|
let relationshipId;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
relationshipId = 'relationship id';
|
||||||
|
|
||||||
|
item = Object.assign(new Item(), {
|
||||||
|
uuid: 'publication',
|
||||||
|
metadata: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
relatedItem = Object.assign(new Item(), {
|
||||||
|
uuid: 'relatedItem',
|
||||||
|
metadata: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', {
|
||||||
|
isSelectedVirtualMetadata: observableOf(false),
|
||||||
|
setSelectedVirtualMetadata: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [VirtualMetadataComponent, VarDirective],
|
||||||
|
providers: [
|
||||||
|
{provide: ObjectUpdatesService, useValue: objectUpdatesService},
|
||||||
|
], schemas: [
|
||||||
|
NO_ERRORS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(VirtualMetadataComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
de = fixture.debugElement;
|
||||||
|
|
||||||
|
comp.url = url;
|
||||||
|
comp.leftItem = item;
|
||||||
|
comp.rightItem = relatedItem;
|
||||||
|
comp.relationshipId = relationshipId;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when clicking the save button', () => {
|
||||||
|
it('should emit a save event', () => {
|
||||||
|
|
||||||
|
spyOn(comp.save, 'emit');
|
||||||
|
fixture.debugElement
|
||||||
|
.query(By.css('button.save'))
|
||||||
|
.triggerEventHandler('click', null);
|
||||||
|
expect(comp.save.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when clicking the close button', () => {
|
||||||
|
it('should emit a close event', () => {
|
||||||
|
|
||||||
|
spyOn(comp.close, 'emit');
|
||||||
|
fixture.debugElement
|
||||||
|
.query(By.css('button.close'))
|
||||||
|
.triggerEventHandler('click', null);
|
||||||
|
expect(comp.close.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when selecting an item', () => {
|
||||||
|
it('should call the updates service setSelectedVirtualMetadata method', () => {
|
||||||
|
|
||||||
|
fixture.debugElement
|
||||||
|
.query(By.css('div.item'))
|
||||||
|
.triggerEventHandler('click', null);
|
||||||
|
expect(objectUpdatesService.setSelectedVirtualMetadata).toHaveBeenCalledWith(
|
||||||
|
url,
|
||||||
|
relationshipId,
|
||||||
|
item.uuid,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
@@ -0,0 +1,120 @@
|
|||||||
|
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
import {MetadataValue} from '../../../core/shared/metadata.models';
|
||||||
|
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-virtual-metadata',
|
||||||
|
templateUrl: './virtual-metadata.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component that lists both items of a relationship, along with their virtual metadata of the relationship.
|
||||||
|
* The component is shown when a relationship is marked to be deleted.
|
||||||
|
* Each item has a checkbox to indicate whether its virtual metadata should be saved as real metadata.
|
||||||
|
*/
|
||||||
|
export class VirtualMetadataComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current url of this page
|
||||||
|
*/
|
||||||
|
@Input() url: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the relationship to be deleted.
|
||||||
|
*/
|
||||||
|
@Input() relationshipId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The left item of the relationship to be deleted.
|
||||||
|
*/
|
||||||
|
@Input() leftItem: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The right item of the relationship to be deleted.
|
||||||
|
*/
|
||||||
|
@Input() rightItem: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when the close button is pressed.
|
||||||
|
*/
|
||||||
|
@Output() close = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when the save button is pressed.
|
||||||
|
*/
|
||||||
|
@Output() save = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an array of the left and the right item of the relationship to be deleted.
|
||||||
|
*/
|
||||||
|
get items() {
|
||||||
|
return [this.leftItem, this.rightItem];
|
||||||
|
}
|
||||||
|
|
||||||
|
private virtualMetadata: Map<string, VirtualMetadata[]> = new Map<string, VirtualMetadata[]>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected objectUpdatesService: ObjectUpdatesService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the virtual metadata of a given item corresponding to this relationship.
|
||||||
|
* @param item the item to get the virtual metadata for
|
||||||
|
*/
|
||||||
|
getVirtualMetadata(item: Item): VirtualMetadata[] {
|
||||||
|
|
||||||
|
return Object.entries(item.metadata)
|
||||||
|
.map(([key, value]) =>
|
||||||
|
value
|
||||||
|
.filter((metadata: MetadataValue) =>
|
||||||
|
!key.startsWith('relation') && metadata.authority && metadata.authority.endsWith(this.relationshipId))
|
||||||
|
.map((metadata: MetadataValue) => {
|
||||||
|
return {
|
||||||
|
metadataField: key,
|
||||||
|
metadataValue: metadata,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.reduce((previous, current) => previous.concat(current), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select/deselect the virtual metadata of an item to be saved as real metadata.
|
||||||
|
* @param item the item for which (not) to save the virtual metadata as real metadata
|
||||||
|
* @param selected whether or not to save the virtual metadata as real metadata
|
||||||
|
*/
|
||||||
|
setSelectedVirtualMetadataItem(item: Item, selected: boolean) {
|
||||||
|
this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.relationshipId, item.uuid, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the virtual metadata of a given item is selected to be saved as real metadata
|
||||||
|
* @param item the item for which to check whether the virtual metadata is selected to be saved as real metadata
|
||||||
|
*/
|
||||||
|
isSelectedVirtualMetadataItem(item: Item): Observable<boolean> {
|
||||||
|
return this.objectUpdatesService.isSelectedVirtualMetadata(this.url, this.relationshipId, item.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent unnecessary rerendering so fields don't lose focus
|
||||||
|
*/
|
||||||
|
trackItem(index, item: Item) {
|
||||||
|
return item && item.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.items.forEach((item) => {
|
||||||
|
this.virtualMetadata.set(item.uuid, this.getVirtualMetadata(item));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a virtual metadata entry.
|
||||||
|
*/
|
||||||
|
export interface VirtualMetadata {
|
||||||
|
metadataField: string,
|
||||||
|
metadataValue: MetadataValue,
|
||||||
|
}
|
@@ -11,6 +11,7 @@ let fixture: ComponentFixture<ItemPageUriFieldComponent>;
|
|||||||
|
|
||||||
const mockField = 'dc.identifier.uri';
|
const mockField = 'dc.identifier.uri';
|
||||||
const mockValue = 'test value';
|
const mockValue = 'test value';
|
||||||
|
const mockLabel = 'test label';
|
||||||
|
|
||||||
describe('ItemPageUriFieldComponent', () => {
|
describe('ItemPageUriFieldComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
@@ -32,6 +33,8 @@ describe('ItemPageUriFieldComponent', () => {
|
|||||||
fixture = TestBed.createComponent(ItemPageUriFieldComponent);
|
fixture = TestBed.createComponent(ItemPageUriFieldComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||||
|
comp.fields = [mockField];
|
||||||
|
comp.label = mockLabel;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@@ -8,7 +8,8 @@ import { ItemPageFieldComponent } from '../item-page-field.component';
|
|||||||
templateUrl: './item-page-uri-field.component.html'
|
templateUrl: './item-page-uri-field.component.html'
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* This component is used for displaying the uri (dc.identifier.uri) metadata of an item
|
* This component can be used to represent any uri on a simple item page.
|
||||||
|
* It expects 4 parameters: The item, a separator, the metadata keys and an i18n key
|
||||||
*/
|
*/
|
||||||
export class ItemPageUriFieldComponent extends ItemPageFieldComponent {
|
export class ItemPageUriFieldComponent extends ItemPageFieldComponent {
|
||||||
|
|
||||||
@@ -21,19 +22,16 @@ export class ItemPageUriFieldComponent extends ItemPageFieldComponent {
|
|||||||
* Separator string between multiple values of the metadata fields defined
|
* Separator string between multiple values of the metadata fields defined
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
separator: string;
|
@Input() separator: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fields (schema.element.qualifier) used to render their values.
|
* Fields (schema.element.qualifier) used to render their values.
|
||||||
* In this component, we want to display values for metadata 'dc.identifier.uri'
|
|
||||||
*/
|
*/
|
||||||
fields: string[] = [
|
@Input() fields: string[];
|
||||||
'dc.identifier.uri'
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label i18n key for the rendered metadata
|
* Label i18n key for the rendered metadata
|
||||||
*/
|
*/
|
||||||
label = 'item.page.uri';
|
@Input() label: string;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -63,7 +63,10 @@
|
|||||||
[fields]="['dc.identifier.citation']"
|
[fields]="['dc.identifier.citation']"
|
||||||
[label]="'item.page.citation'">
|
[label]="'item.page.citation'">
|
||||||
</ds-generic-item-page-field>
|
</ds-generic-item-page-field>
|
||||||
<ds-item-page-uri-field [item]="object"></ds-item-page-uri-field>
|
<ds-item-page-uri-field [item]="object"
|
||||||
|
[fields]="['dc.identifier.uri']"
|
||||||
|
[label]="'item.page.uri'">
|
||||||
|
</ds-item-page-uri-field>
|
||||||
<ds-item-page-collections [item]="object"></ds-item-page-collections>
|
<ds-item-page-collections [item]="object"></ds-item-page-collections>
|
||||||
<div>
|
<div>
|
||||||
<a class="btn btn-outline-primary" [routerLink]="['/items/' + object.id + '/full']">
|
<a class="btn btn-outline-primary" [routerLink]="['/items/' + object.id + '/full']">
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { filter, map, take } from 'rxjs/operators';
|
import { delay, filter, map, take } from 'rxjs/operators';
|
||||||
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, ViewEncapsulation } from '@angular/core';
|
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
|
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
|
||||||
|
|
||||||
@@ -125,8 +125,11 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.router.events
|
this.router.events.pipe(
|
||||||
.subscribe((event) => {
|
// This fixes an ExpressionChangedAfterItHasBeenCheckedError from being thrown while loading the component
|
||||||
|
// More information on this bug-fix: https://blog.angular-university.io/angular-debugging/
|
||||||
|
delay(0)
|
||||||
|
).subscribe((event) => {
|
||||||
if (event instanceof NavigationStart) {
|
if (event instanceof NavigationStart) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
} else if (
|
} else if (
|
||||||
|
@@ -28,6 +28,12 @@ export class NormalizedExternalSourceEntry extends NormalizedObject<ExternalSour
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
value: string;
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the external source this entry originates from
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
externalSource: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata of the entry
|
* Metadata of the entry
|
||||||
*/
|
*/
|
||||||
|
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 */
|
||||||
|
@@ -114,8 +114,10 @@ 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 {EntityTypeService} from './data/entity-type.service';
|
||||||
import { SiteDataService } from './data/site-data.service';
|
import { SiteDataService } from './data/site-data.service';
|
||||||
import { NormalizedSite } from './cache/models/normalized-site.model';
|
import { NormalizedSite } from './cache/models/normalized-site.model';
|
||||||
|
|
||||||
@@ -238,6 +240,8 @@ const PROVIDERS = [
|
|||||||
TaskResponseParsingService,
|
TaskResponseParsingService,
|
||||||
ClaimedTaskDataService,
|
ClaimedTaskDataService,
|
||||||
PoolTaskDataService,
|
PoolTaskDataService,
|
||||||
|
EntityTypeService,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -318,9 +318,11 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
/**
|
/**
|
||||||
* Delete an existing DSpace Object on the server
|
* Delete an existing DSpace Object on the server
|
||||||
* @param dso The DSpace Object to be removed
|
* @param dso The DSpace Object to be removed
|
||||||
* Return an observable that emits true when the deletion was successful, false when it failed
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return an observable that emits true when the deletion was successful, false when it failed
|
||||||
*/
|
*/
|
||||||
delete(dso: T): Observable<boolean> {
|
delete(dso: T, copyVirtualMetadata?: string[]): Observable<boolean> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
@@ -329,6 +331,13 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
hrefObs.pipe(
|
hrefObs.pipe(
|
||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href)),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
|
if (copyVirtualMetadata) {
|
||||||
|
copyVirtualMetadata.forEach((id) =>
|
||||||
|
href += (href.includes('?') ? '&' : '?')
|
||||||
|
+ 'copyVirtualMetadata='
|
||||||
|
+ id
|
||||||
|
);
|
||||||
|
}
|
||||||
const request = new DeleteByIDRequest(requestId, href, dso.uuid);
|
const request = new DeleteByIDRequest(requestId, href, dso.uuid);
|
||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
})
|
})
|
||||||
|
103
src/app/core/data/entity-type.service.ts
Normal file
103
src/app/core/data/entity-type.service.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { DataService } from './data.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { GetRequest } from './request.models';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import {switchMap, take, tap} from 'rxjs/operators';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import {RelationshipType} from '../shared/item-relationships/relationship-type.model';
|
||||||
|
import {PaginatedList} from './paginated-list';
|
||||||
|
import {ItemType} from '../shared/item-relationships/item-type.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service handling all ItemType requests
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class EntityTypeService extends DataService<ItemType> {
|
||||||
|
|
||||||
|
protected linkPath = 'entitytypes';
|
||||||
|
protected forceBypassCache = false;
|
||||||
|
|
||||||
|
constructor(protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<ItemType>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBrowseEndpoint(options, linkPath?: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint for the item type's allowed relationship types
|
||||||
|
* @param entityTypeId
|
||||||
|
*/
|
||||||
|
getRelationshipTypesEndpoint(entityTypeId: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
switchMap((href) => this.halService.getEndpoint('relationshiptypes', `${href}/${entityTypeId}`))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the allowed relationship types for an entity type
|
||||||
|
* @param entityTypeId
|
||||||
|
*/
|
||||||
|
getEntityTypeRelationships(entityTypeId: string): Observable<RemoteData<PaginatedList<RelationshipType>>> {
|
||||||
|
|
||||||
|
const href$ = this.getRelationshipTypesEndpoint(entityTypeId);
|
||||||
|
|
||||||
|
href$.pipe(take(1)).subscribe((href) => {
|
||||||
|
const request = new GetRequest(this.requestService.generateRequestId(), href);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.rdbService.buildList(href$);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an entity type by their label
|
||||||
|
* @param label
|
||||||
|
*/
|
||||||
|
getEntityTypeByLabel(label: string): Observable<RemoteData<ItemType>> {
|
||||||
|
|
||||||
|
// TODO: Remove mock data once REST API supports this
|
||||||
|
/*
|
||||||
|
href$.pipe(take(1)).subscribe((href) => {
|
||||||
|
const request = new GetRequest(this.requestService.generateRequestId(), href);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.rdbService.buildSingle<EntityType>(href$);
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mock:
|
||||||
|
const index = [
|
||||||
|
'Publication',
|
||||||
|
'Person',
|
||||||
|
'Project',
|
||||||
|
'OrgUnit',
|
||||||
|
'Journal',
|
||||||
|
'JournalVolume',
|
||||||
|
'JournalIssue',
|
||||||
|
'DataPackage',
|
||||||
|
'DataFile',
|
||||||
|
].indexOf(label);
|
||||||
|
|
||||||
|
return this.findById((index + 1) + '');
|
||||||
|
}
|
||||||
|
}
|
@@ -4,7 +4,7 @@ import { ResponseParsingService } from './parsing.service';
|
|||||||
import { RestRequest } from './request.models';
|
import { RestRequest } from './request.models';
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
import { FacetValue } from '../../shared/search/facet-value.model';
|
import {FacetValue} from '../../shared/search/facet-value.model';
|
||||||
import { BaseResponseParsingService } from './base-response-parsing.service';
|
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
@@ -15,6 +15,7 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
|
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
|
||||||
|
|
||||||
describe('ItemDataService', () => {
|
describe('ItemDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -194,4 +195,24 @@ describe('ItemDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('importExternalSourceEntry', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
const externalSourceEntry = Object.assign(new ExternalSourceEntry(), {
|
||||||
|
display: 'John, Doe',
|
||||||
|
value: 'John, Doe',
|
||||||
|
self: 'http://test-rest.com/server/api/integration/externalSources/orcidV2/entryValues/0000-0003-4851-8004'
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = initTestService();
|
||||||
|
spyOn(requestService, 'configure');
|
||||||
|
result = service.importExternalSourceEntry(externalSourceEntry, 'collection-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure a POST request', () => {
|
||||||
|
result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -37,6 +37,7 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
|||||||
import { Collection } from '../shared/collection.model';
|
import { Collection } from '../shared/collection.model';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemDataService extends DataService<Item> {
|
export class ItemDataService extends DataService<Item> {
|
||||||
@@ -248,6 +249,40 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import an external source entry into a collection
|
||||||
|
* @param externalSourceEntry
|
||||||
|
* @param collectionId
|
||||||
|
*/
|
||||||
|
public importExternalSourceEntry(externalSourceEntry: ExternalSourceEntry, collectionId: string): Observable<RemoteData<Item>> {
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
|
options.headers = headers;
|
||||||
|
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
const href$ = this.halService.getEndpoint(this.linkPath).pipe(map((href) => `${href}?owningCollection=${collectionId}`));
|
||||||
|
|
||||||
|
href$.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
map((href: string) => {
|
||||||
|
const request = new PostRequest(requestId, href, externalSourceEntry.self, options);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
find((request: RequestEntry) => request.completed),
|
||||||
|
getResponseFromEntry(),
|
||||||
|
map((response: any) => {
|
||||||
|
if (isNotEmpty(response.resourceSelfLinks)) {
|
||||||
|
return response.resourceSelfLinks[0];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
switchMap((selfLink: string) => this.findByHref(selfLink))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the endpoint for an item's bitstreams
|
* Get the endpoint for an item's bitstreams
|
||||||
* @param itemId
|
* @param itemId
|
||||||
|
@@ -10,11 +10,14 @@ import { SearchResult } from '../../shared/search/search-result.model';
|
|||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { skip, take } from 'rxjs/operators';
|
import { skip, take } from 'rxjs/operators';
|
||||||
import { ExternalSource } from '../shared/external-source.model';
|
import { ExternalSource } from '../shared/external-source.model';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
describe('LookupRelationService', () => {
|
describe('LookupRelationService', () => {
|
||||||
let service: LookupRelationService;
|
let service: LookupRelationService;
|
||||||
let externalSourceService: ExternalSourceService;
|
let externalSourceService: ExternalSourceService;
|
||||||
let searchService: SearchService;
|
let searchService: SearchService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
|
||||||
const totalExternal = 8;
|
const totalExternal = 8;
|
||||||
const optionsWithQuery = new PaginatedSearchOptions({ query: 'test-query' });
|
const optionsWithQuery = new PaginatedSearchOptions({ query: 'test-query' });
|
||||||
@@ -35,15 +38,18 @@ describe('LookupRelationService', () => {
|
|||||||
name: 'orcidV2',
|
name: 'orcidV2',
|
||||||
hierarchical: false
|
hierarchical: false
|
||||||
});
|
});
|
||||||
|
const searchServiceEndpoint = 'http://test-rest.com/server/api/core/search';
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
externalSourceService = jasmine.createSpyObj('externalSourceService', {
|
externalSourceService = jasmine.createSpyObj('externalSourceService', {
|
||||||
getExternalSourceEntries: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: totalExternal, totalPages: totalExternal, currentPage: 1 }), [{}]))
|
getExternalSourceEntries: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: totalExternal, totalPages: totalExternal, currentPage: 1 }), [{}]))
|
||||||
});
|
});
|
||||||
searchService = jasmine.createSpyObj('searchService', {
|
searchService = jasmine.createSpyObj('searchService', {
|
||||||
search: createSuccessfulRemoteDataObject$(createPaginatedList(localResults))
|
search: createSuccessfulRemoteDataObject$(createPaginatedList(localResults)),
|
||||||
|
getEndpoint: observableOf(searchServiceEndpoint)
|
||||||
});
|
});
|
||||||
service = new LookupRelationService(externalSourceService, searchService);
|
requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring']);
|
||||||
|
service = new LookupRelationService(externalSourceService, searchService, requestService);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -113,4 +119,14 @@ describe('LookupRelationService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('removeLocalResultsCache', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.removeLocalResultsCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call requestService\'s removeByHrefSubstring with the search endpoint', () => {
|
||||||
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(searchServiceEndpoint);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -15,6 +15,7 @@ import { getAllSucceededRemoteData, getRemoteDataPayload } from '../shared/opera
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ExternalSource } from '../shared/external-source.model';
|
import { ExternalSource } from '../shared/external-source.model';
|
||||||
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
|
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service for retrieving local and external entries information during a relation lookup
|
* A service for retrieving local and external entries information during a relation lookup
|
||||||
@@ -35,7 +36,8 @@ export class LookupRelationService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
constructor(protected externalSourceService: ExternalSourceService,
|
constructor(protected externalSourceService: ExternalSourceService,
|
||||||
protected searchService: SearchService) {
|
protected searchService: SearchService,
|
||||||
|
protected requestService: RequestService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,4 +93,11 @@ export class LookupRelationService {
|
|||||||
startWith(0)
|
startWith(0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove cached requests from local results
|
||||||
|
*/
|
||||||
|
removeLocalResultsCache() {
|
||||||
|
this.searchService.getEndpoint().subscribe((href) => this.requestService.removeByHrefSubstring(href));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { type } from '../../../shared/ngrx/type';
|
import {type} from '../../../shared/ngrx/type';
|
||||||
import { Action } from '@ngrx/store';
|
import {Action} from '@ngrx/store';
|
||||||
import { Identifiable } from './object-updates.reducer';
|
import {Identifiable} from './object-updates.reducer';
|
||||||
import { INotification } from '../../../shared/notifications/models/notification.model';
|
import {INotification} from '../../../shared/notifications/models/notification.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of ObjectUpdatesAction type definitions
|
* The list of ObjectUpdatesAction type definitions
|
||||||
@@ -11,6 +11,7 @@ export const ObjectUpdatesActionTypes = {
|
|||||||
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
|
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
|
||||||
SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'),
|
SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'),
|
||||||
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
|
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
|
||||||
|
SELECT_VIRTUAL_METADATA: type('dspace/core/cache/object-updates/SELECT_VIRTUAL_METADATA'),
|
||||||
DISCARD: type('dspace/core/cache/object-updates/DISCARD'),
|
DISCARD: type('dspace/core/cache/object-updates/DISCARD'),
|
||||||
REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
|
REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
|
||||||
REMOVE: type('dspace/core/cache/object-updates/REMOVE'),
|
REMOVE: type('dspace/core/cache/object-updates/REMOVE'),
|
||||||
@@ -83,6 +84,41 @@ export class AddFieldUpdateAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to select/deselect virtual metadata in the ObjectUpdates state for a certain page url
|
||||||
|
*/
|
||||||
|
export class SelectVirtualMetadataAction implements Action {
|
||||||
|
|
||||||
|
type = ObjectUpdatesActionTypes.SELECT_VIRTUAL_METADATA;
|
||||||
|
payload: {
|
||||||
|
url: string,
|
||||||
|
source: string,
|
||||||
|
uuid: string,
|
||||||
|
select: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new SelectVirtualMetadataAction
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the unique url of the page for which a field update is added
|
||||||
|
* @param source
|
||||||
|
* the id of the relationship which adds the virtual metadata
|
||||||
|
* @param uuid
|
||||||
|
* the id of the item which has the virtual metadata
|
||||||
|
* @param select
|
||||||
|
* whether to select or deselect the virtual metadata to be saved as real metadata
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
url: string,
|
||||||
|
source: string,
|
||||||
|
uuid: string,
|
||||||
|
select: boolean,
|
||||||
|
) {
|
||||||
|
this.payload = { url, source, uuid, select: select};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An ngrx action to set the editable state of an existing field in the ObjectUpdates state for a certain page url
|
* An ngrx action to set the editable state of an existing field in the ObjectUpdates state for a certain page url
|
||||||
*/
|
*/
|
||||||
@@ -242,4 +278,5 @@ export type ObjectUpdatesAction
|
|||||||
| DiscardObjectUpdatesAction
|
| DiscardObjectUpdatesAction
|
||||||
| ReinstateObjectUpdatesAction
|
| ReinstateObjectUpdatesAction
|
||||||
| RemoveObjectUpdatesAction
|
| RemoveObjectUpdatesAction
|
||||||
| RemoveFieldUpdateAction;
|
| RemoveFieldUpdateAction
|
||||||
|
| SelectVirtualMetadataAction;
|
||||||
|
@@ -5,10 +5,11 @@ import {
|
|||||||
FieldChangeType,
|
FieldChangeType,
|
||||||
InitializeFieldsAction,
|
InitializeFieldsAction,
|
||||||
ReinstateObjectUpdatesAction,
|
ReinstateObjectUpdatesAction,
|
||||||
RemoveFieldUpdateAction, RemoveObjectUpdatesAction,
|
RemoveFieldUpdateAction, RemoveObjectUpdatesAction, SelectVirtualMetadataAction,
|
||||||
SetEditableFieldUpdateAction, SetValidFieldUpdateAction
|
SetEditableFieldUpdateAction, SetValidFieldUpdateAction
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { OBJECT_UPDATES_TRASH_PATH, objectUpdatesReducer } from './object-updates.reducer';
|
import { OBJECT_UPDATES_TRASH_PATH, objectUpdatesReducer } from './object-updates.reducer';
|
||||||
|
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
||||||
|
|
||||||
class NullAction extends RemoveFieldUpdateAction {
|
class NullAction extends RemoveFieldUpdateAction {
|
||||||
type = null;
|
type = null;
|
||||||
@@ -44,6 +45,7 @@ const identifiable3 = {
|
|||||||
language: null,
|
language: null,
|
||||||
value: 'Unchanged value'
|
value: 'Unchanged value'
|
||||||
};
|
};
|
||||||
|
const relationship: Relationship = Object.assign(new Relationship(), {uuid: 'test relationship uuid'});
|
||||||
|
|
||||||
const modDate = new Date(2010, 2, 11);
|
const modDate = new Date(2010, 2, 11);
|
||||||
const uuid = identifiable1.uuid;
|
const uuid = identifiable1.uuid;
|
||||||
@@ -79,7 +81,10 @@ describe('objectUpdatesReducer', () => {
|
|||||||
changeType: FieldChangeType.ADD
|
changeType: FieldChangeType.ADD
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
lastModified: modDate
|
lastModified: modDate,
|
||||||
|
virtualMetadataSources: {
|
||||||
|
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,7 +107,10 @@ describe('objectUpdatesReducer', () => {
|
|||||||
isValid: true
|
isValid: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
lastModified: modDate
|
lastModified: modDate,
|
||||||
|
virtualMetadataSources: {
|
||||||
|
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
[url + OBJECT_UPDATES_TRASH_PATH]: {
|
[url + OBJECT_UPDATES_TRASH_PATH]: {
|
||||||
fieldStates: {
|
fieldStates: {
|
||||||
@@ -133,7 +141,10 @@ describe('objectUpdatesReducer', () => {
|
|||||||
changeType: FieldChangeType.ADD
|
changeType: FieldChangeType.ADD
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
lastModified: modDate
|
lastModified: modDate,
|
||||||
|
virtualMetadataSources: {
|
||||||
|
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -195,6 +206,12 @@ describe('objectUpdatesReducer', () => {
|
|||||||
objectUpdatesReducer(testState, action);
|
objectUpdatesReducer(testState, action);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should perform the SELECT_VIRTUAL_METADATA action without affecting the previous state', () => {
|
||||||
|
const action = new SelectVirtualMetadataAction(url, relationship.uuid, identifiable1.uuid, true);
|
||||||
|
// testState has already been frozen above
|
||||||
|
objectUpdatesReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => {
|
it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => {
|
||||||
const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate);
|
const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate);
|
||||||
|
|
||||||
@@ -213,6 +230,7 @@ describe('objectUpdatesReducer', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
fieldUpdates: {},
|
fieldUpdates: {},
|
||||||
|
virtualMetadataSources: {},
|
||||||
lastModified: modDate
|
lastModified: modDate
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -7,9 +7,13 @@ import {
|
|||||||
ObjectUpdatesActionTypes,
|
ObjectUpdatesActionTypes,
|
||||||
ReinstateObjectUpdatesAction,
|
ReinstateObjectUpdatesAction,
|
||||||
RemoveFieldUpdateAction,
|
RemoveFieldUpdateAction,
|
||||||
RemoveObjectUpdatesAction, SetEditableFieldUpdateAction, SetValidFieldUpdateAction
|
RemoveObjectUpdatesAction,
|
||||||
|
SetEditableFieldUpdateAction,
|
||||||
|
SetValidFieldUpdateAction,
|
||||||
|
SelectVirtualMetadataAction,
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||||
|
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path where discarded objects are saved
|
* Path where discarded objects are saved
|
||||||
@@ -42,7 +46,7 @@ export interface Identifiable {
|
|||||||
/**
|
/**
|
||||||
* The state of a single field update
|
* The state of a single field update
|
||||||
*/
|
*/
|
||||||
export interface FieldUpdate {
|
export interface FieldUpdate {
|
||||||
field: Identifiable,
|
field: Identifiable,
|
||||||
changeType: FieldChangeType
|
changeType: FieldChangeType
|
||||||
}
|
}
|
||||||
@@ -54,12 +58,36 @@ export interface FieldUpdates {
|
|||||||
[uuid: string]: FieldUpdate;
|
[uuid: string]: FieldUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The states of all virtual metadata selections available for a single page, mapped by the relationship uuid
|
||||||
|
*/
|
||||||
|
export interface VirtualMetadataSources {
|
||||||
|
[source: string]: VirtualMetadataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selection of virtual metadata for a relationship, mapped by the uuid of either the item or the relationship type
|
||||||
|
*/
|
||||||
|
export interface VirtualMetadataSource {
|
||||||
|
[uuid: string]: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fieldupdate interface which represents a relationship selected to be deleted,
|
||||||
|
* along with a selection of the virtual metadata to keep
|
||||||
|
*/
|
||||||
|
export interface DeleteRelationship extends Relationship {
|
||||||
|
keepLeftVirtualMetadata: boolean,
|
||||||
|
keepRightVirtualMetadata: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The updated state of a single page
|
* The updated state of a single page
|
||||||
*/
|
*/
|
||||||
export interface ObjectUpdatesEntry {
|
export interface ObjectUpdatesEntry {
|
||||||
fieldStates: FieldStates;
|
fieldStates: FieldStates;
|
||||||
fieldUpdates: FieldUpdates
|
fieldUpdates: FieldUpdates;
|
||||||
|
virtualMetadataSources: VirtualMetadataSources;
|
||||||
lastModified: Date;
|
lastModified: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +124,9 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
|
|||||||
case ObjectUpdatesActionTypes.ADD_FIELD: {
|
case ObjectUpdatesActionTypes.ADD_FIELD: {
|
||||||
return addFieldUpdate(state, action as AddFieldUpdateAction);
|
return addFieldUpdate(state, action as AddFieldUpdateAction);
|
||||||
}
|
}
|
||||||
|
case ObjectUpdatesActionTypes.SELECT_VIRTUAL_METADATA: {
|
||||||
|
return selectVirtualMetadata(state, action as SelectVirtualMetadataAction);
|
||||||
|
}
|
||||||
case ObjectUpdatesActionTypes.DISCARD: {
|
case ObjectUpdatesActionTypes.DISCARD: {
|
||||||
return discardObjectUpdates(state, action as DiscardObjectUpdatesAction);
|
return discardObjectUpdates(state, action as DiscardObjectUpdatesAction);
|
||||||
}
|
}
|
||||||
@@ -135,6 +166,7 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
|
|||||||
state[url],
|
state[url],
|
||||||
{ fieldStates: fieldStates },
|
{ fieldStates: fieldStates },
|
||||||
{ fieldUpdates: {} },
|
{ fieldUpdates: {} },
|
||||||
|
{ virtualMetadataSources: {} },
|
||||||
{ lastModified: lastModifiedServer }
|
{ lastModified: lastModifiedServer }
|
||||||
);
|
);
|
||||||
return Object.assign({}, state, { [url]: newPageState });
|
return Object.assign({}, state, { [url]: newPageState });
|
||||||
@@ -169,6 +201,51 @@ function addFieldUpdate(state: any, action: AddFieldUpdateAction) {
|
|||||||
return Object.assign({}, state, { [url]: newPageState });
|
return Object.assign({}, state, { [url]: newPageState });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the selected virtual metadata in the store
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform on the current state
|
||||||
|
*/
|
||||||
|
function selectVirtualMetadata(state: any, action: SelectVirtualMetadataAction) {
|
||||||
|
|
||||||
|
const url: string = action.payload.url;
|
||||||
|
const source: string = action.payload.source;
|
||||||
|
const uuid: string = action.payload.uuid;
|
||||||
|
const select: boolean = action.payload.select;
|
||||||
|
|
||||||
|
const pageState: ObjectUpdatesEntry = state[url] || {};
|
||||||
|
|
||||||
|
const virtualMetadataSource = Object.assign(
|
||||||
|
{},
|
||||||
|
pageState.virtualMetadataSources[source],
|
||||||
|
{
|
||||||
|
[uuid]: select,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const virtualMetadataSources = Object.assign(
|
||||||
|
{},
|
||||||
|
pageState.virtualMetadataSources,
|
||||||
|
{
|
||||||
|
[source]: virtualMetadataSource,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const newPageState = Object.assign(
|
||||||
|
{},
|
||||||
|
pageState,
|
||||||
|
{virtualMetadataSources: virtualMetadataSources},
|
||||||
|
);
|
||||||
|
|
||||||
|
return Object.assign(
|
||||||
|
{},
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
[url]: newPageState,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discard all updates for a specific action's url in the store
|
* Discard all updates for a specific action's url in the store
|
||||||
* @param state The current state
|
* @param state The current state
|
||||||
|
@@ -4,13 +4,14 @@ import { ObjectUpdatesService } from './object-updates.service';
|
|||||||
import {
|
import {
|
||||||
DiscardObjectUpdatesAction,
|
DiscardObjectUpdatesAction,
|
||||||
FieldChangeType,
|
FieldChangeType,
|
||||||
InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction,
|
InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction,
|
||||||
SetEditableFieldUpdateAction
|
SetEditableFieldUpdateAction
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { Notification } from '../../../shared/notifications/models/notification.model';
|
import { Notification } from '../../../shared/notifications/models/notification.model';
|
||||||
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
||||||
import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer';
|
import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer';
|
||||||
|
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
||||||
|
|
||||||
describe('ObjectUpdatesService', () => {
|
describe('ObjectUpdatesService', () => {
|
||||||
let service: ObjectUpdatesService;
|
let service: ObjectUpdatesService;
|
||||||
@@ -22,6 +23,7 @@ describe('ObjectUpdatesService', () => {
|
|||||||
const identifiable2 = { uuid: '26cbb5ce-5786-4e57-a394-b9fcf8eaf241' };
|
const identifiable2 = { uuid: '26cbb5ce-5786-4e57-a394-b9fcf8eaf241' };
|
||||||
const identifiable3 = { uuid: 'c5d2c2f7-d757-48bf-84cc-8c9229c8407e' };
|
const identifiable3 = { uuid: 'c5d2c2f7-d757-48bf-84cc-8c9229c8407e' };
|
||||||
const identifiables = [identifiable1, identifiable2];
|
const identifiables = [identifiable1, identifiable2];
|
||||||
|
const relationship: Relationship = Object.assign(new Relationship(), {uuid: 'test relationship uuid'});
|
||||||
|
|
||||||
const fieldUpdates = {
|
const fieldUpdates = {
|
||||||
[identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE },
|
[identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE },
|
||||||
@@ -38,11 +40,11 @@ describe('ObjectUpdatesService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const objectEntry = {
|
const objectEntry = {
|
||||||
fieldStates, fieldUpdates, lastModified: modDate
|
fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}
|
||||||
};
|
};
|
||||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||||
spyOn(store, 'dispatch');
|
spyOn(store, 'dispatch');
|
||||||
service = new ObjectUpdatesService(store);
|
service = (new ObjectUpdatesService(store));
|
||||||
|
|
||||||
spyOn(service as any, 'getObjectEntry').and.returnValue(observableOf(objectEntry));
|
spyOn(service as any, 'getObjectEntry').and.returnValue(observableOf(objectEntry));
|
||||||
spyOn(service as any, 'getFieldState').and.callFake((uuid) => {
|
spyOn(service as any, 'getFieldState').and.callFake((uuid) => {
|
||||||
@@ -251,4 +253,10 @@ describe('ObjectUpdatesService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setSelectedVirtualMetadata', () => {
|
||||||
|
it('should dispatch a SELECT_VIRTUAL_METADATA action with the correct URL, relationship, identifiable and boolean', () => {
|
||||||
|
service.setSelectedVirtualMetadata(url, relationship.uuid, identifiable1.uuid, true);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SelectVirtualMetadataAction(url, relationship.uuid, identifiable1.uuid, true));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -8,7 +8,8 @@ import {
|
|||||||
Identifiable,
|
Identifiable,
|
||||||
OBJECT_UPDATES_TRASH_PATH,
|
OBJECT_UPDATES_TRASH_PATH,
|
||||||
ObjectUpdatesEntry,
|
ObjectUpdatesEntry,
|
||||||
ObjectUpdatesState
|
ObjectUpdatesState,
|
||||||
|
VirtualMetadataSource
|
||||||
} from './object-updates.reducer';
|
} from './object-updates.reducer';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
@@ -18,10 +19,11 @@ import {
|
|||||||
InitializeFieldsAction,
|
InitializeFieldsAction,
|
||||||
ReinstateObjectUpdatesAction,
|
ReinstateObjectUpdatesAction,
|
||||||
RemoveFieldUpdateAction,
|
RemoveFieldUpdateAction,
|
||||||
|
SelectVirtualMetadataAction,
|
||||||
SetEditableFieldUpdateAction,
|
SetEditableFieldUpdateAction,
|
||||||
SetValidFieldUpdateAction
|
SetValidFieldUpdateAction
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
|
||||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { INotification } from '../../../shared/notifications/models/notification.model';
|
import { INotification } from '../../../shared/notifications/models/notification.model';
|
||||||
|
|
||||||
@@ -37,6 +39,10 @@ function filterByUrlAndUUIDFieldStateSelector(url: string, uuid: string): Memoiz
|
|||||||
return createSelector(filterByUrlObjectUpdatesStateSelector(url), (state: ObjectUpdatesEntry) => state.fieldStates[uuid]);
|
return createSelector(filterByUrlObjectUpdatesStateSelector(url), (state: ObjectUpdatesEntry) => state.fieldStates[uuid]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function virtualMetadataSourceSelector(url: string, source: string): MemoizedSelector<CoreState, VirtualMetadataSource> {
|
||||||
|
return createSelector(filterByUrlObjectUpdatesStateSelector(url), (state: ObjectUpdatesEntry) => state.virtualMetadataSources[source]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that dispatches and reads from the ObjectUpdates' state in the store
|
* Service that dispatches and reads from the ObjectUpdates' state in the store
|
||||||
*/
|
*/
|
||||||
@@ -91,18 +97,24 @@ export class ObjectUpdatesService {
|
|||||||
*/
|
*/
|
||||||
getFieldUpdates(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
|
getFieldUpdates(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
|
||||||
const objectUpdates = this.getObjectEntry(url);
|
const objectUpdates = this.getObjectEntry(url);
|
||||||
return objectUpdates.pipe(map((objectEntry) => {
|
return objectUpdates.pipe(
|
||||||
const fieldUpdates: FieldUpdates = {};
|
switchMap((objectEntry) => {
|
||||||
Object.keys(objectEntry.fieldStates).forEach((uuid) => {
|
const fieldUpdates: FieldUpdates = {};
|
||||||
let fieldUpdate = objectEntry.fieldUpdates[uuid];
|
if (hasValue(objectEntry)) {
|
||||||
if (isEmpty(fieldUpdate)) {
|
Object.keys(objectEntry.fieldStates).forEach((uuid) => {
|
||||||
const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid);
|
fieldUpdates[uuid] = objectEntry.fieldUpdates[uuid];
|
||||||
fieldUpdate = { field: identifiable, changeType: undefined };
|
});
|
||||||
}
|
}
|
||||||
fieldUpdates[uuid] = fieldUpdate;
|
return this.getFieldUpdatesExclusive(url, initialFields).pipe(
|
||||||
});
|
map((fieldUpdatesExclusive) => {
|
||||||
return fieldUpdates;
|
Object.keys(fieldUpdatesExclusive).forEach((uuid) => {
|
||||||
}))
|
fieldUpdates[uuid] = fieldUpdatesExclusive[uuid];
|
||||||
|
});
|
||||||
|
return fieldUpdates;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -195,6 +207,34 @@ export class ObjectUpdatesService {
|
|||||||
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the virtual metadata of a given item is selected to be saved as real metadata
|
||||||
|
* @param url The URL of the page on which the field resides
|
||||||
|
* @param relationship The id of the relationship for which to check whether the virtual metadata is selected to be
|
||||||
|
* saved as real metadata
|
||||||
|
* @param item The id of the item for which to check whether the virtual metadata is selected to be
|
||||||
|
* saved as real metadata
|
||||||
|
*/
|
||||||
|
isSelectedVirtualMetadata(url: string, relationship: string, item: string): Observable<boolean> {
|
||||||
|
|
||||||
|
return this.store
|
||||||
|
.pipe(
|
||||||
|
select(virtualMetadataSourceSelector(url, relationship)),
|
||||||
|
map((virtualMetadataSource) => virtualMetadataSource && virtualMetadataSource[item]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to dispatch a SelectVirtualMetadataAction to the store
|
||||||
|
* @param url The page's URL for which the changes are saved
|
||||||
|
* @param relationship the relationship for which virtual metadata is selected
|
||||||
|
* @param uuid the selection identifier, can either be the item uuid or the relationship type uuid
|
||||||
|
* @param selected whether or not to select the virtual metadata to be saved
|
||||||
|
*/
|
||||||
|
setSelectedVirtualMetadata(url: string, relationship: string, uuid: string, selected: boolean) {
|
||||||
|
this.store.dispatch(new SelectVirtualMetadataAction(url, relationship, uuid, selected));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches a SetEditableFieldUpdateAction to the store to set a field's editable state
|
* Dispatches a SetEditableFieldUpdateAction to the store to set a field's editable state
|
||||||
* @param url The URL of the page on which the field resides
|
* @param url The URL of the page on which the field resides
|
||||||
|
@@ -54,10 +54,12 @@ describe('RelationshipService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const relatedItem1 = Object.assign(new Item(), {
|
const relatedItem1 = Object.assign(new Item(), {
|
||||||
|
self: 'fake-item-url/author1',
|
||||||
id: 'author1',
|
id: 'author1',
|
||||||
uuid: 'author1'
|
uuid: 'author1'
|
||||||
});
|
});
|
||||||
const relatedItem2 = Object.assign(new Item(), {
|
const relatedItem2 = Object.assign(new Item(), {
|
||||||
|
self: 'fake-item-url/author2',
|
||||||
id: 'author2',
|
id: 'author2',
|
||||||
uuid: 'author2'
|
uuid: 'author2'
|
||||||
});
|
});
|
||||||
@@ -112,19 +114,19 @@ describe('RelationshipService', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(service, 'findById').and.returnValue(getRemotedataObservable(relationship1));
|
spyOn(service, 'findById').and.returnValue(getRemotedataObservable(relationship1));
|
||||||
spyOn(objectCache, 'remove');
|
spyOn(objectCache, 'remove');
|
||||||
service.deleteRelationship(relationships[0].uuid).subscribe();
|
service.deleteRelationship(relationships[0].uuid, 'right').subscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send a DeleteRequest', () => {
|
it('should send a DeleteRequest', () => {
|
||||||
const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationship1.uuid);
|
const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationship1.uuid + '?copyVirtualMetadata=right');
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear the related items their cache', () => {
|
it('should clear the cache of the related items', () => {
|
||||||
expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1.self);
|
expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1.self);
|
||||||
expect(objectCache.remove).toHaveBeenCalledWith(item.self);
|
expect(objectCache.remove).toHaveBeenCalledWith(item.self);
|
||||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.uuid);
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self);
|
||||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.uuid);
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,35 +1,47 @@
|
|||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
|
||||||
import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs';
|
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
|
|
||||||
import { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
|
||||||
import { AppState, keySelector } from '../../app.reducer';
|
|
||||||
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
|
||||||
import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
|
import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
|
||||||
import { RemoveNameVariantAction, SetNameVariantAction } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions';
|
import { RequestService } from './request.service';
|
||||||
import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
|
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
|
||||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
|
|
||||||
import { SearchParam } from '../cache/models/search-param.model';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
|
||||||
import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models';
|
|
||||||
import { RestResponse } from '../cache/response.models';
|
|
||||||
import { CoreState } from '../core.reducers';
|
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
|
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
|
||||||
|
import {
|
||||||
|
configureRequest,
|
||||||
|
getRemoteDataPayload,
|
||||||
|
getResponseFromEntry,
|
||||||
|
getSucceededRemoteData
|
||||||
|
} from '../shared/operators';
|
||||||
|
import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { Relationship } from '../shared/item-relationships/relationship.model';
|
||||||
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||||
import { RemoteData, RemoteDataState } from './remote-data';
|
import { RemoteData, RemoteDataState } from './remote-data';
|
||||||
|
import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
import { ItemDataService } from './item-data.service';
|
import { ItemDataService } from './item-data.service';
|
||||||
import { Relationship } from '../shared/item-relationships/relationship.model';
|
import {
|
||||||
import { Item } from '../shared/item.model';
|
compareArraysUsingIds,
|
||||||
|
paginatedRelationsToItems,
|
||||||
|
relationsToItems
|
||||||
|
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
import { RequestService } from './request.service';
|
import { SearchParam } from '../cache/models/search-param.model';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
|
import { AppState, keySelector } from '../../app.reducer';
|
||||||
|
import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
|
||||||
|
import {
|
||||||
|
RemoveNameVariantAction,
|
||||||
|
SetNameVariantAction
|
||||||
|
} from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions';
|
||||||
|
|
||||||
const relationshipListsStateSelector = (state: AppState) => state.relationshipLists;
|
const relationshipListsStateSelector = (state: AppState) => state.relationshipLists;
|
||||||
|
|
||||||
@@ -81,15 +93,22 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
* Send a delete request for a relationship by ID
|
* Send a delete request for a relationship by ID
|
||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
deleteRelationship(id: string): Observable<RestResponse> {
|
deleteRelationship(id: string, copyVirtualMetadata: string): Observable<RestResponse> {
|
||||||
return this.getRelationshipEndpoint(id).pipe(
|
return this.getRelationshipEndpoint(id).pipe(
|
||||||
isNotEmptyOperator(),
|
isNotEmptyOperator(),
|
||||||
take(1),
|
take(1),
|
||||||
map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)),
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) =>
|
||||||
|
new DeleteRequest(this.requestService.generateRequestId(), endpointURL + '?copyVirtualMetadata=' + copyVirtualMetadata)
|
||||||
|
),
|
||||||
configureRequest(this.requestService),
|
configureRequest(this.requestService),
|
||||||
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
|
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
|
||||||
getResponseFromEntry(),
|
getResponseFromEntry(),
|
||||||
tap(() => this.removeRelationshipItemsFromCacheByRelationship(id))
|
switchMap((response) =>
|
||||||
|
this.clearRelatedCache(id).pipe(
|
||||||
|
map(() => response),
|
||||||
|
)
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,4 +436,26 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
return update$;
|
return update$;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear object and request caches of the items related to a relationship (left and right items)
|
||||||
|
* @param uuid The uuid of the relationship for which to clear the related items from the cache
|
||||||
|
*/
|
||||||
|
clearRelatedCache(uuid: string): Observable<void> {
|
||||||
|
return this.findById(uuid).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
switchMap((rd: RemoteData<Relationship>) =>
|
||||||
|
observableCombineLatest(
|
||||||
|
rd.payload.leftItem.pipe(getSucceededRemoteData()),
|
||||||
|
rd.payload.rightItem.pipe(getSucceededRemoteData())
|
||||||
|
)
|
||||||
|
),
|
||||||
|
take(1),
|
||||||
|
map(([leftItem, rightItem]) => {
|
||||||
|
this.objectCache.remove(leftItem.payload.self);
|
||||||
|
this.objectCache.remove(rightItem.payload.self);
|
||||||
|
this.requestService.removeByHrefSubstring(leftItem.payload.self);
|
||||||
|
this.requestService.removeByHrefSubstring(rightItem.payload.self);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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 */
|
||||||
@@ -380,6 +381,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;
|
||||||
|
}
|
@@ -24,6 +24,11 @@ export class ExternalSourceEntry extends ListableObject {
|
|||||||
*/
|
*/
|
||||||
value: string;
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the external source this entry originates from
|
||||||
|
*/
|
||||||
|
externalSource: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata of the entry
|
* Metadata of the entry
|
||||||
*/
|
*/
|
||||||
|
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;
|
||||||
|
}
|
@@ -1,2 +1,4 @@
|
|||||||
<div>{{object.display}}</div>
|
<div class="d-inline-block">
|
||||||
<div *ngIf="uri"><a target="_blank" [href]="uri.value">{{uri.value}}</a></div>
|
<div>{{object.display}}</div>
|
||||||
|
<div *ngIf="uri"><a target="_blank" [href]="uri.value">{{uri.value}}</a></div>
|
||||||
|
</div>
|
||||||
|
@@ -3,7 +3,7 @@ import { ExternalSourceEntry } from '../../../../../core/shared/external-source-
|
|||||||
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
import { Context } from '../../../../../core/shared/context.model';
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
import { Metadata } from '../../../../../core/shared/metadata.utils';
|
import { Metadata } from '../../../../../core/shared/metadata.utils';
|
||||||
import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||||
|
|
||||||
|
@@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -97,6 +97,7 @@ import { PaginatedList } from '../../../../core/data/paginated-list';
|
|||||||
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
|
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
|
||||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
|
|
||||||
export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null {
|
export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null {
|
||||||
switch (model.type) {
|
switch (model.type) {
|
||||||
@@ -185,6 +186,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
|||||||
hasRelationLookup: boolean;
|
hasRelationLookup: boolean;
|
||||||
modalRef: NgbModalRef;
|
modalRef: NgbModalRef;
|
||||||
item: Item;
|
item: Item;
|
||||||
|
collection: Collection;
|
||||||
listId: string;
|
listId: string;
|
||||||
searchConfig: string;
|
searchConfig: string;
|
||||||
|
|
||||||
@@ -236,19 +238,18 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
|||||||
if (this.hasRelationLookup) {
|
if (this.hasRelationLookup) {
|
||||||
|
|
||||||
this.listId = 'list-' + this.model.relationship.relationshipType;
|
this.listId = 'list-' + this.model.relationship.relationshipType;
|
||||||
const item$ = this.submissionObjectService
|
|
||||||
|
const submissionObject$ = this.submissionObjectService
|
||||||
.findById(this.model.submissionId).pipe(
|
.findById(this.model.submissionId).pipe(
|
||||||
getAllSucceededRemoteData(),
|
getAllSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload()
|
||||||
switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable<RemoteData<Item>>)
|
|
||||||
.pipe(
|
|
||||||
getAllSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const item$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
|
||||||
|
const collection$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.collection as Observable<RemoteData<Collection>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
|
||||||
|
|
||||||
this.subs.push(item$.subscribe((item) => this.item = item));
|
this.subs.push(item$.subscribe((item) => this.item = item));
|
||||||
|
this.subs.push(collection$.subscribe((collection) => this.collection = collection));
|
||||||
this.reorderables$ = item$.pipe(
|
this.reorderables$ = item$.pipe(
|
||||||
switchMap((item) => this.relationService.getItemRelationshipsByLabel(item, this.model.relationship.relationshipType)
|
switchMap((item) => this.relationService.getItemRelationshipsByLabel(item, this.model.relationship.relationshipType)
|
||||||
.pipe(
|
.pipe(
|
||||||
@@ -343,6 +344,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
|||||||
modalComp.label = this.model.label;
|
modalComp.label = this.model.label;
|
||||||
modalComp.metadataFields = this.model.metadataFields;
|
modalComp.metadataFields = this.model.metadataFields;
|
||||||
modalComp.item = this.item;
|
modalComp.item = this.item;
|
||||||
|
modalComp.collection = this.collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -25,12 +25,14 @@
|
|||||||
[title]="'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + source.id | translate : {count: (totalExternal$ | async)[idx]}">
|
[title]="'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + source.id | translate : {count: (totalExternal$ | async)[idx]}">
|
||||||
<ng-template ngbTabContent>
|
<ng-template ngbTabContent>
|
||||||
<ds-dynamic-lookup-relation-external-source-tab
|
<ds-dynamic-lookup-relation-external-source-tab
|
||||||
|
[label]="label"
|
||||||
[listId]="listId"
|
[listId]="listId"
|
||||||
[repeatable]="repeatable"
|
[item]="item"
|
||||||
|
[collection]="collection"
|
||||||
|
[relationship]="relationshipOptions"
|
||||||
[context]="context"
|
[context]="context"
|
||||||
[externalSource]="source"
|
[externalSource]="source"
|
||||||
(selectObject)="select($event)"
|
(importedObject)="imported($event)"
|
||||||
(deselectObject)="deselect($event)"
|
|
||||||
class="d-block pt-3">
|
class="d-block pt-3">
|
||||||
</ds-dynamic-lookup-relation-external-source-tab>
|
</ds-dynamic-lookup-relation-external-source-tab>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@@ -1,3 +1,11 @@
|
|||||||
.modal-footer {
|
.modal-footer {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Render child-modals slightly smaller than this modal to avoid complete overlap */
|
||||||
|
:host {
|
||||||
|
::ng-deep .modal-content {
|
||||||
|
width: 90%;
|
||||||
|
margin: 5%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -66,6 +66,11 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
*/
|
*/
|
||||||
item;
|
item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The collection we're submitting an item to
|
||||||
|
*/
|
||||||
|
collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the selection repeatable?
|
* Is the selection repeatable?
|
||||||
*/
|
*/
|
||||||
@@ -233,6 +238,15 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an external object has been imported, resets the total values and adds the object to the selected list
|
||||||
|
* @param object
|
||||||
|
*/
|
||||||
|
imported(object) {
|
||||||
|
this.setTotals();
|
||||||
|
this.select(object);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate and set the total entries available for each tab
|
* Calculate and set the total entries available for each tab
|
||||||
*/
|
*/
|
||||||
|
@@ -10,13 +10,13 @@
|
|||||||
<ng-container *ngVar="(entriesRD$ | async) as entriesRD">
|
<ng-container *ngVar="(entriesRD$ | async) as entriesRD">
|
||||||
<ds-viewable-collection *ngIf="entriesRD?.hasSucceeded && !entriesRD?.isLoading && entriesRD?.payload?.page?.length > 0" @fadeIn
|
<ds-viewable-collection *ngIf="entriesRD?.hasSucceeded && !entriesRD?.isLoading && entriesRD?.payload?.page?.length > 0" @fadeIn
|
||||||
[objects]="entriesRD"
|
[objects]="entriesRD"
|
||||||
[selectable]="true"
|
|
||||||
[selectionConfig]="{ repeatable: repeatable, listId: listId }"
|
[selectionConfig]="{ repeatable: repeatable, listId: listId }"
|
||||||
[config]="initialPagination"
|
[config]="initialPagination"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[context]="context"
|
[context]="context"
|
||||||
(deselectObject)="deselectObject.emit($event)"
|
[importable]="true"
|
||||||
(selectObject)="selectObject.emit($event)">
|
[importConfig]="importConfig"
|
||||||
|
(importObject)="import($event)">
|
||||||
</ds-viewable-collection>
|
</ds-viewable-collection>
|
||||||
<ds-loading *ngIf="!entriesRD || !entriesRD?.payload || entriesRD?.isLoading"
|
<ds-loading *ngIf="!entriesRD || !entriesRD?.payload || entriesRD?.isLoading"
|
||||||
message="{{'loading.search-results' | translate}}"></ds-loading>
|
message="{{'loading.search-results' | translate}}"></ds-loading>
|
||||||
|
@@ -3,7 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
import { VarDirective } from '../../../../../utils/var.directive';
|
import { VarDirective } from '../../../../../utils/var.directive';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model';
|
||||||
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
|
||||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
@@ -18,12 +18,20 @@ import { ExternalSource } from '../../../../../../core/shared/external-source.mo
|
|||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { ExternalSourceEntry } from '../../../../../../core/shared/external-source-entry.model';
|
import { ExternalSourceEntry } from '../../../../../../core/shared/external-source-entry.model';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service';
|
||||||
|
import { Item } from '../../../../../../core/shared/item.model';
|
||||||
|
import { Collection } from '../../../../../../core/shared/collection.model';
|
||||||
|
import { RelationshipOptions } from '../../../models/relationship-options.model';
|
||||||
|
import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/external-source-entry-import-modal.component';
|
||||||
|
|
||||||
describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
|
describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
|
||||||
let component: DsDynamicLookupRelationExternalSourceTabComponent;
|
let component: DsDynamicLookupRelationExternalSourceTabComponent;
|
||||||
let fixture: ComponentFixture<DsDynamicLookupRelationExternalSourceTabComponent>;
|
let fixture: ComponentFixture<DsDynamicLookupRelationExternalSourceTabComponent>;
|
||||||
let pSearchOptions;
|
let pSearchOptions;
|
||||||
let externalSourceService;
|
let externalSourceService;
|
||||||
|
let selectableListService;
|
||||||
|
let modalService;
|
||||||
|
|
||||||
const externalSource = {
|
const externalSource = {
|
||||||
id: 'orcidV2',
|
id: 'orcidV2',
|
||||||
@@ -68,6 +76,10 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
] as ExternalSourceEntry[];
|
] as ExternalSourceEntry[];
|
||||||
|
const item = Object.assign(new Item(), { id: 'submission-item' });
|
||||||
|
const collection = Object.assign(new Collection(), { id: 'submission-collection' });
|
||||||
|
const relationship = Object.assign(new RelationshipOptions(), { relationshipType: 'isAuthorOfPublication' });
|
||||||
|
const label = 'Author';
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
pSearchOptions = new PaginatedSearchOptions({
|
pSearchOptions = new PaginatedSearchOptions({
|
||||||
@@ -76,20 +88,22 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
|
|||||||
externalSourceService = jasmine.createSpyObj('externalSourceService', {
|
externalSourceService = jasmine.createSpyObj('externalSourceService', {
|
||||||
getExternalSourceEntries: createSuccessfulRemoteDataObject$(createPaginatedList(externalEntries))
|
getExternalSourceEntries: createSuccessfulRemoteDataObject$(createPaginatedList(externalEntries))
|
||||||
});
|
});
|
||||||
|
selectableListService = jasmine.createSpyObj('selectableListService', ['selectSingle']);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
init();
|
init();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [DsDynamicLookupRelationExternalSourceTabComponent, VarDirective],
|
declarations: [DsDynamicLookupRelationExternalSourceTabComponent, VarDirective],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), BrowserAnimationsModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule.forRoot(), BrowserAnimationsModule],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: SearchConfigurationService, useValue: {
|
provide: SearchConfigurationService, useValue: {
|
||||||
paginatedSearchOptions: observableOf(pSearchOptions)
|
paginatedSearchOptions: observableOf(pSearchOptions)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ provide: ExternalSourceService, useValue: externalSourceService }
|
{ provide: ExternalSourceService, useValue: externalSourceService },
|
||||||
|
{ provide: SelectableListService, useValue: selectableListService }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -99,13 +113,18 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
|
|||||||
fixture = TestBed.createComponent(DsDynamicLookupRelationExternalSourceTabComponent);
|
fixture = TestBed.createComponent(DsDynamicLookupRelationExternalSourceTabComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
component.externalSource = externalSource;
|
component.externalSource = externalSource;
|
||||||
|
component.item = item;
|
||||||
|
component.collection = collection;
|
||||||
|
component.relationship = relationship;
|
||||||
|
component.label = label;
|
||||||
|
modalService = (component as any).modalService;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the external entries finished loading successfully', () => {
|
describe('when the external entries finished loading successfully', () => {
|
||||||
it('should display a ds-viewable-collection component', () => {
|
it('should display a ds-viewable-collection component', () => {
|
||||||
const collection = fixture.debugElement.query(By.css('ds-viewable-collection'));
|
const viewableCollection = fixture.debugElement.query(By.css('ds-viewable-collection'));
|
||||||
expect(collection).toBeDefined();
|
expect(viewableCollection).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,8 +135,8 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not display a ds-viewable-collection component', () => {
|
it('should not display a ds-viewable-collection component', () => {
|
||||||
const collection = fixture.debugElement.query(By.css('ds-viewable-collection'));
|
const viewableCollection = fixture.debugElement.query(By.css('ds-viewable-collection'));
|
||||||
expect(collection).toBeNull();
|
expect(viewableCollection).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display a ds-loading component', () => {
|
it('should display a ds-loading component', () => {
|
||||||
@@ -133,8 +152,8 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not display a ds-viewable-collection component', () => {
|
it('should not display a ds-viewable-collection component', () => {
|
||||||
const collection = fixture.debugElement.query(By.css('ds-viewable-collection'));
|
const viewableCollection = fixture.debugElement.query(By.css('ds-viewable-collection'));
|
||||||
expect(collection).toBeNull();
|
expect(viewableCollection).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display a ds-error component', () => {
|
it('should display a ds-error component', () => {
|
||||||
@@ -150,8 +169,8 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not display a ds-viewable-collection component', () => {
|
it('should not display a ds-viewable-collection component', () => {
|
||||||
const collection = fixture.debugElement.query(By.css('ds-viewable-collection'));
|
const viewableCollection = fixture.debugElement.query(By.css('ds-viewable-collection'));
|
||||||
expect(collection).toBeNull();
|
expect(viewableCollection).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display a message the list is empty', () => {
|
it('should display a message the list is empty', () => {
|
||||||
@@ -159,4 +178,15 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
|
|||||||
expect(empty).not.toBeNull();
|
expect(empty).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('import', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ importedObject: new EventEmitter<any>() }) }));
|
||||||
|
component.import(externalEntries[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open a new ExternalSourceEntryImportModalComponent', () => {
|
||||||
|
expect(modalService.open).toHaveBeenCalledWith(ExternalSourceEntryImportModalComponent, jasmine.any(Object))
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../../../../+my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../../../../../../+my-dspace-page/my-dspace-page.component';
|
||||||
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -14,6 +14,14 @@ import { Context } from '../../../../../../core/shared/context.model';
|
|||||||
import { ListableObject } from '../../../../../object-collection/shared/listable-object.model';
|
import { ListableObject } from '../../../../../object-collection/shared/listable-object.model';
|
||||||
import { fadeIn, fadeInOut } from '../../../../../animations/fade';
|
import { fadeIn, fadeInOut } from '../../../../../animations/fade';
|
||||||
import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model';
|
||||||
|
import { RelationshipOptions } from '../../../models/relationship-options.model';
|
||||||
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/external-source-entry-import-modal.component';
|
||||||
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
|
import { hasValue } from '../../../../../empty.util';
|
||||||
|
import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service';
|
||||||
|
import { Item } from '../../../../../../core/shared/item.model';
|
||||||
|
import { Collection } from '../../../../../../core/shared/collection.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-dynamic-lookup-relation-external-source-tab',
|
selector: 'ds-dynamic-lookup-relation-external-source-tab',
|
||||||
@@ -31,11 +39,12 @@ import { PaginationComponentOptions } from '../../../../../pagination/pagination
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* The tab displaying a list of importable entries for an external source
|
* Component rendering the tab content of an external source during submission lookup
|
||||||
|
* Shows a list of entries matching the current search query with the option to import them into the repository
|
||||||
*/
|
*/
|
||||||
export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit {
|
export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* The label to use to display i18n messages (describing the type of relationship)
|
* The label to use for all messages (added to the end of relevant i18n keys)
|
||||||
*/
|
*/
|
||||||
@Input() label: string;
|
@Input() label: string;
|
||||||
|
|
||||||
@@ -45,27 +54,32 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
|
|||||||
@Input() listId: string;
|
@Input() listId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the selection repeatable?
|
* The item in submission
|
||||||
*/
|
*/
|
||||||
@Input() repeatable: boolean;
|
@Input() item: Item;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The context to display lists
|
* The collection the user is submitting an item into
|
||||||
|
*/
|
||||||
|
@Input() collection: Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship-options for the current lookup
|
||||||
|
*/
|
||||||
|
@Input() relationship: RelationshipOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context to displaying lists for
|
||||||
*/
|
*/
|
||||||
@Input() context: Context;
|
@Input() context: Context;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an event to deselect an object from the list
|
* Emit an event when an object has been imported (or selected from similar local entries)
|
||||||
*/
|
*/
|
||||||
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
@Output() importedObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an event to select an object from the list
|
* The initial pagination options
|
||||||
*/
|
|
||||||
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The initial pagination to start with
|
|
||||||
*/
|
*/
|
||||||
initialPagination = Object.assign(new PaginationComponentOptions(), {
|
initialPagination = Object.assign(new PaginationComponentOptions(), {
|
||||||
id: 'submission-external-source-relation-list',
|
id: 'submission-external-source-relation-list',
|
||||||
@@ -82,15 +96,68 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
|
|||||||
*/
|
*/
|
||||||
entriesRD$: Observable<RemoteData<PaginatedList<ExternalSourceEntry>>>;
|
entriesRD$: Observable<RemoteData<PaginatedList<ExternalSourceEntry>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config to use for the import buttons
|
||||||
|
*/
|
||||||
|
importConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The modal for importing the entry
|
||||||
|
*/
|
||||||
|
modalRef: NgbModalRef;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription to the modal's importedObject event-emitter
|
||||||
|
*/
|
||||||
|
importObjectSub: Subscription;
|
||||||
|
|
||||||
constructor(private router: Router,
|
constructor(private router: Router,
|
||||||
public searchConfigService: SearchConfigurationService,
|
public searchConfigService: SearchConfigurationService,
|
||||||
private externalSourceService: ExternalSourceService) {
|
private externalSourceService: ExternalSourceService,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
private selectableListService: SelectableListService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the entries for the selected external source
|
||||||
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe(
|
this.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe(
|
||||||
switchMap((searchOptions: PaginatedSearchOptions) =>
|
switchMap((searchOptions: PaginatedSearchOptions) =>
|
||||||
this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions).pipe(startWith(undefined)))
|
this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions).pipe(startWith(undefined)))
|
||||||
)
|
);
|
||||||
|
this.importConfig = {
|
||||||
|
buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the import of an entry by opening up an import modal window
|
||||||
|
* @param entry The entry to import
|
||||||
|
*/
|
||||||
|
import(entry) {
|
||||||
|
this.modalRef = this.modalService.open(ExternalSourceEntryImportModalComponent, {
|
||||||
|
size: 'lg',
|
||||||
|
container: 'ds-dynamic-lookup-relation-modal'
|
||||||
|
});
|
||||||
|
const modalComp = this.modalRef.componentInstance;
|
||||||
|
modalComp.externalSourceEntry = entry;
|
||||||
|
modalComp.item = this.item;
|
||||||
|
modalComp.collection = this.collection;
|
||||||
|
modalComp.relationship = this.relationship;
|
||||||
|
modalComp.label = this.label;
|
||||||
|
this.importObjectSub = modalComp.importedObject.subscribe((object) => {
|
||||||
|
this.selectableListService.selectSingle(this.listId, object);
|
||||||
|
this.importedObject.emit(object);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from open subscriptions
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (hasValue(this.importObjectSub)) {
|
||||||
|
this.importObjectSub.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,61 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-title">{{ (labelPrefix + label + '.title') | translate }}</h4>
|
||||||
|
<button type="button" class="close" aria-label="Close button" aria-describedby="modal-title"
|
||||||
|
(click)="modal.dismiss()">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<h4>{{ (labelPrefix + 'head.' + externalSourceEntry.externalSource | translate) }}</h4>
|
||||||
|
<div id="external-source-entry-information" class="mb-3">
|
||||||
|
<div><span>{{externalSourceEntry.display}}</span></div>
|
||||||
|
<div *ngIf="uri"><a href="{{uri.value}}">{{uri.value}}</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>{{ (labelPrefix + 'select' | translate) }}</h4>
|
||||||
|
|
||||||
|
<div id="external-source-entry-entities" class="mb-3">
|
||||||
|
<h5 class="font-weight-bold">{{ (labelPrefix + 'entities' | translate) }}</h5>
|
||||||
|
|
||||||
|
<div id="external-source-entry-collection" class="mb-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="collection">{{ (labelPrefix + 'collection' | translate) }}</label>
|
||||||
|
<input type="text" class="form-control" id="collection" placeholder="Enter collection ID" [(ngModel)]="collectionId">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ds-search-results *ngIf="(localEntitiesRD$ | async)?.payload?.page?.length > 0"
|
||||||
|
[searchResults]="(localEntitiesRD$ | async)"
|
||||||
|
[sortConfig]="this.lookupRelationService.searchConfig?.sort"
|
||||||
|
[searchConfig]="this.lookupRelationService.searchConfig"
|
||||||
|
[selectable]="true"
|
||||||
|
[disableHeader]="true"
|
||||||
|
[hidePaginationDetail]="true"
|
||||||
|
[selectionConfig]="{ repeatable: false, listId: entityListId }"
|
||||||
|
[linkType]="linkTypes.ExternalLink"
|
||||||
|
[context]="context"
|
||||||
|
(deselectObject)="deselectEntity()"
|
||||||
|
(selectObject)="selectEntity($event)">
|
||||||
|
</ds-search-results>
|
||||||
|
<div class="ml-4">
|
||||||
|
<input class="form-check-input" type="radio" name="new-entity" id="new-entity" value="new-entity" (click)="selectNewEntity()" [checked]="selectedImportType === importType.NewEntity" />
|
||||||
|
<label class="form-check-label" for="new-entity">{{ (labelPrefix + 'entities.new' | translate) }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="external-source-entry-authority" *ngIf="authorityEnabled">
|
||||||
|
<h5 class="font-weight-bold">{{ (labelPrefix + 'authority' | translate) }}</h5>
|
||||||
|
|
||||||
|
<div class="ml-4">
|
||||||
|
<input class="form-check-input" type="radio" name="new-authority" id="new-authority" value="new-authority" (click)="selectNewAuthority()" [checked]="selectedImportType === importType.NewAuthority" />
|
||||||
|
<label class="form-check-label" for="new-authority">{{ (labelPrefix + 'authority.new' | translate) }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="close()">{{ (labelPrefix + 'cancel' | translate) }}</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-primary" [disabled]="selectedImportType === importType.None" (click)="import()">{{ (labelPrefix + 'import' | translate) }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,3 @@
|
|||||||
|
.modal-footer {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
@@ -0,0 +1,194 @@
|
|||||||
|
import { ExternalSourceEntryImportModalComponent, ImportType } from './external-source-entry-import-modal.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { LookupRelationService } from '../../../../../../../core/data/lookup-relation.service';
|
||||||
|
import { ExternalSourceEntry } from '../../../../../../../core/shared/external-source-entry.model';
|
||||||
|
import { Item } from '../../../../../../../core/shared/item.model';
|
||||||
|
import { ItemSearchResult } from '../../../../../../object-collection/shared/item-search-result.model';
|
||||||
|
import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../../../../../testing/utils';
|
||||||
|
import { Collection } from '../../../../../../../core/shared/collection.model';
|
||||||
|
import { RelationshipOptions } from '../../../../models/relationship-options.model';
|
||||||
|
import { SelectableListService } from '../../../../../../object-list/selectable-list/selectable-list.service';
|
||||||
|
import { ItemDataService } from '../../../../../../../core/data/item-data.service';
|
||||||
|
import { NotificationsService } from '../../../../../../notifications/notifications.service';
|
||||||
|
|
||||||
|
describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
|
||||||
|
let component: ExternalSourceEntryImportModalComponent;
|
||||||
|
let fixture: ComponentFixture<ExternalSourceEntryImportModalComponent>;
|
||||||
|
let lookupRelationService: LookupRelationService;
|
||||||
|
let selectService: SelectableListService;
|
||||||
|
let itemService: ItemDataService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
let modalStub: NgbActiveModal;
|
||||||
|
|
||||||
|
const uri = 'https://orcid.org/0001-0001-0001-0001';
|
||||||
|
const entry = Object.assign(new ExternalSourceEntry(), {
|
||||||
|
id: '0001-0001-0001-0001',
|
||||||
|
display: 'John Doe',
|
||||||
|
value: 'John, Doe',
|
||||||
|
metadata: {
|
||||||
|
'dc.identifier.uri': [
|
||||||
|
{
|
||||||
|
value: uri
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const label = 'Author';
|
||||||
|
const relationship = Object.assign(new RelationshipOptions(), { relationshipType: 'isAuthorOfPublication' });
|
||||||
|
const submissionCollection = Object.assign(new Collection(), { uuid: '9398affe-a977-4992-9a1d-6f00908a259f' });
|
||||||
|
const submissionItem = Object.assign(new Item(), { uuid: '26224069-5f99-412a-9e9b-7912a7e35cb1' });
|
||||||
|
const item1 = Object.assign(new Item(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' });
|
||||||
|
const item2 = Object.assign(new Item(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' });
|
||||||
|
const item3 = Object.assign(new Item(), { uuid: 'c3bcbff5-ec0c-4831-8e4c-94b9c933ccac' });
|
||||||
|
const searchResult1 = Object.assign(new ItemSearchResult(), { indexableObject: item1 });
|
||||||
|
const searchResult2 = Object.assign(new ItemSearchResult(), { indexableObject: item2 });
|
||||||
|
const searchResult3 = Object.assign(new ItemSearchResult(), { indexableObject: item3 });
|
||||||
|
const importedItem = Object.assign(new Item(), { uuid: '5d0098fc-344a-4067-a57d-457092b72e82' });
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
lookupRelationService = jasmine.createSpyObj('lookupRelationService', {
|
||||||
|
getLocalResults: createSuccessfulRemoteDataObject$(createPaginatedList([searchResult1, searchResult2, searchResult3])),
|
||||||
|
removeLocalResultsCache: {}
|
||||||
|
});
|
||||||
|
selectService = jasmine.createSpyObj('selectService', ['deselectAll']);
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', ['success']);
|
||||||
|
itemService = jasmine.createSpyObj('itemService', {
|
||||||
|
importExternalSourceEntry: createSuccessfulRemoteDataObject$(importedItem)
|
||||||
|
});
|
||||||
|
modalStub = jasmine.createSpyObj('modal', ['close']);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ExternalSourceEntryImportModalComponent],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule.forRoot()],
|
||||||
|
providers: [
|
||||||
|
{ provide: LookupRelationService, useValue: lookupRelationService },
|
||||||
|
{ provide: SelectableListService, useValue: selectService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
{ provide: ItemDataService, useValue: itemService },
|
||||||
|
{ provide: NgbActiveModal, useValue: modalStub }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ExternalSourceEntryImportModalComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.externalSourceEntry = entry;
|
||||||
|
component.label = label;
|
||||||
|
component.relationship = relationship;
|
||||||
|
component.collection = submissionCollection;
|
||||||
|
component.item = submissionItem;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('close', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the modal', () => {
|
||||||
|
expect(modalStub.close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectEntity', () => {
|
||||||
|
const entity = Object.assign(new Item(), { uuid: 'd8698de5-5b05-4ea4-9d02-da73803a50f9' });
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component.selectEntity(entity);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set selected entity', () => {
|
||||||
|
expect(component.selectedEntity).toBe(entity);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the import type to local entity', () => {
|
||||||
|
expect(component.selectedImportType).toEqual(ImportType.LocalEntity);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deselectEntity', () => {
|
||||||
|
const entity = Object.assign(new Item(), { uuid: 'd8698de5-5b05-4ea4-9d02-da73803a50f9' });
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component.selectedImportType = ImportType.LocalEntity;
|
||||||
|
component.selectedEntity = entity;
|
||||||
|
component.deselectEntity();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the selected entity', () => {
|
||||||
|
expect(component.selectedEntity).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the import type to none', () => {
|
||||||
|
expect(component.selectedImportType).toEqual(ImportType.None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectNewEntity', () => {
|
||||||
|
describe('when current import type is set to new entity', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.selectedImportType = ImportType.NewEntity;
|
||||||
|
component.selectNewEntity();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the import type to none', () => {
|
||||||
|
expect(component.selectedImportType).toEqual(ImportType.None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when current import type is not set to new entity', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.selectedImportType = ImportType.None;
|
||||||
|
component.selectNewEntity();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the import type to new entity', () => {
|
||||||
|
expect(component.selectedImportType).toEqual(ImportType.NewEntity);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deselect the entity and authority list', () => {
|
||||||
|
expect(selectService.deselectAll).toHaveBeenCalledWith(component.entityListId);
|
||||||
|
expect(selectService.deselectAll).toHaveBeenCalledWith(component.authorityListId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectNewAuthority', () => {
|
||||||
|
describe('when current import type is set to new authority', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.selectedImportType = ImportType.NewAuthority;
|
||||||
|
component.selectNewAuthority();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the import type to none', () => {
|
||||||
|
expect(component.selectedImportType).toEqual(ImportType.None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when current import type is not set to new authority', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.selectedImportType = ImportType.None;
|
||||||
|
component.selectNewAuthority();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the import type to new authority', () => {
|
||||||
|
expect(component.selectedImportType).toEqual(ImportType.NewAuthority);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deselect the entity and authority list', () => {
|
||||||
|
expect(selectService.deselectAll).toHaveBeenCalledWith(component.entityListId);
|
||||||
|
expect(selectService.deselectAll).toHaveBeenCalledWith(component.authorityListId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,311 @@
|
|||||||
|
import { Component, EventEmitter, OnInit } from '@angular/core';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ExternalSourceEntry } from '../../../../../../../core/shared/external-source-entry.model';
|
||||||
|
import { MetadataValue } from '../../../../../../../core/shared/metadata.models';
|
||||||
|
import { Metadata } from '../../../../../../../core/shared/metadata.utils';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from '../../../../../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../../../../../core/data/paginated-list';
|
||||||
|
import { SearchResult } from '../../../../../../search/search-result.model';
|
||||||
|
import { Item } from '../../../../../../../core/shared/item.model';
|
||||||
|
import { RelationshipOptions } from '../../../../models/relationship-options.model';
|
||||||
|
import { LookupRelationService } from '../../../../../../../core/data/lookup-relation.service';
|
||||||
|
import { PaginatedSearchOptions } from '../../../../../../search/paginated-search-options.model';
|
||||||
|
import { CollectionElementLinkType } from '../../../../../../object-collection/collection-element-link.type';
|
||||||
|
import { Context } from '../../../../../../../core/shared/context.model';
|
||||||
|
import { SelectableListService } from '../../../../../../object-list/selectable-list/selectable-list.service';
|
||||||
|
import { ListableObject } from '../../../../../../object-collection/shared/listable-object.model';
|
||||||
|
import { Collection } from '../../../../../../../core/shared/collection.model';
|
||||||
|
import { ItemDataService } from '../../../../../../../core/data/item-data.service';
|
||||||
|
import { PaginationComponentOptions } from '../../../../../../pagination/pagination-component-options.model';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../../../core/shared/operators';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { ItemSearchResult } from '../../../../../../object-collection/shared/item-search-result.model';
|
||||||
|
import { NotificationsService } from '../../../../../../notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The possible types of import for the external entry
|
||||||
|
*/
|
||||||
|
export enum ImportType {
|
||||||
|
None = 'None',
|
||||||
|
LocalEntity = 'LocalEntity',
|
||||||
|
LocalAuthority = 'LocalAuthority',
|
||||||
|
NewEntity = 'NewEntity',
|
||||||
|
NewAuthority = 'NewAuthority'
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-external-source-entry-import-modal',
|
||||||
|
styleUrls: ['./external-source-entry-import-modal.component.scss'],
|
||||||
|
templateUrl: './external-source-entry-import-modal.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component to display a modal window for importing an external source entry
|
||||||
|
* Shows information about the selected entry and a selectable list of local entities and authorities with similar names
|
||||||
|
* and the ability to add one of those results to the selection instead of the external entry.
|
||||||
|
* The other option is to import the external entry as a new entity or authority into the repository.
|
||||||
|
*/
|
||||||
|
export class ExternalSourceEntryImportModalComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The prefix for every i18n key within this modal
|
||||||
|
*/
|
||||||
|
labelPrefix = 'submission.sections.describe.relationship-lookup.external-source.import-modal.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label to use for all messages (added to the end of relevant i18n keys)
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The external source entry
|
||||||
|
*/
|
||||||
|
externalSourceEntry: ExternalSourceEntry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item in submission
|
||||||
|
*/
|
||||||
|
item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The collection the user is submitting in
|
||||||
|
*/
|
||||||
|
collection: Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the collection to import entries to
|
||||||
|
*/
|
||||||
|
collectionId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current relationship-options used for filtering results
|
||||||
|
*/
|
||||||
|
relationship: RelationshipOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata value for the entry's uri
|
||||||
|
*/
|
||||||
|
uri: MetadataValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local entities with a similar name
|
||||||
|
*/
|
||||||
|
localEntitiesRD$: Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search options to use for fetching similar results
|
||||||
|
*/
|
||||||
|
searchOptions: PaginatedSearchOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of link to render in listable elements
|
||||||
|
*/
|
||||||
|
linkTypes = CollectionElementLinkType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context we're currently in (submission)
|
||||||
|
*/
|
||||||
|
context = Context.SubmissionModal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List ID for selecting local entities
|
||||||
|
*/
|
||||||
|
entityListId = 'external-source-import-entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List ID for selecting local authorities
|
||||||
|
*/
|
||||||
|
authorityListId = 'external-source-import-authority';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ImportType enum
|
||||||
|
*/
|
||||||
|
importType = ImportType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of import the user currently has selected
|
||||||
|
*/
|
||||||
|
selectedImportType = ImportType.None;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected local entity
|
||||||
|
*/
|
||||||
|
selectedEntity: ListableObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected local authority
|
||||||
|
*/
|
||||||
|
selectedAuthority: ListableObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object has been imported, send it to the parent component
|
||||||
|
*/
|
||||||
|
importedObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should it display the ability to import the entry as an authority?
|
||||||
|
*/
|
||||||
|
authorityEnabled = false;
|
||||||
|
|
||||||
|
constructor(public modal: NgbActiveModal,
|
||||||
|
public lookupRelationService: LookupRelationService,
|
||||||
|
private selectService: SelectableListService,
|
||||||
|
private itemService: ItemDataService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private translateService: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.uri = Metadata.first(this.externalSourceEntry.metadata, 'dc.identifier.uri');
|
||||||
|
const pagination = Object.assign(new PaginationComponentOptions(), { id: 'external-entry-import', pageSize: 5 });
|
||||||
|
this.searchOptions = Object.assign(new PaginatedSearchOptions({ query: this.externalSourceEntry.value, pagination: pagination }));
|
||||||
|
this.localEntitiesRD$ = this.lookupRelationService.getLocalResults(this.relationship, this.searchOptions);
|
||||||
|
this.collectionId = this.collection.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the window
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
this.modal.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the import of the external entry
|
||||||
|
*/
|
||||||
|
import() {
|
||||||
|
switch (this.selectedImportType) {
|
||||||
|
case ImportType.LocalEntity : {
|
||||||
|
this.importLocalEntity();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ImportType.NewEntity : {
|
||||||
|
this.importNewEntity();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ImportType.LocalAuthority : {
|
||||||
|
this.importLocalAuthority();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ImportType.NewAuthority : {
|
||||||
|
this.importNewAuthority();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.selectedImportType = ImportType.None;
|
||||||
|
this.deselectAllLists();
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import the selected local entity
|
||||||
|
*/
|
||||||
|
importLocalEntity() {
|
||||||
|
if (this.selectedEntity !== undefined) {
|
||||||
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + this.label + '.added.local-entity'));
|
||||||
|
this.importedObject.emit(this.selectedEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and import a new entity from the external entry
|
||||||
|
*/
|
||||||
|
importNewEntity() {
|
||||||
|
this.itemService.importExternalSourceEntry(this.externalSourceEntry, this.collectionId).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
take(1)
|
||||||
|
).subscribe((item: Item) => {
|
||||||
|
this.lookupRelationService.removeLocalResultsCache();
|
||||||
|
const searchResult = Object.assign(new ItemSearchResult(), {
|
||||||
|
indexableObject: item
|
||||||
|
});
|
||||||
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + this.label + '.added.new-entity'));
|
||||||
|
this.importedObject.emit(searchResult);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import the selected local authority
|
||||||
|
*/
|
||||||
|
importLocalAuthority() {
|
||||||
|
// TODO: Implement ability to import local authorities
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and import a new authority from the external entry
|
||||||
|
*/
|
||||||
|
importNewAuthority() {
|
||||||
|
// TODO: Implement ability to import new authorities
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deselected a local entity
|
||||||
|
*/
|
||||||
|
deselectEntity() {
|
||||||
|
this.selectedEntity = undefined;
|
||||||
|
if (this.selectedImportType === ImportType.LocalEntity) {
|
||||||
|
this.selectedImportType = ImportType.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected a local entity
|
||||||
|
* @param entity
|
||||||
|
*/
|
||||||
|
selectEntity(entity) {
|
||||||
|
this.selectedEntity = entity;
|
||||||
|
this.selectedImportType = ImportType.LocalEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected/deselected the new entity option
|
||||||
|
*/
|
||||||
|
selectNewEntity() {
|
||||||
|
if (this.selectedImportType === ImportType.NewEntity) {
|
||||||
|
this.selectedImportType = ImportType.None;
|
||||||
|
} else {
|
||||||
|
this.selectedImportType = ImportType.NewEntity;
|
||||||
|
this.deselectAllLists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deselected a local authority
|
||||||
|
*/
|
||||||
|
deselectAuthority() {
|
||||||
|
this.selectedAuthority = undefined;
|
||||||
|
if (this.selectedImportType === ImportType.LocalAuthority) {
|
||||||
|
this.selectedImportType = ImportType.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected a local authority
|
||||||
|
* @param authority
|
||||||
|
*/
|
||||||
|
selectAuthority(authority) {
|
||||||
|
this.selectedAuthority = authority;
|
||||||
|
this.selectedImportType = ImportType.LocalAuthority;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected/deselected the new authority option
|
||||||
|
*/
|
||||||
|
selectNewAuthority() {
|
||||||
|
if (this.selectedImportType === ImportType.NewAuthority) {
|
||||||
|
this.selectedImportType = ImportType.None;
|
||||||
|
} else {
|
||||||
|
this.selectedImportType = ImportType.NewAuthority;
|
||||||
|
this.deselectAllLists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deselect every element from both entity and authority lists
|
||||||
|
*/
|
||||||
|
deselectAllLists() {
|
||||||
|
this.selectService.deselectAll(this.entityListId);
|
||||||
|
this.selectService.deselectAll(this.authorityListId);
|
||||||
|
}
|
||||||
|
}
|
@@ -128,7 +128,7 @@ export class RelationshipEffects {
|
|||||||
this.relationshipService.getRelationshipByItemsAndLabel(item1, item2, relationshipType).pipe(
|
this.relationshipService.getRelationshipByItemsAndLabel(item1, item2, relationshipType).pipe(
|
||||||
take(1),
|
take(1),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
mergeMap((relationship: Relationship) => this.relationshipService.deleteRelationship(relationship.id)),
|
mergeMap((relationship: Relationship) => this.relationshipService.deleteRelationship(relationship.id, 'none')),
|
||||||
take(1)
|
take(1)
|
||||||
).subscribe();
|
).subscribe();
|
||||||
}
|
}
|
||||||
|
@@ -1325,7 +1325,7 @@ export const mockUploadConfigResponse = {
|
|||||||
},
|
},
|
||||||
self: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/bitstream-metadata'
|
self: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/bitstream-metadata'
|
||||||
},
|
},
|
||||||
required: false,
|
required: true,
|
||||||
maxSize: 536870912,
|
maxSize: 536870912,
|
||||||
name: 'upload',
|
name: 'upload',
|
||||||
type: 'submissionupload',
|
type: 'submissionupload',
|
||||||
@@ -1336,6 +1336,10 @@ export const mockUploadConfigResponse = {
|
|||||||
self: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload'
|
self: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Clone the object and change one property
|
||||||
|
export const mockUploadConfigResponseNotRequired = JSON.parse(JSON.stringify(mockUploadConfigResponse));
|
||||||
|
mockUploadConfigResponseNotRequired.required = false;
|
||||||
|
|
||||||
export const mockAccessConditionOptions = [
|
export const mockAccessConditionOptions = [
|
||||||
{
|
{
|
||||||
name: 'openaccess',
|
name: 'openaccess',
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
[hideGear]="hideGear"
|
[hideGear]="hideGear"
|
||||||
[linkType]="linkType"
|
[linkType]="linkType"
|
||||||
[context]="context"
|
[context]="context"
|
||||||
|
[hidePaginationDetail]="hidePaginationDetail"
|
||||||
(paginationChange)="onPaginationChange($event)"
|
(paginationChange)="onPaginationChange($event)"
|
||||||
(pageChange)="onPageChange($event)"
|
(pageChange)="onPageChange($event)"
|
||||||
(pageSizeChange)="onPageSizeChange($event)"
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
@@ -14,6 +15,9 @@
|
|||||||
(sortFieldChange)="onSortFieldChange($event)"
|
(sortFieldChange)="onSortFieldChange($event)"
|
||||||
[selectable]="selectable"
|
[selectable]="selectable"
|
||||||
[selectionConfig]="selectionConfig"
|
[selectionConfig]="selectionConfig"
|
||||||
|
[importable]="importable"
|
||||||
|
[importConfig]="importConfig"
|
||||||
|
(importObject)="importObject.emit($event)"
|
||||||
*ngIf="(currentMode$ | async) === viewModeEnum.ListElement">
|
*ngIf="(currentMode$ | async) === viewModeEnum.ListElement">
|
||||||
</ds-object-list>
|
</ds-object-list>
|
||||||
|
|
||||||
@@ -23,6 +27,7 @@
|
|||||||
[hideGear]="hideGear"
|
[hideGear]="hideGear"
|
||||||
[linkType]="linkType"
|
[linkType]="linkType"
|
||||||
[context]="context"
|
[context]="context"
|
||||||
|
[hidePaginationDetail]="hidePaginationDetail"
|
||||||
(paginationChange)="onPaginationChange($event)"
|
(paginationChange)="onPaginationChange($event)"
|
||||||
(pageChange)="onPageChange($event)"
|
(pageChange)="onPageChange($event)"
|
||||||
(pageSizeChange)="onPageSizeChange($event)"
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
@@ -37,6 +42,7 @@
|
|||||||
[hideGear]="hideGear"
|
[hideGear]="hideGear"
|
||||||
[linkType]="linkType"
|
[linkType]="linkType"
|
||||||
[context]="context"
|
[context]="context"
|
||||||
|
[hidePaginationDetail]="hidePaginationDetail"
|
||||||
*ngIf="(currentMode$ | async) === viewModeEnum.DetailedListElement">
|
*ngIf="(currentMode$ | async) === viewModeEnum.DetailedListElement">
|
||||||
</ds-object-detail>
|
</ds-object-detail>
|
||||||
|
|
||||||
|
@@ -53,6 +53,21 @@ export class ObjectCollectionComponent implements OnInit {
|
|||||||
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||||
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to add an import button to the object elements
|
||||||
|
*/
|
||||||
|
@Input() importable = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The config to use for the import button
|
||||||
|
*/
|
||||||
|
@Input() importConfig: { buttonLabel: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an import event to the parent component
|
||||||
|
*/
|
||||||
|
@Output() importObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The link type of the rendered list elements
|
* The link type of the rendered list elements
|
||||||
*/
|
*/
|
||||||
@@ -63,6 +78,11 @@ export class ObjectCollectionComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() context: Context;
|
@Input() context: Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option for hiding the pagination detail
|
||||||
|
*/
|
||||||
|
@Input() hidePaginationDetail = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the page info of the list
|
* the page info of the list
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
<div class="d-inline-block mr-2">
|
||||||
|
<button (click)="importObject.emit(object)"
|
||||||
|
class="btn btn-outline-primary btn-sm float-left"
|
||||||
|
title="{{importConfig?.buttonLabel | translate}}">
|
||||||
|
<i class="fas fa-cloud-download-alt fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { ListableObject } from '../listable-object.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-importable-list-item-control',
|
||||||
|
templateUrl: './importable-list-item-control.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component adding an import button to a list item
|
||||||
|
*/
|
||||||
|
export class ImportableListItemControlComponent {
|
||||||
|
/**
|
||||||
|
* The item or metadata to determine the component for
|
||||||
|
*/
|
||||||
|
@Input() object: ListableObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra configuration for the import button
|
||||||
|
*/
|
||||||
|
@Input() importConfig: { buttonLabel: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the object to import
|
||||||
|
*/
|
||||||
|
@Output() importObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||||
|
}
|
@@ -89,7 +89,7 @@ export class ObjectDetailComponent {
|
|||||||
/**
|
/**
|
||||||
* Option for hiding the pagination detail
|
* Option for hiding the pagination detail
|
||||||
*/
|
*/
|
||||||
public hidePaginationDetail = true;
|
@Input() hidePaginationDetail = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event fired when the page is changed.
|
* An event fired when the page is changed.
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
[sortOptions]="sortConfig"
|
[sortOptions]="sortConfig"
|
||||||
[hideGear]="hideGear"
|
[hideGear]="hideGear"
|
||||||
[hidePagerWhenSinglePage]="hidePagerWhenSinglePage"
|
[hidePagerWhenSinglePage]="hidePagerWhenSinglePage"
|
||||||
|
[hidePaginationDetail]="hidePaginationDetail"
|
||||||
(pageChange)="onPageChange($event)"
|
(pageChange)="onPageChange($event)"
|
||||||
(pageSizeChange)="onPageSizeChange($event)"
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
(sortDirectionChange)="onSortDirectionChange($event)"
|
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||||
|
@@ -69,6 +69,11 @@ export class ObjectGridComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() context: Context;
|
@Input() context: Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option for hiding the pagination detail
|
||||||
|
*/
|
||||||
|
@Input() hidePaginationDetail = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Behavior subject to output the current listable objects
|
* Behavior subject to output the current listable objects
|
||||||
*/
|
*/
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
<ng-container *ngIf="status">
|
<ng-container *ngIf="status">
|
||||||
<ds-mydspace-item-status [status]="status"></ds-mydspace-item-status>
|
<ds-mydspace-item-status [status]="status"></ds-mydspace-item-status>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<ds-item-type-badge [object]="item"></ds-item-type-badge>
|
||||||
<ds-truncatable [id]="item.id">
|
<ds-truncatable [id]="item.id">
|
||||||
<h3 [innerHTML]="item.firstMetadataValue('dc.title') || ('mydspace.results.no-title' | translate)" [ngClass]="{'lead': true,'text-muted': !item.firstMetadataValue('dc.title')}"></h3>
|
<h3 [innerHTML]="item.firstMetadataValue('dc.title') || ('mydspace.results.no-title' | translate)" [ngClass]="{'lead': true,'text-muted': !item.firstMetadataValue('dc.title')}"></h3>
|
||||||
<div>
|
<div>
|
||||||
|
@@ -47,6 +47,23 @@ const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const mockItemWithEntityType: Item = Object.assign(new Item(), {
|
||||||
|
bundles: observableOf({}),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'relationship.type': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: 'Publication'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
describe('ItemListPreviewComponent', () => {
|
describe('ItemListPreviewComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
@@ -128,4 +145,16 @@ describe('ItemListPreviewComponent', () => {
|
|||||||
expect(dateField).not.toBeNull();
|
expect(dateField).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('When the item has an entity type', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.item = mockItemWithEntityType;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the entity type span', () => {
|
||||||
|
const entityField = fixture.debugElement.query(By.css('ds-item-type-badge'));
|
||||||
|
expect(entityField).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
[sortOptions]="sortConfig"
|
[sortOptions]="sortConfig"
|
||||||
[hideGear]="hideGear"
|
[hideGear]="hideGear"
|
||||||
[hidePagerWhenSinglePage]="hidePagerWhenSinglePage"
|
[hidePagerWhenSinglePage]="hidePagerWhenSinglePage"
|
||||||
|
[hidePaginationDetail]="hidePaginationDetail"
|
||||||
(pageChange)="onPageChange($event)"
|
(pageChange)="onPageChange($event)"
|
||||||
(pageSizeChange)="onPageSizeChange($event)"
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
(sortDirectionChange)="onSortDirectionChange($event)"
|
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||||
@@ -19,6 +20,11 @@
|
|||||||
(deselectObject)="deselectObject.emit($event)"
|
(deselectObject)="deselectObject.emit($event)"
|
||||||
(selectObject)="selectObject.emit($event)"></ds-selectable-list-item-control>
|
(selectObject)="selectObject.emit($event)"></ds-selectable-list-item-control>
|
||||||
</span>
|
</span>
|
||||||
|
<span *ngIf="importable">
|
||||||
|
<ds-importable-list-item-control [object]="object"
|
||||||
|
[importConfig]="importConfig"
|
||||||
|
(importObject)="importObject.emit($event)"></ds-importable-list-item-control>
|
||||||
|
</span>
|
||||||
<ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [index]="i" [context]="context" [linkType]="linkType"
|
<ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [index]="i" [context]="context" [linkType]="linkType"
|
||||||
[listID]="selectionConfig?.listId"></ds-listable-object-component-loader>
|
[listID]="selectionConfig?.listId"></ds-listable-object-component-loader>
|
||||||
</li>
|
</li>
|
||||||
|
@@ -61,6 +61,21 @@ export class ObjectListComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() context: Context;
|
@Input() context: Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option for hiding the pagination detail
|
||||||
|
*/
|
||||||
|
@Input() hidePaginationDetail = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to add an import button to the object
|
||||||
|
*/
|
||||||
|
@Input() importable = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config used for the import button
|
||||||
|
*/
|
||||||
|
@Input() importConfig: { importLabel: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current listable objects
|
* The current listable objects
|
||||||
*/
|
*/
|
||||||
@@ -119,6 +134,12 @@ export class ObjectListComponent {
|
|||||||
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||||
|
|
||||||
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an import event to the parent component
|
||||||
|
*/
|
||||||
|
@Output() importObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event fired when the sort field is changed.
|
* An event fired when the sort field is changed.
|
||||||
* Event's payload equals to the newly selected sort field.
|
* Event's payload equals to the newly selected sort field.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<div *ngIf="currentPageState == undefined || currentPageState == currentPage">
|
<div *ngIf="currentPageState == undefined || currentPageState == currentPage">
|
||||||
<div class="pagination-masked clearfix top">
|
<div *ngIf="(!hidePaginationDetail && collectionSize > 0) || !hideGear" class="pagination-masked clearfix top">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div *ngIf="!hidePaginationDetail && collectionSize > 0" class="col-auto pagination-info">
|
<div *ngIf="!hidePaginationDetail && collectionSize > 0" class="col-auto pagination-info">
|
||||||
<span class="align-middle hidden-xs-down">{{ 'pagination.showing.label' | translate }}</span>
|
<span class="align-middle hidden-xs-down">{{ 'pagination.showing.label' | translate }}</span>
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
[selectable]="selectable"
|
[selectable]="selectable"
|
||||||
[selectionConfig]="selectionConfig"
|
[selectionConfig]="selectionConfig"
|
||||||
[context]="context"
|
[context]="context"
|
||||||
|
[hidePaginationDetail]="hidePaginationDetail"
|
||||||
(deselectObject)="deselectObject.emit($event)"
|
(deselectObject)="deselectObject.emit($event)"
|
||||||
(selectObject)="selectObject.emit($event)"
|
(selectObject)="selectObject.emit($event)"
|
||||||
>
|
>
|
||||||
|
@@ -67,6 +67,11 @@ export class SearchResultsComponent {
|
|||||||
|
|
||||||
@Input() context: Context;
|
@Input() context: Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option for hiding the pagination detail
|
||||||
|
*/
|
||||||
|
@Input() hidePaginationDetail = false;
|
||||||
|
|
||||||
@Input() selectionConfig: {repeatable: boolean, listId: string};
|
@Input() selectionConfig: {repeatable: boolean, listId: string};
|
||||||
|
|
||||||
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||||
|
@@ -47,7 +47,10 @@ import { LogOutComponent } from './log-out/log-out.component';
|
|||||||
import { FormComponent } from './form/form.component';
|
import { FormComponent } from './form/form.component';
|
||||||
import { DsDynamicTypeaheadComponent } from './form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component';
|
import { DsDynamicTypeaheadComponent } from './form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component';
|
||||||
import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
|
import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
|
||||||
import { DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
|
import {
|
||||||
|
DsDynamicFormControlContainerComponent,
|
||||||
|
dsDynamicFormControlMapFn
|
||||||
|
} from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
|
||||||
import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component';
|
import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component';
|
||||||
import { DYNAMIC_FORM_CONTROL_MAP_FN, DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
|
import { DYNAMIC_FORM_CONTROL_MAP_FN, DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
|
||||||
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
|
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
|
||||||
@@ -172,6 +175,8 @@ import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.componen
|
|||||||
import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.component';
|
import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.component';
|
||||||
import { SelectableListItemControlComponent } from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component';
|
import { SelectableListItemControlComponent } from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component';
|
||||||
import { DsDynamicLookupRelationExternalSourceTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component';
|
import { DsDynamicLookupRelationExternalSourceTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component';
|
||||||
|
import { ExternalSourceEntryImportModalComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component';
|
||||||
|
import { ImportableListItemControlComponent } from './object-collection/shared/importable-list-item-control/importable-list-item-control.component';
|
||||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||||
import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
|
import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
|
||||||
import { LogInContainerComponent } from './log-in/container/log-in-container.component';
|
import { LogInContainerComponent } from './log-in/container/log-in-container.component';
|
||||||
@@ -338,11 +343,12 @@ const COMPONENTS = [
|
|||||||
CollectionSelectComponent,
|
CollectionSelectComponent,
|
||||||
MetadataRepresentationLoaderComponent,
|
MetadataRepresentationLoaderComponent,
|
||||||
SelectableListItemControlComponent,
|
SelectableListItemControlComponent,
|
||||||
|
ExternalSourceEntryImportModalComponent,
|
||||||
|
ImportableListItemControlComponent,
|
||||||
ExistingMetadataListElementComponent,
|
ExistingMetadataListElementComponent,
|
||||||
LogInShibbolethComponent,
|
LogInShibbolethComponent,
|
||||||
LogInPasswordComponent,
|
LogInPasswordComponent,
|
||||||
LogInContainerComponent,
|
LogInContainerComponent
|
||||||
ItemTypeBadgeComponent
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
@@ -405,6 +411,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
DsDynamicLookupRelationSearchTabComponent,
|
DsDynamicLookupRelationSearchTabComponent,
|
||||||
DsDynamicLookupRelationSelectionTabComponent,
|
DsDynamicLookupRelationSelectionTabComponent,
|
||||||
DsDynamicLookupRelationExternalSourceTabComponent,
|
DsDynamicLookupRelationExternalSourceTabComponent,
|
||||||
|
ExternalSourceEntryImportModalComponent,
|
||||||
LogInPasswordComponent,
|
LogInPasswordComponent,
|
||||||
LogInShibbolethComponent
|
LogInShibbolethComponent
|
||||||
];
|
];
|
||||||
|
@@ -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');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { createSuccessfulRemoteDataObject$, createTestComponent } from '../../../shared/testing/utils';
|
import { createSuccessfulRemoteDataObject$, createTestComponent } from '../../../shared/testing/utils';
|
||||||
|
import { SubmissionObjectState } from '../../objects/submission-objects.reducer';
|
||||||
import { SubmissionService } from '../../submission.service';
|
import { SubmissionService } from '../../submission.service';
|
||||||
import { SubmissionServiceStub } from '../../../shared/testing/submission-service-stub';
|
import { SubmissionServiceStub } from '../../../shared/testing/submission-service-stub';
|
||||||
import { SectionsService } from '../sections.service';
|
import { SectionsService } from '../sections.service';
|
||||||
@@ -18,7 +19,7 @@ import {
|
|||||||
mockSubmissionId,
|
mockSubmissionId,
|
||||||
mockSubmissionState,
|
mockSubmissionState,
|
||||||
mockUploadConfigResponse,
|
mockUploadConfigResponse,
|
||||||
mockUploadFiles
|
mockUploadConfigResponseNotRequired, mockUploadFiles,
|
||||||
} from '../../../shared/mocks/mock-submission';
|
} from '../../../shared/mocks/mock-submission';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
@@ -31,7 +32,6 @@ import { cold, hot } from 'jasmine-marbles';
|
|||||||
import { Collection } from '../../../core/shared/collection.model';
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
import { ResourcePolicy } from '../../../core/shared/resource-policy.model';
|
import { ResourcePolicy } from '../../../core/shared/resource-policy.model';
|
||||||
import { ResourcePolicyService } from '../../../core/data/resource-policy.service';
|
import { ResourcePolicyService } from '../../../core/data/resource-policy.service';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
|
||||||
import { ConfigData } from '../../../core/config/config-data';
|
import { ConfigData } from '../../../core/config/config-data';
|
||||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
import { Group } from '../../../core/eperson/models/group.model';
|
import { Group } from '../../../core/eperson/models/group.model';
|
||||||
@@ -65,17 +65,7 @@ function getMockResourcePolicyService(): ResourcePolicyService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const sectionObject: SectionDataObject = {
|
let sectionObject: SectionDataObject;
|
||||||
config: 'https://dspace7.4science.it/or2018/api/config/submissionforms/upload',
|
|
||||||
mandatory: true,
|
|
||||||
data: {
|
|
||||||
files: []
|
|
||||||
},
|
|
||||||
errors: [],
|
|
||||||
header: 'submit.progressbar.describe.upload',
|
|
||||||
id: 'upload',
|
|
||||||
sectionType: SectionsType.Upload
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('SubmissionSectionUploadComponent test suite', () => {
|
describe('SubmissionSectionUploadComponent test suite', () => {
|
||||||
|
|
||||||
@@ -90,30 +80,48 @@ describe('SubmissionSectionUploadComponent test suite', () => {
|
|||||||
let uploadsConfigService: any;
|
let uploadsConfigService: any;
|
||||||
let bitstreamService: any;
|
let bitstreamService: any;
|
||||||
|
|
||||||
const submissionId = mockSubmissionId;
|
let submissionId: string;
|
||||||
const collectionId = mockSubmissionCollectionId;
|
let collectionId: string;
|
||||||
const submissionState = Object.assign({}, mockSubmissionState[mockSubmissionId]);
|
let submissionState: SubmissionObjectState;
|
||||||
const mockCollection = Object.assign(new Collection(), {
|
let mockCollection: Collection;
|
||||||
name: 'Community 1-Collection 1',
|
let mockDefaultAccessCondition: ResourcePolicy;
|
||||||
id: collectionId,
|
|
||||||
metadata: [
|
|
||||||
{
|
|
||||||
key: 'dc.title',
|
|
||||||
language: 'en_US',
|
|
||||||
value: 'Community 1-Collection 1'
|
|
||||||
}],
|
|
||||||
_links: {
|
|
||||||
defaultAccessConditions: collectionId + '/defaultAccessConditions'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const mockDefaultAccessCondition = Object.assign(new ResourcePolicy(), {
|
|
||||||
name: null,
|
|
||||||
groupUUID: '11cc35e5-a11d-4b64-b5b9-0052a5d15509',
|
|
||||||
id: 20,
|
|
||||||
uuid: 'resource-policy-20'
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
sectionObject = {
|
||||||
|
config: 'https://dspace7.4science.it/or2018/api/config/submissionforms/upload',
|
||||||
|
mandatory: true,
|
||||||
|
data: {
|
||||||
|
files: []
|
||||||
|
},
|
||||||
|
errors: [],
|
||||||
|
header: 'submit.progressbar.describe.upload',
|
||||||
|
id: 'upload',
|
||||||
|
sectionType: SectionsType.Upload
|
||||||
|
};
|
||||||
|
submissionId = mockSubmissionId;
|
||||||
|
collectionId = mockSubmissionCollectionId;
|
||||||
|
submissionState = Object.assign({}, mockSubmissionState[mockSubmissionId]) as any;
|
||||||
|
mockCollection = Object.assign(new Collection(), {
|
||||||
|
name: 'Community 1-Collection 1',
|
||||||
|
id: collectionId,
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Community 1-Collection 1'
|
||||||
|
}],
|
||||||
|
_links: {
|
||||||
|
defaultAccessConditions: collectionId + '/defaultAccessConditions'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mockDefaultAccessCondition = Object.assign(new ResourcePolicy(), {
|
||||||
|
name: null,
|
||||||
|
groupUUID: '11cc35e5-a11d-4b64-b5b9-0052a5d15509',
|
||||||
|
id: 20,
|
||||||
|
uuid: 'resource-policy-20'
|
||||||
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@@ -206,7 +214,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
|
|||||||
|
|
||||||
comp.onSectionInit();
|
comp.onSectionInit();
|
||||||
|
|
||||||
const expectedGroupsMap = new Map([
|
const expectedGroupsMap = new Map([
|
||||||
[mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
|
[mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
|
||||||
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
|
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
|
||||||
]);
|
]);
|
||||||
@@ -215,6 +223,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
|
|||||||
expect(comp.collectionName).toBe(mockCollection.name);
|
expect(comp.collectionName).toBe(mockCollection.name);
|
||||||
expect(comp.availableAccessConditionOptions.length).toBe(4);
|
expect(comp.availableAccessConditionOptions.length).toBe(4);
|
||||||
expect(comp.availableAccessConditionOptions).toEqual(mockUploadConfigResponse.accessConditionOptions as any);
|
expect(comp.availableAccessConditionOptions).toEqual(mockUploadConfigResponse.accessConditionOptions as any);
|
||||||
|
expect(comp.required$.getValue()).toBe(true);
|
||||||
expect(compAsAny.subs.length).toBe(2);
|
expect(compAsAny.subs.length).toBe(2);
|
||||||
expect(compAsAny.availableGroups.size).toBe(2);
|
expect(compAsAny.availableGroups.size).toBe(2);
|
||||||
expect(compAsAny.availableGroups).toEqual(expectedGroupsMap);
|
expect(compAsAny.availableGroups).toEqual(expectedGroupsMap);
|
||||||
@@ -245,7 +254,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
|
|||||||
|
|
||||||
comp.onSectionInit();
|
comp.onSectionInit();
|
||||||
|
|
||||||
const expectedGroupsMap = new Map([
|
const expectedGroupsMap = new Map([
|
||||||
[mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
|
[mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
|
||||||
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
|
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
|
||||||
]);
|
]);
|
||||||
@@ -254,6 +263,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
|
|||||||
expect(comp.collectionName).toBe(mockCollection.name);
|
expect(comp.collectionName).toBe(mockCollection.name);
|
||||||
expect(comp.availableAccessConditionOptions.length).toBe(4);
|
expect(comp.availableAccessConditionOptions.length).toBe(4);
|
||||||
expect(comp.availableAccessConditionOptions).toEqual(mockUploadConfigResponse.accessConditionOptions as any);
|
expect(comp.availableAccessConditionOptions).toEqual(mockUploadConfigResponse.accessConditionOptions as any);
|
||||||
|
expect(comp.required$.getValue()).toBe(true);
|
||||||
expect(compAsAny.subs.length).toBe(2);
|
expect(compAsAny.subs.length).toBe(2);
|
||||||
expect(compAsAny.availableGroups.size).toBe(2);
|
expect(compAsAny.availableGroups.size).toBe(2);
|
||||||
expect(compAsAny.availableGroups).toEqual(expectedGroupsMap);
|
expect(compAsAny.availableGroups).toEqual(expectedGroupsMap);
|
||||||
@@ -263,17 +273,67 @@ describe('SubmissionSectionUploadComponent test suite', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should the properly section status', () => {
|
it('should properly read the section status when required is true', () => {
|
||||||
bitstreamService.getUploadedFileList.and.returnValue(hot('-a-b', {
|
submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState));
|
||||||
|
|
||||||
|
collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection));
|
||||||
|
|
||||||
|
resourcePolicyService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(mockDefaultAccessCondition));
|
||||||
|
|
||||||
|
uploadsConfigService.getConfigByHref.and.returnValue(observableOf(
|
||||||
|
new ConfigData(new PageInfo(), mockUploadConfigResponse as any)
|
||||||
|
));
|
||||||
|
|
||||||
|
groupService.findById.and.returnValues(
|
||||||
|
createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)),
|
||||||
|
createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup))
|
||||||
|
);
|
||||||
|
|
||||||
|
bitstreamService.getUploadedFileList.and.returnValue(cold('-a-b', {
|
||||||
a: [],
|
a: [],
|
||||||
b: mockUploadFiles
|
b: mockUploadFiles
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
comp.onSectionInit();
|
||||||
|
|
||||||
|
expect(comp.required$.getValue()).toBe(true);
|
||||||
|
|
||||||
expect(compAsAny.getSectionStatus()).toBeObservable(cold('-c-d', {
|
expect(compAsAny.getSectionStatus()).toBeObservable(cold('-c-d', {
|
||||||
c: false,
|
c: false,
|
||||||
d: true
|
d: true
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should properly read the section status when required is false', () => {
|
||||||
|
submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState));
|
||||||
|
|
||||||
|
collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection));
|
||||||
|
|
||||||
|
resourcePolicyService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(mockDefaultAccessCondition));
|
||||||
|
|
||||||
|
uploadsConfigService.getConfigByHref.and.returnValue(observableOf(
|
||||||
|
new ConfigData(new PageInfo(), mockUploadConfigResponseNotRequired as any)
|
||||||
|
));
|
||||||
|
|
||||||
|
groupService.findById.and.returnValues(
|
||||||
|
createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)),
|
||||||
|
createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup))
|
||||||
|
);
|
||||||
|
|
||||||
|
bitstreamService.getUploadedFileList.and.returnValue(cold('-a-b', {
|
||||||
|
a: [],
|
||||||
|
b: mockUploadFiles
|
||||||
|
}));
|
||||||
|
|
||||||
|
comp.onSectionInit();
|
||||||
|
|
||||||
|
expect(comp.required$.getValue()).toBe(false);
|
||||||
|
|
||||||
|
expect(compAsAny.getSectionStatus()).toBeObservable(cold('-c-d', {
|
||||||
|
c: true,
|
||||||
|
d: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
|
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
|
||||||
|
|
||||||
import { combineLatest, Observable, Subscription } from 'rxjs';
|
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription} from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, find, flatMap, map, reduce, take, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, find, flatMap, map, reduce, take, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { SectionModelComponent } from '../models/section.model';
|
import { SectionModelComponent } from '../models/section.model';
|
||||||
@@ -104,6 +104,12 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
|
|||||||
*/
|
*/
|
||||||
protected availableGroups: Map<string, Group[]>; // Groups for any policy
|
protected availableGroups: Map<string, Group[]>; // Groups for any policy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the upload required
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
public required$ = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||||
* @type {Array}
|
* @type {Array}
|
||||||
@@ -172,6 +178,7 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
|
|||||||
}),
|
}),
|
||||||
flatMap(() => config$),
|
flatMap(() => config$),
|
||||||
flatMap((config: SubmissionUploadsModel) => {
|
flatMap((config: SubmissionUploadsModel) => {
|
||||||
|
this.required$.next(config.required);
|
||||||
this.availableAccessConditionOptions = isNotEmpty(config.accessConditionOptions) ? config.accessConditionOptions : [];
|
this.availableAccessConditionOptions = isNotEmpty(config.accessConditionOptions) ? config.accessConditionOptions : [];
|
||||||
|
|
||||||
this.collectionPolicyType = this.availableAccessConditionOptions.length > 0
|
this.collectionPolicyType = this.availableAccessConditionOptions.length > 0
|
||||||
@@ -221,7 +228,7 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
// retrieve submission's bitstreams from state
|
// retrieve submission's bitstreams from state
|
||||||
combineLatest(this.configMetadataForm$,
|
observableCombineLatest(this.configMetadataForm$,
|
||||||
this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id)).pipe(
|
this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id)).pipe(
|
||||||
filter(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => {
|
filter(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => {
|
||||||
return isNotEmpty(configMetadataForm) && isNotUndefined(fileList)
|
return isNotEmpty(configMetadataForm) && isNotUndefined(fileList)
|
||||||
@@ -273,8 +280,13 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
|
|||||||
* the section status
|
* the section status
|
||||||
*/
|
*/
|
||||||
protected getSectionStatus(): Observable<boolean> {
|
protected getSectionStatus(): Observable<boolean> {
|
||||||
return this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id).pipe(
|
// if not mandatory, always true
|
||||||
map((fileList: any[]) => (isNotUndefined(fileList) && fileList.length > 0)));
|
// if mandatory, at least one file is required
|
||||||
|
return observableCombineLatest(this.required$,
|
||||||
|
this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id),
|
||||||
|
(required,fileList: any[]) => {
|
||||||
|
return (!required || (isNotUndefined(fileList) && fileList.length > 0));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
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';
|
||||||
import {AuthConfig} from './auth-config.interfaces';
|
import {AuthConfig} from './auth-config.interfaces';
|
||||||
|
|
||||||
@@ -28,5 +29,6 @@ export interface GlobalConfig extends Config {
|
|||||||
languages: LangConfig[];
|
languages: LangConfig[];
|
||||||
browseBy: BrowseByConfig;
|
browseBy: BrowseByConfig;
|
||||||
item: ItemPageConfig;
|
item: ItemPageConfig;
|
||||||
|
collection: CollectionPageConfig;
|
||||||
themes: Theme[];
|
themes: Theme[];
|
||||||
}
|
}
|
||||||
|
@@ -50,7 +50,10 @@ a<div class="top-item-page">
|
|||||||
[fields]="['dc.identifier.citation']"
|
[fields]="['dc.identifier.citation']"
|
||||||
[label]="'item.page.citation'">
|
[label]="'item.page.citation'">
|
||||||
</ds-generic-item-page-field>
|
</ds-generic-item-page-field>
|
||||||
<ds-item-page-uri-field [item]="object"></ds-item-page-uri-field>
|
<<ds-item-page-uri-field [item]="object"
|
||||||
|
[fields]="['dc.identifier.uri']"
|
||||||
|
[label]="'item.page.uri'">
|
||||||
|
</ds-item-page-uri-field>
|
||||||
<ds-item-page-collections [item]="object"></ds-item-page-collections>
|
<ds-item-page-collections [item]="object"></ds-item-page-collections>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user