mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'Keep-virtual-metadata-on-relationship-delete' into Virtual-metadata-on-item-delete
This commit is contained in:
15
.travis.yml
15
.travis.yml
@@ -1,5 +1,5 @@
|
|||||||
sudo: required
|
sudo: required
|
||||||
dist: trusty
|
dist: bionic
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Install the latest docker-compose version for ci testing.
|
# Install the latest docker-compose version for ci testing.
|
||||||
@@ -12,6 +12,9 @@ env:
|
|||||||
DSPACE_REST_NAMESPACE: '/server/api'
|
DSPACE_REST_NAMESPACE: '/server/api'
|
||||||
DSPACE_REST_SSL: false
|
DSPACE_REST_SSL: false
|
||||||
|
|
||||||
|
services:
|
||||||
|
- xvfb
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
# Docker Compose Install
|
# Docker Compose Install
|
||||||
- curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
|
- curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
|
||||||
@@ -33,14 +36,6 @@ before_script:
|
|||||||
after_script:
|
after_script:
|
||||||
- docker-compose -f ./docker/docker-compose-travis.yml down
|
- docker-compose -f ./docker/docker-compose-travis.yml down
|
||||||
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
sources:
|
|
||||||
- google-chrome
|
|
||||||
packages:
|
|
||||||
- dpkg
|
|
||||||
- google-chrome-stable
|
|
||||||
|
|
||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
@@ -53,8 +48,6 @@ cache:
|
|||||||
bundler_args: --retry 5
|
bundler_args: --retry 5
|
||||||
|
|
||||||
script:
|
script:
|
||||||
# Use Chromium instead of Chrome.
|
|
||||||
- export CHROME_BIN=chromium-browser
|
|
||||||
- yarn run build
|
- yarn run build
|
||||||
- yarn run ci
|
- yarn run ci
|
||||||
- cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
|
- cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
46
e2e/search-navbar/search-navbar.e2e-spec.ts
Normal file
46
e2e/search-navbar/search-navbar.e2e-spec.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { ProtractorPage } from './search-navbar.po';
|
||||||
|
import { browser } from 'protractor';
|
||||||
|
|
||||||
|
describe('protractor SearchNavbar', () => {
|
||||||
|
let page: ProtractorPage;
|
||||||
|
let queryString: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
page = new ProtractorPage();
|
||||||
|
queryString = 'the test query';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should go to search page with correct query if submitted (from home)', () => {
|
||||||
|
page.navigateToHome();
|
||||||
|
return checkIfSearchWorks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should go to search page with correct query if submitted (from search)', () => {
|
||||||
|
page.navigateToSearch();
|
||||||
|
return checkIfSearchWorks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('check if can submit search box with pressing button', () => {
|
||||||
|
page.navigateToHome();
|
||||||
|
page.expandAndFocusSearchBox();
|
||||||
|
page.setCurrentQuery(queryString);
|
||||||
|
page.submitNavbarSearchForm();
|
||||||
|
browser.wait(() => {
|
||||||
|
return browser.getCurrentUrl().then((url: string) => {
|
||||||
|
return url.indexOf('query=' + encodeURI(queryString)) !== -1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkIfSearchWorks(): boolean {
|
||||||
|
page.setCurrentQuery(queryString);
|
||||||
|
page.submitByPressingEnter();
|
||||||
|
browser.wait(() => {
|
||||||
|
return browser.getCurrentUrl().then((url: string) => {
|
||||||
|
return url.indexOf('query=' + encodeURI(queryString)) !== -1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
40
e2e/search-navbar/search-navbar.po.ts
Normal file
40
e2e/search-navbar/search-navbar.po.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { browser, element, by, protractor } from 'protractor';
|
||||||
|
import { promise } from 'selenium-webdriver';
|
||||||
|
|
||||||
|
export class ProtractorPage {
|
||||||
|
HOME = '/home';
|
||||||
|
SEARCH = '/search';
|
||||||
|
|
||||||
|
navigateToHome() {
|
||||||
|
return browser.get(this.HOME);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToSearch() {
|
||||||
|
return browser.get(this.SEARCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentQuery(): promise.Promise<string> {
|
||||||
|
return element(by.css('#search-navbar-container form input')).getAttribute('value');
|
||||||
|
}
|
||||||
|
|
||||||
|
expandAndFocusSearchBox() {
|
||||||
|
element(by.css('#search-navbar-container form a')).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentQuery(query: string) {
|
||||||
|
element(by.css('#search-navbar-container form input[name="query"]')).sendKeys(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitNavbarSearchForm() {
|
||||||
|
element(by.css('#search-navbar-container form .submit-icon')).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
submitByPressingEnter() {
|
||||||
|
element(by.css('#search-navbar-container form input[name="query"]')).sendKeys(protractor.Key.ENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitByPressingEnter() {
|
||||||
|
element(by.css('#search-navbar-container form input[name="query"]')).sendKeys(protractor.Key.ENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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', () => {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { browser, element, by, protractor } from 'protractor';
|
import { browser, by, element, protractor } from 'protractor';
|
||||||
import { promise } from 'selenium-webdriver';
|
import { promise } from 'selenium-webdriver';
|
||||||
|
|
||||||
export class ProtractorPage {
|
export class ProtractorPage {
|
||||||
@@ -27,15 +27,15 @@ export class ProtractorPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setCurrentScope(scope: string) {
|
setCurrentScope(scope: string) {
|
||||||
element(by.css('option[value="' + scope + '"]')).click();
|
element(by.css('#search-form option[value="' + scope + '"]')).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentQuery(query: string) {
|
setCurrentQuery(query: string) {
|
||||||
element(by.css('input[name="query"]')).sendKeys(query);
|
element(by.css('#search-form input[name="query"]')).sendKeys(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
submitSearchForm() {
|
submitSearchForm() {
|
||||||
element(by.css('button.search-button')).click();
|
element(by.css('#search-form button.search-button')).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRandomScopeOption(): promise.Promise<string> {
|
getRandomScopeOption(): promise.Promise<string> {
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
var SpecReporter = require('jasmine-spec-reporter').SpecReporter;
|
var SpecReporter = require('jasmine-spec-reporter').SpecReporter;
|
||||||
|
|
||||||
exports.config = {
|
exports.config = {
|
||||||
allScriptsTimeout: 11000,
|
allScriptsTimeout: 600000,
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// Uncomment to run tests using a remote Selenium server
|
// Uncomment to run tests using a remote Selenium server
|
||||||
//seleniumAddress: 'http://selenium.address:4444/wd/hub',
|
//seleniumAddress: 'http://selenium.address:4444/wd/hub',
|
||||||
@@ -73,7 +73,7 @@ exports.config = {
|
|||||||
framework: 'jasmine',
|
framework: 'jasmine',
|
||||||
jasmineNodeOpts: {
|
jasmineNodeOpts: {
|
||||||
showColors: true,
|
showColors: true,
|
||||||
defaultTimeoutInterval: 30000,
|
defaultTimeoutInterval: 600000,
|
||||||
print: function () {}
|
print: function () {}
|
||||||
},
|
},
|
||||||
useAllAngular2AppRoots: true,
|
useAllAngular2AppRoots: true,
|
||||||
|
@@ -338,8 +338,40 @@
|
|||||||
|
|
||||||
"collection.edit.tabs.roles.title": "Collection Edit - Roles",
|
"collection.edit.tabs.roles.title": "Collection Edit - Roles",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.external": "This collection harvests its content from an external source",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.form.errors.oaiSource.required": "You must provide a set id of the target collection.",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.form.harvestType": "Content being harvested",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.form.head": "Configure an external source",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.form.metadataConfigId": "Metadata Format",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.form.oaiSetId": "OAI specific set id",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.form.oaiSource": "OAI Provider",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.form.options.harvestType.METADATA_AND_BITSTREAMS": "Harvest metadata and bitstreams (requires ORE support)",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.form.options.harvestType.METADATA_AND_REF": "Harvest metadata and references to bitstreams (requires ORE support)",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.form.options.harvestType.METADATA_ONLY": "Harvest metadata only",
|
||||||
|
|
||||||
"collection.edit.tabs.source.head": "Content Source",
|
"collection.edit.tabs.source.head": "Content Source",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.notifications.discarded.title": "Changed discarded",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.notifications.invalid.title": "Metadata invalid",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.notifications.saved.content": "Your changes to this collection's content source were saved.",
|
||||||
|
|
||||||
|
"collection.edit.tabs.source.notifications.saved.title": "Content Source saved",
|
||||||
|
|
||||||
"collection.edit.tabs.source.title": "Collection Edit - Content Source",
|
"collection.edit.tabs.source.title": "Collection Edit - Content Source",
|
||||||
|
|
||||||
|
|
||||||
@@ -382,6 +414,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"collection.source.update.notifications.error.content": "The provided settings have been tested and didn't work.",
|
||||||
|
|
||||||
|
"collection.source.update.notifications.error.title": "Server Error",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"communityList.tabTitle": "DSpace - Community List",
|
"communityList.tabTitle": "DSpace - Community List",
|
||||||
|
|
||||||
"communityList.title": "List of Communities",
|
"communityList.title": "List of Communities",
|
||||||
@@ -997,6 +1035,8 @@
|
|||||||
|
|
||||||
"loading.collections": "Loading collections...",
|
"loading.collections": "Loading collections...",
|
||||||
|
|
||||||
|
"loading.content-source": "Loading content source...",
|
||||||
|
|
||||||
"loading.community": "Loading community...",
|
"loading.community": "Loading community...",
|
||||||
|
|
||||||
"loading.default": "Loading...",
|
"loading.default": "Loading...",
|
||||||
@@ -1566,6 +1606,8 @@
|
|||||||
|
|
||||||
"search.results.no-results-link": "quotes around it",
|
"search.results.no-results-link": "quotes around it",
|
||||||
|
|
||||||
|
"search.results.empty": "Your search returned no results.",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"search.sidebar.close": "Back to results",
|
"search.sidebar.close": "Back to results",
|
||||||
@@ -1621,8 +1663,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",
|
||||||
@@ -1639,13 +1740,21 @@
|
|||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items",
|
"submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items",
|
||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Search for Authors",
|
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Local Authors ({{ count }})",
|
||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Search for Journals",
|
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})",
|
||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Search for Journal Issues",
|
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Local Journal Issues ({{ count }})",
|
||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Search for Journal Volumes",
|
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Local Journal Volumes ({{ count }})",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Journals ({{ count }})",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding Agency": "Search for Funding Agencies",
|
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding Agency": "Search for Funding Agencies",
|
||||||
|
|
||||||
@@ -1679,6 +1788,14 @@
|
|||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Selected Issue",
|
"submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Selected Issue",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Search Results",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||||
|
|
||||||
|
"submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results",
|
||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don\'t you can still use it for this submission.",
|
"submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don\'t you can still use it for this submission.",
|
||||||
|
|
||||||
"submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant",
|
"submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant",
|
||||||
|
50
src/Typescript-codestyle.xml
Normal file
50
src/Typescript-codestyle.xml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<code_scheme name="Default (1)" version="173">
|
||||||
|
<option name="INSERT_OVERRIDE_ANNOTATION" value="false" />
|
||||||
|
<option name="RIGHT_MARGIN" value="100" />
|
||||||
|
<JavaCodeStyleSettings>
|
||||||
|
<option name="INSERT_OVERRIDE_ANNOTATION" value="false" />
|
||||||
|
</JavaCodeStyleSettings>
|
||||||
|
<TypeScriptCodeStyleSettings version="0">
|
||||||
|
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||||
|
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||||
|
<option name="IMPORT_SORT_MODULE_NAME" value="true" />
|
||||||
|
</TypeScriptCodeStyleSettings>
|
||||||
|
<XML>
|
||||||
|
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
||||||
|
</XML>
|
||||||
|
<codeStyleSettings language="CoffeeScript">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="4" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="TAB_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="GSP">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="Groovy">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="JavaScript">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="8" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="SCSS">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="TypeScript">
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
@@ -17,6 +17,7 @@ import { CreateCollectionParentSelectorComponent } from '../../shared/dso-select
|
|||||||
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||||
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||||
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||||
|
import {CreateItemParentSelectorComponent} from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component representing the admin sidebar
|
* Component representing the admin sidebar
|
||||||
@@ -137,18 +138,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
parentID: 'new',
|
parentID: 'new',
|
||||||
active: false,
|
active: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.ONCLICK,
|
|
||||||
// text: 'menu.section.new_item',
|
|
||||||
// function: () => {
|
|
||||||
// this.modalService.open(CreateItemParentSelectorComponent);
|
|
||||||
// }
|
|
||||||
// } as OnClickMenuItemModel,
|
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.ONCLICK,
|
||||||
text: 'menu.section.new_item',
|
text: 'menu.section.new_item',
|
||||||
link: '/submit'
|
function: () => {
|
||||||
} as LinkMenuItemModel,
|
this.modalService.open(CreateItemParentSelectorComponent);
|
||||||
|
}
|
||||||
|
} as OnClickMenuItemModel,
|
||||||
|
// model: {
|
||||||
|
// type: MenuItemType.LINK,
|
||||||
|
// text: 'menu.section.new_item',
|
||||||
|
// link: '/submit'
|
||||||
|
// } as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'new_item_version',
|
id: 'new_item_version',
|
||||||
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
<ng-container *ngVar="(subCollectionsRDObs | async) as subCollectionsRD">
|
<ng-container *ngVar="(subCollectionsRDObs | async) as subCollectionsRD">
|
||||||
<div *ngIf="subCollectionsRD?.hasSucceeded && subCollectionsRD?.payload.totalElements > 0" @fadeIn>
|
<div *ngIf="subCollectionsRD?.hasSucceeded && subCollectionsRD?.payload.totalElements > 0" @fadeIn>
|
||||||
<h2>{{'community.sub-collection-list.head' | translate}}</h2>
|
<h2>{{'community.sub-collection-list.head' | translate}}</h2>
|
||||||
<ul>
|
<ds-viewable-collection
|
||||||
<li *ngFor="let collection of subCollectionsRD?.payload.page">
|
[config]="config"
|
||||||
<p>
|
[sortConfig]="sortConfig"
|
||||||
<span class="lead"><a [routerLink]="['/collections', collection.id]">{{collection.name}}</a></span><br>
|
[objects]="subCollectionsRD"
|
||||||
<span class="text-muted">{{collection.shortDescription}}</span>
|
[hideGear]="false"
|
||||||
</p>
|
(paginationChange)="onPaginationChange($event)">
|
||||||
</li>
|
</ds-viewable-collection>
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="subCollectionsRD?.hasFailed" message="{{'error.sub-collections' | translate}}"></ds-error>
|
<ds-error *ngIf="subCollectionsRD?.hasFailed" message="{{'error.sub-collections' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="subCollectionsRD?.isLoading" message="{{'loading.sub-collections' | translate}}"></ds-loading>
|
<ds-loading *ngIf="subCollectionsRD?.isLoading" message="{{'loading.sub-collections' | translate}}"></ds-loading>
|
||||||
|
@@ -0,0 +1,182 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
import { FindListOptions } from '../../core/data/request.models';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
|
import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub';
|
||||||
|
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
||||||
|
|
||||||
|
describe('CommunityPageSubCollectionList Component', () => {
|
||||||
|
let comp: CommunityPageSubCollectionListComponent;
|
||||||
|
let fixture: ComponentFixture<CommunityPageSubCollectionListComponent>;
|
||||||
|
let collectionDataServiceStub: any;
|
||||||
|
let subCollList = [];
|
||||||
|
|
||||||
|
const collections = [Object.assign(new Community(), {
|
||||||
|
id: '123456789-1',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'Collection 1' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-2',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'Collection 2' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-3',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'Collection 3' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-4',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'Collection 4' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-5',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'Collection 5' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-6',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'Collection 6' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-7',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'Collection 7' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockCommunity = Object.assign(new Community(), {
|
||||||
|
id: '123456789',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'Test title' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
collectionDataServiceStub = {
|
||||||
|
findByParent(parentUUID: string, options: FindListOptions = {}) {
|
||||||
|
let currentPage = options.currentPage;
|
||||||
|
let elementsPerPage = options.elementsPerPage;
|
||||||
|
if (currentPage === undefined) {
|
||||||
|
currentPage = 1
|
||||||
|
}
|
||||||
|
elementsPerPage = 5;
|
||||||
|
const startPageIndex = (currentPage - 1) * elementsPerPage;
|
||||||
|
let endPageIndex = (currentPage * elementsPerPage);
|
||||||
|
if (endPageIndex > subCollList.length) {
|
||||||
|
endPageIndex = subCollList.length;
|
||||||
|
}
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subCollList.slice(startPageIndex, endPageIndex)));
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
SharedModule,
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
NgbModule.forRoot(),
|
||||||
|
NoopAnimationsModule
|
||||||
|
],
|
||||||
|
declarations: [CommunityPageSubCollectionListComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CollectionDataService, useValue: collectionDataServiceStub },
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
|
{ provide: SelectableListService, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CommunityPageSubCollectionListComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.community = mockCommunity;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a list of collections', () => {
|
||||||
|
subCollList = collections;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const collList = fixture.debugElement.queryAll(By.css('li'));
|
||||||
|
expect(collList.length).toEqual(5);
|
||||||
|
expect(collList[0].nativeElement.textContent).toContain('Collection 1');
|
||||||
|
expect(collList[1].nativeElement.textContent).toContain('Collection 2');
|
||||||
|
expect(collList[2].nativeElement.textContent).toContain('Collection 3');
|
||||||
|
expect(collList[3].nativeElement.textContent).toContain('Collection 4');
|
||||||
|
expect(collList[4].nativeElement.textContent).toContain('Collection 5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display the header when list of collections is empty', () => {
|
||||||
|
subCollList = [];
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const subComHead = fixture.debugElement.queryAll(By.css('h2'));
|
||||||
|
expect(subComHead.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update list of collections on pagination change', () => {
|
||||||
|
subCollList = collections;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const pagination = Object.create({
|
||||||
|
pagination:{
|
||||||
|
id: comp.pageId,
|
||||||
|
currentPage: 2,
|
||||||
|
pageSize: 5
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
field: 'dc.title',
|
||||||
|
direction: 'ASC'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
comp.onPaginationChange(pagination);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const collList = fixture.debugElement.queryAll(By.css('li'));
|
||||||
|
expect(collList.length).toEqual(2);
|
||||||
|
expect(collList[0].nativeElement.textContent).toContain('Collection 6');
|
||||||
|
expect(collList[1].nativeElement.textContent).toContain('Collection 7');
|
||||||
|
});
|
||||||
|
});
|
@@ -1,12 +1,16 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Collection } from '../../core/shared/collection.model';
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
|
||||||
import { fadeIn } from '../../shared/animations/fade';
|
import { fadeIn } from '../../shared/animations/fade';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-community-page-sub-collection-list',
|
selector: 'ds-community-page-sub-collection-list',
|
||||||
@@ -16,9 +20,60 @@ import { PaginatedList } from '../../core/data/paginated-list';
|
|||||||
})
|
})
|
||||||
export class CommunityPageSubCollectionListComponent implements OnInit {
|
export class CommunityPageSubCollectionListComponent implements OnInit {
|
||||||
@Input() community: Community;
|
@Input() community: Community;
|
||||||
subCollectionsRDObs: Observable<RemoteData<PaginatedList<Collection>>>;
|
|
||||||
|
/**
|
||||||
|
* The pagination configuration
|
||||||
|
*/
|
||||||
|
config: PaginationComponentOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pagination id
|
||||||
|
*/
|
||||||
|
pageId = 'community-collections-pagination';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sorting configuration
|
||||||
|
*/
|
||||||
|
sortConfig: SortOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of remote data objects of communities' collections
|
||||||
|
*/
|
||||||
|
subCollectionsRDObs: BehaviorSubject<RemoteData<PaginatedList<Collection>>> = new BehaviorSubject<RemoteData<PaginatedList<Collection>>>({} as any);
|
||||||
|
|
||||||
|
constructor(private cds: CollectionDataService) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.subCollectionsRDObs = this.community.collections;
|
this.config = new PaginationComponentOptions();
|
||||||
|
this.config.id = this.pageId;
|
||||||
|
this.config.pageSize = 5;
|
||||||
|
this.config.currentPage = 1;
|
||||||
|
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
|
this.updatePage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when one of the pagination settings is changed
|
||||||
|
* @param event The new pagination data
|
||||||
|
*/
|
||||||
|
onPaginationChange(event) {
|
||||||
|
this.config.currentPage = event.pagination.currentPage;
|
||||||
|
this.config.pageSize = event.pagination.pageSize;
|
||||||
|
this.sortConfig.field = event.sort.field;
|
||||||
|
this.sortConfig.direction = event.sort.direction;
|
||||||
|
this.updatePage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the list of collections
|
||||||
|
*/
|
||||||
|
updatePage() {
|
||||||
|
this.cds.findByParent(this.community.id,{
|
||||||
|
currentPage: this.config.currentPage,
|
||||||
|
elementsPerPage: this.config.pageSize,
|
||||||
|
sort: { field: this.sortConfig.field, direction: this.sortConfig.direction }
|
||||||
|
}).pipe(take(1)).subscribe((results) => {
|
||||||
|
this.subCollectionsRDObs.next(results);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
<ng-container *ngVar="(subCommunitiesRDObs | async) as subCommunitiesRD">
|
<ng-container *ngVar="(subCommunitiesRDObs | async) as subCommunitiesRD">
|
||||||
<div *ngIf="subCommunitiesRD?.hasSucceeded && subCommunitiesRD?.payload.totalElements > 0" @fadeIn>
|
<div *ngIf="subCommunitiesRD?.hasSucceeded && subCommunitiesRD?.payload.totalElements > 0" @fadeIn>
|
||||||
<h2>{{'community.sub-community-list.head' | translate}}</h2>
|
<h2>{{'community.sub-community-list.head' | translate}}</h2>
|
||||||
<ul>
|
<ds-viewable-collection
|
||||||
<li *ngFor="let community of subCommunitiesRD?.payload.page">
|
[config]="config"
|
||||||
<p>
|
[sortConfig]="sortConfig"
|
||||||
<span class="lead"><a [routerLink]="['/communities', community.id]">{{community.name}}</a></span><br>
|
[objects]="subCommunitiesRD"
|
||||||
<span class="text-muted">{{community.shortDescription}}</span>
|
[hideGear]="false"
|
||||||
</p>
|
(paginationChange)="onPaginationChange($event)">
|
||||||
</li>
|
</ds-viewable-collection>
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="subCommunitiesRD?.hasFailed" message="{{'error.sub-communities' | translate}}"></ds-error>
|
<ds-error *ngIf="subCommunitiesRD?.hasFailed" message="{{'error.sub-communities' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="subCommunitiesRD?.isLoading" message="{{'loading.sub-communities' | translate}}"></ds-loading>
|
<ds-loading *ngIf="subCommunitiesRD?.isLoading" message="{{'loading.sub-communities' | translate}}"></ds-loading>
|
||||||
|
@@ -1,21 +1,29 @@
|
|||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {NO_ERRORS_SCHEMA} from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import {CommunityPageSubCommunityListComponent} from './community-page-sub-community-list.component';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import {Community} from '../../core/shared/community.model';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import {RemoteData} from '../../core/data/remote-data';
|
import { By } from '@angular/platform-browser';
|
||||||
import {PaginatedList} from '../../core/data/paginated-list';
|
|
||||||
import {PageInfo} from '../../core/shared/page-info.model';
|
|
||||||
import {SharedModule} from '../../shared/shared.module';
|
|
||||||
import {RouterTestingModule} from '@angular/router/testing';
|
|
||||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
|
||||||
import {By} from '@angular/platform-browser';
|
|
||||||
import {of as observableOf, Observable } from 'rxjs';
|
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
|
||||||
|
|
||||||
describe('SubCommunityList Component', () => {
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
|
import { FindListOptions } from '../../core/data/request.models';
|
||||||
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
|
import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
||||||
|
|
||||||
|
describe('CommunityPageSubCommunityListComponent Component', () => {
|
||||||
let comp: CommunityPageSubCommunityListComponent;
|
let comp: CommunityPageSubCommunityListComponent;
|
||||||
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
|
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
|
||||||
|
let communityDataServiceStub: any;
|
||||||
|
let subCommList = [];
|
||||||
|
|
||||||
const subcommunities = [Object.assign(new Community(), {
|
const subcommunities = [Object.assign(new Community(), {
|
||||||
id: '123456789-1',
|
id: '123456789-1',
|
||||||
@@ -32,34 +40,92 @@ describe('SubCommunityList Component', () => {
|
|||||||
{ language: 'en_US', value: 'SubCommunity 2' }
|
{ language: 'en_US', value: 'SubCommunity 2' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-3',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'SubCommunity 3' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '12345678942',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'SubCommunity 4' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-5',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'SubCommunity 5' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-6',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'SubCommunity 6' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-7',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'SubCommunity 7' }
|
||||||
|
]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
const emptySubCommunitiesCommunity = Object.assign(new Community(), {
|
const mockCommunity = Object.assign(new Community(), {
|
||||||
|
id: '123456789',
|
||||||
metadata: {
|
metadata: {
|
||||||
'dc.title': [
|
'dc.title': [
|
||||||
{ language: 'en_US', value: 'Test title' }
|
{ language: 'en_US', value: 'Test title' }
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), []))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockCommunity = Object.assign(new Community(), {
|
communityDataServiceStub = {
|
||||||
metadata: {
|
findByParent(parentUUID: string, options: FindListOptions = {}) {
|
||||||
'dc.title': [
|
let currentPage = options.currentPage;
|
||||||
{ language: 'en_US', value: 'Test title' }
|
let elementsPerPage = options.elementsPerPage;
|
||||||
]
|
if (currentPage === undefined) {
|
||||||
},
|
currentPage = 1
|
||||||
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subcommunities))
|
}
|
||||||
})
|
elementsPerPage = 5;
|
||||||
;
|
|
||||||
|
const startPageIndex = (currentPage - 1) * elementsPerPage;
|
||||||
|
let endPageIndex = (currentPage * elementsPerPage);
|
||||||
|
if (endPageIndex > subCommList.length) {
|
||||||
|
endPageIndex = subCommList.length;
|
||||||
|
}
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subCommList.slice(startPageIndex, endPageIndex)));
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), SharedModule,
|
imports: [
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
SharedModule,
|
||||||
RouterTestingModule.withRoutes([]),
|
RouterTestingModule.withRoutes([]),
|
||||||
NoopAnimationsModule],
|
NgbModule.forRoot(),
|
||||||
|
NoopAnimationsModule
|
||||||
|
],
|
||||||
declarations: [CommunityPageSubCommunityListComponent],
|
declarations: [CommunityPageSubCommunityListComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CommunityDataService, useValue: communityDataServiceStub },
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
|
{ provide: SelectableListService, useValue: {} },
|
||||||
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
@@ -67,23 +133,52 @@ describe('SubCommunityList Component', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent);
|
fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
|
comp.community = mockCommunity;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display a list of subCommunities', () => {
|
it('should display a list of sub-communities', () => {
|
||||||
comp.community = mockCommunity;
|
subCommList = subcommunities;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const subComList = fixture.debugElement.queryAll(By.css('li'));
|
const subComList = fixture.debugElement.queryAll(By.css('li'));
|
||||||
expect(subComList.length).toEqual(2);
|
expect(subComList.length).toEqual(5);
|
||||||
expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1');
|
expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1');
|
||||||
expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2');
|
expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2');
|
||||||
|
expect(subComList[2].nativeElement.textContent).toContain('SubCommunity 3');
|
||||||
|
expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4');
|
||||||
|
expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not display the header when subCommunities are empty', () => {
|
it('should not display the header when list of sub-communities is empty', () => {
|
||||||
comp.community = emptySubCommunitiesCommunity;
|
subCommList = [];
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const subComHead = fixture.debugElement.queryAll(By.css('h2'));
|
const subComHead = fixture.debugElement.queryAll(By.css('h2'));
|
||||||
expect(subComHead.length).toEqual(0);
|
expect(subComHead.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update list of sub-communities on pagination change', () => {
|
||||||
|
subCommList = subcommunities;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const pagination = Object.create({
|
||||||
|
pagination:{
|
||||||
|
id: comp.pageId,
|
||||||
|
currentPage: 2,
|
||||||
|
pageSize: 5
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
field: 'dc.title',
|
||||||
|
direction: 'ASC'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
comp.onPaginationChange(pagination);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const collList = fixture.debugElement.queryAll(By.css('li'));
|
||||||
|
expect(collList.length).toEqual(2);
|
||||||
|
expect(collList[0].nativeElement.textContent).toContain('SubCommunity 6');
|
||||||
|
expect(collList[1].nativeElement.textContent).toContain('SubCommunity 7');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,26 +1,82 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
|
||||||
import { fadeIn } from '../../shared/animations/fade';
|
import { fadeIn } from '../../shared/animations/fade';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
import {Observable} from 'rxjs';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-community-page-sub-community-list',
|
selector: 'ds-community-page-sub-community-list',
|
||||||
styleUrls: ['./community-page-sub-community-list.component.scss'],
|
styleUrls: ['./community-page-sub-community-list.component.scss'],
|
||||||
templateUrl: './community-page-sub-community-list.component.html',
|
templateUrl: './community-page-sub-community-list.component.html',
|
||||||
animations:[fadeIn]
|
animations: [fadeIn]
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Component to render the sub-communities of a Community
|
* Component to render the sub-communities of a Community
|
||||||
*/
|
*/
|
||||||
export class CommunityPageSubCommunityListComponent implements OnInit {
|
export class CommunityPageSubCommunityListComponent implements OnInit {
|
||||||
@Input() community: Community;
|
@Input() community: Community;
|
||||||
subCommunitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
|
||||||
|
/**
|
||||||
|
* The pagination configuration
|
||||||
|
*/
|
||||||
|
config: PaginationComponentOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pagination id
|
||||||
|
*/
|
||||||
|
pageId = 'community-subCommunities-pagination';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sorting configuration
|
||||||
|
*/
|
||||||
|
sortConfig: SortOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of remote data objects of communities' collections
|
||||||
|
*/
|
||||||
|
subCommunitiesRDObs: BehaviorSubject<RemoteData<PaginatedList<Community>>> = new BehaviorSubject<RemoteData<PaginatedList<Community>>>({} as any);
|
||||||
|
|
||||||
|
constructor(private cds: CommunityDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.subCommunitiesRDObs = this.community.subcommunities;
|
this.config = new PaginationComponentOptions();
|
||||||
|
this.config.id = this.pageId;
|
||||||
|
this.config.pageSize = 5;
|
||||||
|
this.config.currentPage = 1;
|
||||||
|
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
|
this.updatePage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when one of the pagination settings is changed
|
||||||
|
* @param event The new pagination data
|
||||||
|
*/
|
||||||
|
onPaginationChange(event) {
|
||||||
|
this.config.currentPage = event.pagination.currentPage;
|
||||||
|
this.config.pageSize = event.pagination.pageSize;
|
||||||
|
this.sortConfig.field = event.sort.field;
|
||||||
|
this.sortConfig.direction = event.sort.direction;
|
||||||
|
this.updatePage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the list of sub-communities
|
||||||
|
*/
|
||||||
|
updatePage() {
|
||||||
|
this.cds.findByParent(this.community.id, {
|
||||||
|
currentPage: this.config.currentPage,
|
||||||
|
elementsPerPage: this.config.pageSize,
|
||||||
|
sort: { field: this.sortConfig.field, direction: this.sortConfig.direction }
|
||||||
|
}).pipe(take(1)).subscribe((results) => {
|
||||||
|
this.subCommunitiesRDObs.next(results);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,14 +11,14 @@ import { Site } from '../core/shared/site.model';
|
|||||||
})
|
})
|
||||||
export class HomePageComponent implements OnInit {
|
export class HomePageComponent implements OnInit {
|
||||||
|
|
||||||
site$:Observable<Site>;
|
site$: Observable<Site>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route:ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit():void {
|
ngOnInit(): void {
|
||||||
this.site$ = this.route.data.pipe(
|
this.site$ = this.route.data.pipe(
|
||||||
map((data) => data.site as Site),
|
map((data) => data.site as Site),
|
||||||
);
|
);
|
||||||
|
@@ -10,7 +10,7 @@ import { take } from 'rxjs/operators';
|
|||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HomePageResolver implements Resolve<Site> {
|
export class HomePageResolver implements Resolve<Site> {
|
||||||
constructor(private siteService:SiteDataService) {
|
constructor(private siteService: SiteDataService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,7 +19,7 @@ export class HomePageResolver implements Resolve<Site> {
|
|||||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
* @returns Observable<Site> Emits the found Site object, or an error if something went wrong
|
* @returns Observable<Site> Emits the found Site object, or an error if something went wrong
|
||||||
*/
|
*/
|
||||||
resolve(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<Site> | Promise<Site> | Site {
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Site> | Promise<Site> | Site {
|
||||||
return this.siteService.find().pipe(take(1));
|
return this.siteService.find().pipe(take(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,161 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { TopLevelCommunityListComponent } from './top-level-community-list.component';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
|
import { FindListOptions } from '../../core/data/request.models';
|
||||||
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
|
import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
||||||
|
|
||||||
|
describe('TopLevelCommunityList Component', () => {
|
||||||
|
let comp: TopLevelCommunityListComponent;
|
||||||
|
let fixture: ComponentFixture<TopLevelCommunityListComponent>;
|
||||||
|
let communityDataServiceStub: any;
|
||||||
|
|
||||||
|
const topCommList = [Object.assign(new Community(), {
|
||||||
|
id: '123456789-1',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'TopCommunity 1' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-2',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'TopCommunity 2' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-3',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'TopCommunity 3' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '12345678942',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'TopCommunity 4' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-5',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'TopCommunity 5' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-6',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'TopCommunity 6' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
id: '123456789-7',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{ language: 'en_US', value: 'TopCommunity 7' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
communityDataServiceStub = {
|
||||||
|
findTop(options: FindListOptions = {}) {
|
||||||
|
let currentPage = options.currentPage;
|
||||||
|
let elementsPerPage = options.elementsPerPage;
|
||||||
|
if (currentPage === undefined) {
|
||||||
|
currentPage = 1
|
||||||
|
}
|
||||||
|
elementsPerPage = 5;
|
||||||
|
|
||||||
|
const startPageIndex = (currentPage - 1) * elementsPerPage;
|
||||||
|
let endPageIndex = (currentPage * elementsPerPage);
|
||||||
|
if (endPageIndex > topCommList.length) {
|
||||||
|
endPageIndex = topCommList.length;
|
||||||
|
}
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), topCommList.slice(startPageIndex, endPageIndex)));
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
SharedModule,
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
NgbModule.forRoot(),
|
||||||
|
NoopAnimationsModule
|
||||||
|
],
|
||||||
|
declarations: [TopLevelCommunityListComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CommunityDataService, useValue: communityDataServiceStub },
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
|
{ provide: SelectableListService, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(TopLevelCommunityListComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a list of top-communities', () => {
|
||||||
|
const subComList = fixture.debugElement.queryAll(By.css('li'));
|
||||||
|
|
||||||
|
expect(subComList.length).toEqual(5);
|
||||||
|
expect(subComList[0].nativeElement.textContent).toContain('TopCommunity 1');
|
||||||
|
expect(subComList[1].nativeElement.textContent).toContain('TopCommunity 2');
|
||||||
|
expect(subComList[2].nativeElement.textContent).toContain('TopCommunity 3');
|
||||||
|
expect(subComList[3].nativeElement.textContent).toContain('TopCommunity 4');
|
||||||
|
expect(subComList[4].nativeElement.textContent).toContain('TopCommunity 5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update list of top-communities on pagination change', () => {
|
||||||
|
const pagination = Object.create({
|
||||||
|
pagination:{
|
||||||
|
id: comp.pageId,
|
||||||
|
currentPage: 2,
|
||||||
|
pageSize: 5
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
field: 'dc.title',
|
||||||
|
direction: 'ASC'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
comp.onPaginationChange(pagination);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const collList = fixture.debugElement.queryAll(By.css('li'));
|
||||||
|
expect(collList.length).toEqual(2);
|
||||||
|
expect(collList[0].nativeElement.textContent).toContain('TopCommunity 6');
|
||||||
|
expect(collList[1].nativeElement.textContent).toContain('TopCommunity 7');
|
||||||
|
});
|
||||||
|
});
|
@@ -1,15 +1,15 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
|
||||||
import { fadeInOut } from '../../shared/animations/fade';
|
import { fadeInOut } from '../../shared/animations/fade';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* this component renders the Top-Level Community list
|
* this component renders the Top-Level Community list
|
||||||
@@ -33,6 +33,11 @@ export class TopLevelCommunityListComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
config: PaginationComponentOptions;
|
config: PaginationComponentOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pagination id
|
||||||
|
*/
|
||||||
|
pageId = 'top-level-pagination';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The sorting configuration
|
* The sorting configuration
|
||||||
*/
|
*/
|
||||||
@@ -40,7 +45,7 @@ export class TopLevelCommunityListComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(private cds: CommunityDataService) {
|
constructor(private cds: CommunityDataService) {
|
||||||
this.config = new PaginationComponentOptions();
|
this.config = new PaginationComponentOptions();
|
||||||
this.config.id = 'top-level-pagination';
|
this.config.id = this.pageId;
|
||||||
this.config.pageSize = 5;
|
this.config.pageSize = 5;
|
||||||
this.config.currentPage = 1;
|
this.config.currentPage = 1;
|
||||||
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
@@ -55,10 +60,10 @@ export class TopLevelCommunityListComponent implements OnInit {
|
|||||||
* @param event The new pagination data
|
* @param event The new pagination data
|
||||||
*/
|
*/
|
||||||
onPaginationChange(event) {
|
onPaginationChange(event) {
|
||||||
this.config.currentPage = event.page;
|
this.config.currentPage = event.pagination.currentPage;
|
||||||
this.config.pageSize = event.pageSize;
|
this.config.pageSize = event.pagination.pageSize;
|
||||||
this.sortConfig.field = event.sortField;
|
this.sortConfig.field = event.sort.field;
|
||||||
this.sortConfig.direction = event.sortDirection;
|
this.sortConfig.direction = event.sort.direction;
|
||||||
this.updatePage();
|
this.updatePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,7 +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';
|
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
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import {Component, Input, OnInit} 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 {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';
|
||||||
|
@@ -11,7 +11,7 @@ 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';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
let objectUpdatesService;
|
let objectUpdatesService;
|
||||||
const url = 'http://test-url.com/test-url';
|
const url = 'http://test-url.com/test-url';
|
||||||
|
@@ -1,15 +1,15 @@
|
|||||||
import {Component, Input, OnChanges, OnInit} from '@angular/core';
|
import { Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||||
import {combineLatest as observableCombineLatest, Observable} from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import {filter, map, switchMap, take, tap} from 'rxjs/operators';
|
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
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 { DeleteRelationship, FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
|
||||||
import {ObjectUpdatesService} from '../../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import {Relationship} from '../../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
import {Item} from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import {getRemoteDataPayload, getSucceededRemoteData} from '../../../../core/shared/operators';
|
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 { hasValue, isNotEmpty } from '../../../../shared/empty.util';
|
||||||
import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
// tslint:disable-next-line:component-selector
|
// tslint:disable-next-line:component-selector
|
||||||
|
@@ -1,32 +1,32 @@
|
|||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import {ItemRelationshipsComponent} from './item-relationships.component';
|
import { ItemRelationshipsComponent } from './item-relationships.component';
|
||||||
import {ChangeDetectorRef, DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';
|
import { ChangeDetectorRef, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import {INotification, Notification} from '../../../shared/notifications/models/notification.model';
|
import { INotification, Notification } from '../../../shared/notifications/models/notification.model';
|
||||||
import {NotificationType} from '../../../shared/notifications/models/notification-type';
|
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
||||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
import {TestScheduler} from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
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 {ItemDataService} from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
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 {Relationship} from '../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
import {combineLatest as observableCombineLatest, of as observableOf} 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';
|
||||||
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 {RelationshipService} from '../../../core/data/relationship.service';
|
import { RelationshipService } from '../../../core/data/relationship.service';
|
||||||
import {ObjectCacheService} from '../../../core/cache/object-cache.service';
|
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 { EntityTypeService } from '../../../core/data/entity-type.service';
|
||||||
import {ItemType} from '../../../core/shared/item-relationships/item-type.model';
|
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
||||||
|
|
||||||
let comp: any;
|
let comp: any;
|
||||||
let fixture: ComponentFixture<ItemRelationshipsComponent>;
|
let fixture: ComponentFixture<ItemRelationshipsComponent>;
|
||||||
|
@@ -1,29 +1,29 @@
|
|||||||
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 {DeleteRelationship, 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, map, switchMap, take} 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';
|
||||||
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
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 {ErrorResponse, RestResponse} from '../../../core/cache/response.models';
|
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
|
||||||
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 {getRemoteDataPayload, 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 { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
import {ItemType} from '../../../core/shared/item-relationships/item-type.model';
|
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
||||||
import {EntityTypeService} from '../../../core/data/entity-type.service';
|
import { EntityTypeService } from '../../../core/data/entity-type.service';
|
||||||
import {isNotEmptyOperator} from '../../../shared/empty.util';
|
import { isNotEmptyOperator } from '../../../shared/empty.util';
|
||||||
import {FieldChangeType} from '../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
||||||
import {Relationship} from '../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-relationships',
|
selector: 'ds-item-relationships',
|
||||||
|
@@ -18,7 +18,7 @@ export class LookupGuard implements CanActivate {
|
|||||||
constructor(private dsoService: DsoRedirectDataService) {
|
constructor(private dsoService: DsoRedirectDataService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
canActivate(route: ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable<boolean> {
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||||
const params = this.getLookupParams(route);
|
const params = this.getLookupParams(route);
|
||||||
return this.dsoService.findById(params.id, params.type).pipe(
|
return this.dsoService.findById(params.id, params.type).pipe(
|
||||||
map((response: RemoteData<FindByIDRequest>) => response.hasFailed)
|
map((response: RemoteData<FindByIDRequest>) => response.hasFailed)
|
||||||
|
@@ -5,10 +5,10 @@ import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angu
|
|||||||
import { pushInOut } from '../shared/animations/push';
|
import { pushInOut } from '../shared/animations/push';
|
||||||
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 { hasValue } from '../shared/empty.util';
|
import { hasValue } from '../shared/empty.util';
|
||||||
import { RouteService } from '../core/services/route.service';
|
import { RouteService } from '../core/services/route.service';
|
||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a search page using a configuration as input.
|
* This component renders a search page using a configuration as input.
|
||||||
@@ -61,5 +61,8 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements
|
|||||||
if (hasValue(this.configuration)) {
|
if (hasValue(this.configuration)) {
|
||||||
this.routeService.setParameter('configuration', this.configuration);
|
this.routeService.setParameter('configuration', this.configuration);
|
||||||
}
|
}
|
||||||
|
if (hasValue(this.fixedFilterQuery)) {
|
||||||
|
this.routeService.setParameter('fixedFilterQuery', this.fixedFilterQuery);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,12 +3,17 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CoreModule } from '../core/core.module';
|
import { CoreModule } from '../core/core.module';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { SearchPageRoutingModule } from './search-page-routing.module';
|
import { SearchPageRoutingModule } from './search-page-routing.module';
|
||||||
import { SearchPageComponent } from './search-page.component';
|
import { SearchComponent } from './search.component';
|
||||||
|
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||||
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||||
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
||||||
import { SearchTrackerComponent } from './search-tracker.component';
|
import { SearchTrackerComponent } from './search-tracker.component';
|
||||||
import { StatisticsModule } from '../statistics/statistics.module';
|
import { StatisticsModule } from '../statistics/statistics.module';
|
||||||
import { SearchComponent } from './search.component';
|
import { SearchPageComponent } from './search-page.component';
|
||||||
|
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
|
||||||
|
import { SearchFilterService } from '../core/shared/search/search-filter.service';
|
||||||
|
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||||
|
|
||||||
const components = [
|
const components = [
|
||||||
SearchPageComponent,
|
SearchPageComponent,
|
||||||
@@ -25,8 +30,14 @@ const components = [
|
|||||||
CoreModule.forRoot(),
|
CoreModule.forRoot(),
|
||||||
StatisticsModule.forRoot(),
|
StatisticsModule.forRoot(),
|
||||||
],
|
],
|
||||||
providers: [ConfigurationSearchPageGuard],
|
|
||||||
declarations: components,
|
declarations: components,
|
||||||
|
providers: [
|
||||||
|
SidebarService,
|
||||||
|
SidebarFilterService,
|
||||||
|
SearchFilterService,
|
||||||
|
ConfigurationSearchPageGuard,
|
||||||
|
SearchConfigurationService
|
||||||
|
],
|
||||||
exports: components
|
exports: components
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -9,10 +9,10 @@ import { RouteService } from '../core/services/route.service';
|
|||||||
import { hasValue } from '../shared/empty.util';
|
import { hasValue } from '../shared/empty.util';
|
||||||
import { SearchSuccessResponse } from '../core/cache/response.models';
|
import { SearchSuccessResponse } from '../core/cache/response.models';
|
||||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
||||||
import { SearchQueryResponse } from '../shared/search/search-query-response.model';
|
import { SearchQueryResponse } from '../shared/search/search-query-response.model';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component triggers a page view statistic
|
* This component triggers a page view statistic
|
||||||
@@ -42,7 +42,7 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit {
|
|||||||
super(service, sidebarService, windowService, searchConfigService, routeService, router);
|
super(service, sidebarService, windowService, searchConfigService, routeService, router);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit():void {
|
ngOnInit(): void {
|
||||||
// super.ngOnInit();
|
// super.ngOnInit();
|
||||||
this.getSearchOptions().pipe(
|
this.getSearchOptions().pipe(
|
||||||
switchMap((options) => this.service.searchEntries(options)
|
switchMap((options) => this.service.searchEntries(options)
|
||||||
@@ -62,7 +62,7 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit {
|
|||||||
.subscribe((entry) => {
|
.subscribe((entry) => {
|
||||||
const config: PaginatedSearchOptions = entry.searchOptions;
|
const config: PaginatedSearchOptions = entry.searchOptions;
|
||||||
const searchQueryResponse: SearchQueryResponse = entry.response;
|
const searchQueryResponse: SearchQueryResponse = entry.response;
|
||||||
const filters:Array<{ filter: string, operator: string, value: string, label: string; }> = [];
|
const filters: Array<{ filter: string, operator: string, value: string, label: string; }> = [];
|
||||||
const appliedFilters = searchQueryResponse.appliedFilters || [];
|
const appliedFilters = searchQueryResponse.appliedFilters || [];
|
||||||
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
|
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
|
||||||
const appliedFilter = appliedFilters[i];
|
const appliedFilter = appliedFilters[i];
|
||||||
|
@@ -46,9 +46,9 @@
|
|||||||
[scopes]="(scopeListRD$ | async)"
|
[scopes]="(scopeListRD$ | async)"
|
||||||
[inPlaceSearch]="inPlaceSearch">
|
[inPlaceSearch]="inPlaceSearch">
|
||||||
</ds-search-form>
|
</ds-search-form>
|
||||||
<div class="row mb-3 mb-md-1">
|
<div class="row mb-3 mb-md-1">
|
||||||
<div class="labels col-sm-9 offset-sm-3">
|
<div class="labels col-sm-9 offset-sm-3">
|
||||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@@ -11,9 +11,9 @@ import { hasValue, isNotEmpty } from '../shared/empty.util';
|
|||||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||||
import { RouteService } from '../core/services/route.service';
|
import { RouteService } from '../core/services/route.service';
|
||||||
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 { SearchResult } from '../shared/search/search-result.model';
|
|
||||||
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
||||||
|
import { SearchResult } from '../shared/search/search-result.model';
|
||||||
|
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
import { currentPath } from '../shared/utils/route.utils';
|
import { currentPath } from '../shared/utils/route.utils';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
@@ -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 (
|
||||||
|
@@ -10,10 +10,14 @@ import { META_REDUCERS, MetaReducer, StoreModule } from '@ngrx/store';
|
|||||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||||
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||||
|
|
||||||
import { storeFreeze } from 'ngrx-store-freeze';
|
import { storeFreeze } from 'ngrx-store-freeze';
|
||||||
|
|
||||||
import { ENV_CONFIG, GLOBAL_CONFIG, GlobalConfig } from '../config';
|
import { ENV_CONFIG, GLOBAL_CONFIG, GlobalConfig } from '../config';
|
||||||
|
import { AdminSidebarSectionComponent } from './+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
||||||
|
import { AdminSidebarComponent } from './+admin/admin-sidebar/admin-sidebar.component';
|
||||||
|
import { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
@@ -23,23 +27,20 @@ import { appMetaReducers, debugMetaReducers } from './app.metareducers';
|
|||||||
import { appReducers, AppState } from './app.reducer';
|
import { appReducers, AppState } from './app.reducer';
|
||||||
|
|
||||||
import { CoreModule } from './core/core.module';
|
import { CoreModule } from './core/core.module';
|
||||||
import { FooterComponent } from './footer/footer.component';
|
|
||||||
import { HeaderComponent } from './header/header.component';
|
|
||||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
|
||||||
|
|
||||||
import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer';
|
|
||||||
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
|
||||||
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
|
||||||
import { SharedModule } from './shared/shared.module';
|
|
||||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
|
||||||
import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component';
|
|
||||||
import { AdminSidebarComponent } from './+admin/admin-sidebar/admin-sidebar.component';
|
|
||||||
import { AdminSidebarSectionComponent } from './+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
|
||||||
import { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
|
||||||
import { NavbarModule } from './navbar/navbar.module';
|
|
||||||
import { ClientCookieService } from './core/services/client-cookie.service';
|
import { ClientCookieService } from './core/services/client-cookie.service';
|
||||||
import { JournalEntitiesModule } from './entity-groups/journal-entities/journal-entities.module';
|
import { JournalEntitiesModule } from './entity-groups/journal-entities/journal-entities.module';
|
||||||
import { ResearchEntitiesModule } from './entity-groups/research-entities/research-entities.module';
|
import { ResearchEntitiesModule } from './entity-groups/research-entities/research-entities.module';
|
||||||
|
import { FooterComponent } from './footer/footer.component';
|
||||||
|
import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component';
|
||||||
|
import { HeaderComponent } from './header/header.component';
|
||||||
|
import { NavbarModule } from './navbar/navbar.module';
|
||||||
|
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||||
|
import { SearchNavbarComponent } from './search-navbar/search-navbar.component';
|
||||||
|
|
||||||
|
import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer';
|
||||||
|
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
||||||
|
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
||||||
|
import { SharedModule } from './shared/shared.module';
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
return ENV_CONFIG;
|
return ENV_CONFIG;
|
||||||
@@ -112,7 +113,8 @@ const DECLARATIONS = [
|
|||||||
FooterComponent,
|
FooterComponent,
|
||||||
PageNotFoundComponent,
|
PageNotFoundComponent,
|
||||||
NotificationComponent,
|
NotificationComponent,
|
||||||
NotificationsBoardComponent
|
NotificationsBoardComponent,
|
||||||
|
SearchNavbarComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const EXPORTS = [
|
const EXPORTS = [
|
||||||
@@ -128,7 +130,7 @@ const EXPORTS = [
|
|||||||
...PROVIDERS
|
...PROVIDERS
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
...DECLARATIONS
|
...DECLARATIONS,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
...EXPORTS
|
...EXPORTS
|
||||||
|
@@ -56,7 +56,7 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
return http.url && http.url.endsWith('/authn/logout');
|
return http.url && http.url.endsWith('/authn/logout');
|
||||||
}
|
}
|
||||||
|
|
||||||
private makeAuthStatusObject(authenticated:boolean, accessToken?: string, error?: string): AuthStatus {
|
private makeAuthStatusObject(authenticated: boolean, accessToken?: string, error?: string): AuthStatus {
|
||||||
const authStatus = new AuthStatus();
|
const authStatus = new AuthStatus();
|
||||||
authStatus.id = null;
|
authStatus.id = null;
|
||||||
authStatus.okay = true;
|
authStatus.okay = true;
|
||||||
|
42
src/app/core/cache/models/normalized-external-source-entry.model.ts
vendored
Normal file
42
src/app/core/cache/models/normalized-external-source-entry.model.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||||
|
import { NormalizedObject } from './normalized-object.model';
|
||||||
|
import { ExternalSourceEntry } from '../../shared/external-source-entry.model';
|
||||||
|
import { mapsTo } from '../builders/build-decorators';
|
||||||
|
import { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalized model class for an external source entry
|
||||||
|
*/
|
||||||
|
@mapsTo(ExternalSourceEntry)
|
||||||
|
@inheritSerialization(NormalizedObject)
|
||||||
|
export class NormalizedExternalSourceEntry extends NormalizedObject<ExternalSourceEntry> {
|
||||||
|
/**
|
||||||
|
* Unique identifier
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value to display
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
display: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value to store the entry with
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the external source this entry originates from
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
externalSource: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata of the entry
|
||||||
|
*/
|
||||||
|
@autoserializeAs(MetadataMapSerializer)
|
||||||
|
metadata: MetadataMap;
|
||||||
|
}
|
29
src/app/core/cache/models/normalized-external-source.model.ts
vendored
Normal file
29
src/app/core/cache/models/normalized-external-source.model.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { autoserialize, inheritSerialization } from 'cerialize';
|
||||||
|
import { NormalizedObject } from './normalized-object.model';
|
||||||
|
import { ExternalSource } from '../../shared/external-source.model';
|
||||||
|
import { mapsTo } from '../builders/build-decorators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalized model class for an external source
|
||||||
|
*/
|
||||||
|
@mapsTo(ExternalSource)
|
||||||
|
@inheritSerialization(NormalizedObject)
|
||||||
|
export class NormalizedExternalSource extends NormalizedObject<ExternalSource> {
|
||||||
|
/**
|
||||||
|
* Unique identifier
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of this external source
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the source hierarchical?
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
hierarchical: boolean;
|
||||||
|
}
|
14
src/app/core/cache/response.models.ts
vendored
14
src/app/core/cache/response.models.ts
vendored
@@ -14,6 +14,7 @@ import { DSpaceObject } from '../shared/dspace-object.model';
|
|||||||
import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model';
|
import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model';
|
||||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
import { MetadataField } from '../metadata/metadata-field.model';
|
import { MetadataField } from '../metadata/metadata-field.model';
|
||||||
|
import { ContentSource } from '../shared/content-source.model';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export class RestResponse {
|
export class RestResponse {
|
||||||
@@ -288,4 +289,17 @@ export class FilteredDiscoveryQueryResponse extends RestResponse {
|
|||||||
super(true, statusCode, statusText);
|
super(true, statusCode, statusText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A successful response containing exactly one MetadataSchema
|
||||||
|
*/
|
||||||
|
export class ContentSourceSuccessResponse extends RestResponse {
|
||||||
|
constructor(
|
||||||
|
public contentsource: ContentSource,
|
||||||
|
public statusCode: number,
|
||||||
|
public statusText: string,
|
||||||
|
) {
|
||||||
|
super(true, statusCode, statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
@@ -119,6 +119,7 @@ import { MetadatafieldParsingService } from './data/metadatafield-parsing.servic
|
|||||||
import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model';
|
import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model';
|
||||||
import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
|
import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
|
||||||
import { BrowseDefinition } from './shared/browse-definition.model';
|
import { BrowseDefinition } from './shared/browse-definition.model';
|
||||||
|
import { ContentSourceResponseParsingService } from './data/content-source-response-parsing.service';
|
||||||
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
||||||
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
||||||
import {EntityTypeService} from './data/entity-type.service';
|
import {EntityTypeService} from './data/entity-type.service';
|
||||||
@@ -137,6 +138,10 @@ import { SearchConfigurationService } from './shared/search/search-configuration
|
|||||||
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
|
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
|
||||||
import { RelationshipTypeService } from './data/relationship-type.service';
|
import { RelationshipTypeService } from './data/relationship-type.service';
|
||||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||||
|
import { NormalizedExternalSource } from './cache/models/normalized-external-source.model';
|
||||||
|
import { NormalizedExternalSourceEntry } from './cache/models/normalized-external-source-entry.model';
|
||||||
|
import { ExternalSourceService } from './data/external-source.service';
|
||||||
|
import { LookupRelationService } from './data/lookup-relation.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -242,6 +247,7 @@ const PROVIDERS = [
|
|||||||
ClaimedTaskDataService,
|
ClaimedTaskDataService,
|
||||||
PoolTaskDataService,
|
PoolTaskDataService,
|
||||||
EntityTypeService,
|
EntityTypeService,
|
||||||
|
ContentSourceResponseParsingService,
|
||||||
SearchService,
|
SearchService,
|
||||||
SidebarService,
|
SidebarService,
|
||||||
SearchFilterService,
|
SearchFilterService,
|
||||||
@@ -249,6 +255,8 @@ const PROVIDERS = [
|
|||||||
SearchConfigurationService,
|
SearchConfigurationService,
|
||||||
SelectableListService,
|
SelectableListService,
|
||||||
RelationshipTypeService,
|
RelationshipTypeService,
|
||||||
|
ExternalSourceService,
|
||||||
|
LookupRelationService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
@@ -294,7 +302,9 @@ export const normalizedModels =
|
|||||||
NormalizedPoolTask,
|
NormalizedPoolTask,
|
||||||
NormalizedRelationship,
|
NormalizedRelationship,
|
||||||
NormalizedRelationshipType,
|
NormalizedRelationshipType,
|
||||||
NormalizedItemType
|
NormalizedItemType,
|
||||||
|
NormalizedExternalSource,
|
||||||
|
NormalizedExternalSourceEntry
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -25,8 +25,12 @@ import {
|
|||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { coreSelector } from '../core.selectors';
|
||||||
|
|
||||||
const bitstreamFormatsStateSelector = (state: CoreState) => state.bitstreamFormats;
|
const bitstreamFormatsStateSelector = createSelector(
|
||||||
|
coreSelector,
|
||||||
|
(state: CoreState) => state.bitstreamFormats
|
||||||
|
);
|
||||||
const selectedBitstreamFormatSelector = createSelector(bitstreamFormatsStateSelector,
|
const selectedBitstreamFormatSelector = createSelector(bitstreamFormatsStateSelector,
|
||||||
(bitstreamFormatRegistryState: BitstreamFormatRegistryState) => bitstreamFormatRegistryState.selectedBitstreamFormats);
|
(bitstreamFormatRegistryState: BitstreamFormatRegistryState) => bitstreamFormatRegistryState.selectedBitstreamFormats);
|
||||||
|
|
||||||
|
@@ -17,5 +17,5 @@ export interface ChangeAnalyzer<T extends CacheableObject> {
|
|||||||
* @param {NormalizedObject} object2
|
* @param {NormalizedObject} object2
|
||||||
* The second object to compare
|
* The second object to compare
|
||||||
*/
|
*/
|
||||||
diff(object1: T | NormalizedObject<T>, object2: T | NormalizedObject<T>): Operation[];
|
diff(object1: T | NormalizedObject<T>, object2: T | NormalizedObject<T>): Operation[];
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -17,6 +17,7 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec
|
|||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import * as uuidv4 from 'uuid/v4';
|
import * as uuidv4 from 'uuid/v4';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
|
|
||||||
const endpoint = 'https://rest.api/core';
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
@@ -191,8 +192,7 @@ describe('DataService', () => {
|
|||||||
dso2.self = selfLink;
|
dso2.self = selfLink;
|
||||||
dso2.metadata = [{ key: 'dc.title', value: name2 }];
|
dso2.metadata = [{ key: 'dc.title', value: name2 }];
|
||||||
|
|
||||||
spyOn(service, 'findByHref').and.returnValues(observableOf(dso));
|
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(dso));
|
||||||
spyOn(objectCache, 'getObjectBySelfLink').and.returnValues(observableOf(dso));
|
|
||||||
spyOn(objectCache, 'addPatch');
|
spyOn(objectCache, 'addPatch');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -37,7 +37,7 @@ import { Operation } from 'fast-json-patch';
|
|||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { configureRequest, getResponseFromEntry } from '../shared/operators';
|
import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
|
||||||
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
||||||
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
@@ -248,8 +248,11 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
* @param {DSpaceObject} object The given object
|
* @param {DSpaceObject} object The given object
|
||||||
*/
|
*/
|
||||||
update(object: T): Observable<RemoteData<T>> {
|
update(object: T): Observable<RemoteData<T>> {
|
||||||
const oldVersion$ = this.objectCache.getObjectBySelfLink(object.self);
|
const oldVersion$ = this.findByHref(object.self);
|
||||||
return oldVersion$.pipe(take(1), mergeMap((oldVersion: NormalizedObject<T>) => {
|
return oldVersion$.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
mergeMap((oldVersion: T) => {
|
||||||
const operations = this.comparator.diff(oldVersion, object);
|
const operations = this.comparator.diff(oldVersion, object);
|
||||||
if (isNotEmpty(operations)) {
|
if (isNotEmpty(operations)) {
|
||||||
this.objectCache.addPatch(object.self, operations);
|
this.objectCache.addPatch(object.self, operations);
|
||||||
@@ -257,7 +260,6 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
return this.findByHref(object.self);
|
return this.findByHref(object.self);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
76
src/app/core/data/external-source.service.spec.ts
Normal file
76
src/app/core/data/external-source.service.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { ExternalSourceService } from './external-source.service';
|
||||||
|
import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
|
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { GetRequest } from './request.models';
|
||||||
|
|
||||||
|
describe('ExternalSourceService', () => {
|
||||||
|
let service: ExternalSourceService;
|
||||||
|
|
||||||
|
let requestService;
|
||||||
|
let rdbService;
|
||||||
|
let halService;
|
||||||
|
|
||||||
|
const entries = [
|
||||||
|
Object.assign(new ExternalSourceEntry(), {
|
||||||
|
id: '0001-0001-0001-0001',
|
||||||
|
display: 'John Doe',
|
||||||
|
value: 'John, Doe',
|
||||||
|
metadata: {
|
||||||
|
'dc.identifier.uri': [
|
||||||
|
{
|
||||||
|
value: 'https://orcid.org/0001-0001-0001-0001'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new ExternalSourceEntry(), {
|
||||||
|
id: '0001-0001-0001-0002',
|
||||||
|
display: 'Sampson Megan',
|
||||||
|
value: 'Sampson, Megan',
|
||||||
|
metadata: {
|
||||||
|
'dc.identifier.uri': [
|
||||||
|
{
|
||||||
|
value: 'https://orcid.org/0001-0001-0001-0002'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: 'request-uuid',
|
||||||
|
configure: {}
|
||||||
|
});
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries))
|
||||||
|
});
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
getEndpoint: observableOf('external-sources-REST-endpoint')
|
||||||
|
});
|
||||||
|
service = new ExternalSourceService(requestService, rdbService, undefined, undefined, undefined, halService, undefined, undefined, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getExternalSourceEntries', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
result = service.getExternalSourceEntries('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure a GetRequest', () => {
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the entries', () => {
|
||||||
|
result.subscribe((resultRD) => {
|
||||||
|
expect(resultRD.payload.page).toBe(entries);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
85
src/app/core/data/external-source.service.ts
Normal file
85
src/app/core/data/external-source.service.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DataService } from './data.service';
|
||||||
|
import { ExternalSource } from '../shared/external-source.model';
|
||||||
|
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 { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { FindListOptions, GetRequest } from './request.models';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||||
|
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||||
|
import { hasValue, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
|
import { configureRequest } from '../shared/operators';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
|
||||||
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service handling all external source requests
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ExternalSourceService extends DataService<ExternalSource> {
|
||||||
|
protected linkPath = 'externalsources';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<ExternalSource>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint to browse external sources
|
||||||
|
* @param options
|
||||||
|
* @param linkPath
|
||||||
|
*/
|
||||||
|
getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(linkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint for an external source's entries
|
||||||
|
* @param externalSourceId The id of the external source to fetch entries for
|
||||||
|
*/
|
||||||
|
getEntriesEndpoint(externalSourceId: string): Observable<string> {
|
||||||
|
return this.getBrowseEndpoint().pipe(
|
||||||
|
map((href) => this.getIDHref(href, externalSourceId)),
|
||||||
|
switchMap((href) => this.halService.getEndpoint('entries', href))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the entries for an external source
|
||||||
|
* @param externalSourceId The id of the external source to fetch entries for
|
||||||
|
* @param searchOptions The search options to limit results to
|
||||||
|
*/
|
||||||
|
getExternalSourceEntries(externalSourceId: string, searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<ExternalSourceEntry>>> {
|
||||||
|
const requestUuid = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
const href$ = this.getEntriesEndpoint(externalSourceId).pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint)
|
||||||
|
);
|
||||||
|
|
||||||
|
href$.pipe(
|
||||||
|
map((endpoint: string) => new GetRequest(requestUuid, endpoint)),
|
||||||
|
configureRequest(this.requestService)
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.rdbService.buildList(href$);
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
||||||
|
132
src/app/core/data/lookup-relation.service.spec.ts
Normal file
132
src/app/core/data/lookup-relation.service.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { LookupRelationService } from './lookup-relation.service';
|
||||||
|
import { ExternalSourceService } from './external-source.service';
|
||||||
|
import { SearchService } from '../shared/search/search.service';
|
||||||
|
import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||||
|
import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model';
|
||||||
|
import { SearchResult } from '../../shared/search/search-result.model';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { skip, take } from 'rxjs/operators';
|
||||||
|
import { ExternalSource } from '../shared/external-source.model';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
describe('LookupRelationService', () => {
|
||||||
|
let service: LookupRelationService;
|
||||||
|
let externalSourceService: ExternalSourceService;
|
||||||
|
let searchService: SearchService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
|
||||||
|
const totalExternal = 8;
|
||||||
|
const optionsWithQuery = new PaginatedSearchOptions({ query: 'test-query' });
|
||||||
|
const relationship = Object.assign(new RelationshipOptions(), {
|
||||||
|
filter: 'test-filter',
|
||||||
|
configuration: 'test-configuration'
|
||||||
|
});
|
||||||
|
const localResults = [
|
||||||
|
Object.assign(new SearchResult(), {
|
||||||
|
indexableObject: Object.assign(new Item(), {
|
||||||
|
uuid: 'test-item-uuid',
|
||||||
|
handle: 'test-item-handle'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
];
|
||||||
|
const externalSource = Object.assign(new ExternalSource(), {
|
||||||
|
id: 'orcidV2',
|
||||||
|
name: 'orcidV2',
|
||||||
|
hierarchical: false
|
||||||
|
});
|
||||||
|
const searchServiceEndpoint = 'http://test-rest.com/server/api/core/search';
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
externalSourceService = jasmine.createSpyObj('externalSourceService', {
|
||||||
|
getExternalSourceEntries: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: totalExternal, totalPages: totalExternal, currentPage: 1 }), [{}]))
|
||||||
|
});
|
||||||
|
searchService = jasmine.createSpyObj('searchService', {
|
||||||
|
search: createSuccessfulRemoteDataObject$(createPaginatedList(localResults)),
|
||||||
|
getEndpoint: observableOf(searchServiceEndpoint)
|
||||||
|
});
|
||||||
|
requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring']);
|
||||||
|
service = new LookupRelationService(externalSourceService, searchService, requestService);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLocalResults', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
result = service.getLocalResults(relationship, optionsWithQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the local results', () => {
|
||||||
|
result.subscribe((resultsRD) => {
|
||||||
|
expect(resultsRD.payload.page).toBe(localResults);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the searchConfig to contain a fixedFilter and configuration', () => {
|
||||||
|
expect(service.searchConfig).toEqual(Object.assign(new PaginatedSearchOptions({}), optionsWithQuery,
|
||||||
|
{ fixedFilter: relationship.filter, configuration: relationship.searchConfiguration }
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTotalLocalResults', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
result = service.getTotalLocalResults(relationship, optionsWithQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with 0', () => {
|
||||||
|
result.pipe(take(1)).subscribe((amount) => {
|
||||||
|
expect(amount).toEqual(0)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct total amount', () => {
|
||||||
|
result.pipe(skip(1)).subscribe((amount) => {
|
||||||
|
expect(amount).toEqual(localResults.length)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set searchConfig', () => {
|
||||||
|
expect(service.searchConfig).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTotalExternalResults', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
result = service.getTotalExternalResults(externalSource, optionsWithQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with 0', () => {
|
||||||
|
result.pipe(take(1)).subscribe((amount) => {
|
||||||
|
expect(amount).toEqual(0)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct total amount', () => {
|
||||||
|
result.pipe(skip(1)).subscribe((amount) => {
|
||||||
|
expect(amount).toEqual(totalExternal)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeLocalResultsCache', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.removeLocalResultsCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call requestService\'s removeByHrefSubstring with the search endpoint', () => {
|
||||||
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(searchServiceEndpoint);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
103
src/app/core/data/lookup-relation.service.ts
Normal file
103
src/app/core/data/lookup-relation.service.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { ExternalSourceService } from './external-source.service';
|
||||||
|
import { SearchService } from '../shared/search/search.service';
|
||||||
|
import { concat, map, multicast, startWith, take, takeWhile } from 'rxjs/operators';
|
||||||
|
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||||
|
import { ReplaySubject } from 'rxjs/internal/ReplaySubject';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { SearchResult } from '../../shared/search/search-result.model';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../shared/operators';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ExternalSource } from '../shared/external-source.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
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class LookupRelationService {
|
||||||
|
/**
|
||||||
|
* The search config last used for retrieving local results
|
||||||
|
*/
|
||||||
|
public searchConfig: PaginatedSearchOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pagination options for retrieving exactly one result
|
||||||
|
*/
|
||||||
|
private singleResultOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'single-result-options',
|
||||||
|
pageSize: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(protected externalSourceService: ExternalSourceService,
|
||||||
|
protected searchService: SearchService,
|
||||||
|
protected requestService: RequestService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the available local entries for a relationship
|
||||||
|
* @param relationship Relationship options
|
||||||
|
* @param searchOptions Search options to filter results
|
||||||
|
* @param setSearchConfig Optionally choose if we should store the used search config in a local variable (defaults to true)
|
||||||
|
*/
|
||||||
|
getLocalResults(relationship: RelationshipOptions, searchOptions: PaginatedSearchOptions, setSearchConfig = true): Observable<RemoteData<PaginatedList<SearchResult<Item>>>> {
|
||||||
|
const newConfig = Object.assign(new PaginatedSearchOptions({}), searchOptions,
|
||||||
|
{ fixedFilter: relationship.filter, configuration: relationship.searchConfiguration }
|
||||||
|
);
|
||||||
|
if (setSearchConfig) {
|
||||||
|
this.searchConfig = newConfig;
|
||||||
|
}
|
||||||
|
return this.searchService.search(newConfig).pipe(
|
||||||
|
/* Make sure to only listen to the first x results, until loading is finished */
|
||||||
|
/* TODO: in Rxjs 6.4.0 and up, we can replace this with takeWhile(predicate, true) - see https://stackoverflow.com/a/44644237 */
|
||||||
|
multicast(
|
||||||
|
() => new ReplaySubject(1),
|
||||||
|
(subject) => subject.pipe(
|
||||||
|
takeWhile((rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) => rd.isLoading),
|
||||||
|
concat(subject.pipe(take(1)))
|
||||||
|
)
|
||||||
|
) as any
|
||||||
|
) as Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the total local entries available for the given relationship
|
||||||
|
* @param relationship Relationship options
|
||||||
|
* @param searchOptions Search options to filter results
|
||||||
|
*/
|
||||||
|
getTotalLocalResults(relationship: RelationshipOptions, searchOptions: PaginatedSearchOptions): Observable<number> {
|
||||||
|
return this.getLocalResults(relationship, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions }), false).pipe(
|
||||||
|
getAllSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((results: PaginatedList<SearchResult<Item>>) => results.totalElements),
|
||||||
|
startWith(0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the total external entries available for a given external source
|
||||||
|
* @param externalSource External Source
|
||||||
|
* @param searchOptions Search options to filter results
|
||||||
|
*/
|
||||||
|
getTotalExternalResults(externalSource: ExternalSource, searchOptions: PaginatedSearchOptions): Observable<number> {
|
||||||
|
return this.externalSourceService.getExternalSourceEntries(externalSource.id, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions })).pipe(
|
||||||
|
getAllSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((results: PaginatedList<ExternalSourceEntry>) => results.totalElements),
|
||||||
|
startWith(0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove cached requests from local results
|
||||||
|
*/
|
||||||
|
removeLocalResultsCache() {
|
||||||
|
this.searchService.getEndpoint().subscribe((href) => this.requestService.removeByHrefSubstring(href));
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
import {Injectable} from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import {createSelector, MemoizedSelector, select, Store} from '@ngrx/store';
|
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
import {CoreState} from '../../core.reducers';
|
import { CoreState } from '../../core.reducers';
|
||||||
import {coreSelector} from '../../core.selectors';
|
import { coreSelector } from '../../core.selectors';
|
||||||
import {
|
import {
|
||||||
FieldState,
|
FieldState,
|
||||||
FieldUpdates,
|
FieldUpdates,
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
ObjectUpdatesState,
|
ObjectUpdatesState,
|
||||||
VirtualMetadataSource
|
VirtualMetadataSource
|
||||||
} from './object-updates.reducer';
|
} from './object-updates.reducer';
|
||||||
import {Observable} from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
AddFieldUpdateAction,
|
AddFieldUpdateAction,
|
||||||
DiscardObjectUpdatesAction,
|
DiscardObjectUpdatesAction,
|
||||||
@@ -23,9 +23,9 @@ import {
|
|||||||
SetEditableFieldUpdateAction,
|
SetEditableFieldUpdateAction,
|
||||||
SetValidFieldUpdateAction
|
SetValidFieldUpdateAction
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import {distinctUntilChanged, filter, map, switchMap} 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';
|
||||||
|
|
||||||
function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
|
function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
|
||||||
return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
|
return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
|
||||||
@@ -100,9 +100,11 @@ export class ObjectUpdatesService {
|
|||||||
return objectUpdates.pipe(
|
return objectUpdates.pipe(
|
||||||
switchMap((objectEntry) => {
|
switchMap((objectEntry) => {
|
||||||
const fieldUpdates: FieldUpdates = {};
|
const fieldUpdates: FieldUpdates = {};
|
||||||
Object.keys(objectEntry.fieldStates).forEach((uuid) => {
|
if (hasValue(objectEntry)) {
|
||||||
fieldUpdates[uuid] = objectEntry.fieldUpdates[uuid];
|
Object.keys(objectEntry.fieldStates).forEach((uuid) => {
|
||||||
});
|
fieldUpdates[uuid] = objectEntry.fieldUpdates[uuid];
|
||||||
|
});
|
||||||
|
}
|
||||||
return this.getFieldUpdatesExclusive(url, initialFields).pipe(
|
return this.getFieldUpdatesExclusive(url, initialFields).pipe(
|
||||||
map((fieldUpdatesExclusive) => {
|
map((fieldUpdatesExclusive) => {
|
||||||
Object.keys(fieldUpdatesExclusive).forEach((uuid) => {
|
Object.keys(fieldUpdatesExclusive).forEach((uuid) => {
|
||||||
|
@@ -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'
|
||||||
});
|
});
|
||||||
@@ -120,7 +122,7 @@ describe('RelationshipService', () => {
|
|||||||
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.self);
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self);
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
|
||||||
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 { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
@@ -16,7 +17,7 @@ import { RestResponse } from '../cache/response.models';
|
|||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { Relationship } from '../shared/item-relationships/relationship.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 } from './remote-data';
|
import { RemoteData, RemoteDataState } from './remote-data';
|
||||||
import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs';
|
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';
|
||||||
@@ -147,9 +148,9 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
this.findById(relationshipId).pipe(
|
this.findById(relationshipId).pipe(
|
||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
switchMap((relationship: Relationship) => combineLatest(
|
switchMap((rel: Relationship) => combineLatest(
|
||||||
relationship.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
|
rel.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
|
||||||
relationship.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload())
|
rel.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload())
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
take(1)
|
take(1)
|
||||||
@@ -165,10 +166,10 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
*/
|
*/
|
||||||
private removeRelationshipItemsFromCache(item) {
|
private removeRelationshipItemsFromCache(item) {
|
||||||
this.objectCache.remove(item.self);
|
this.objectCache.remove(item.self);
|
||||||
this.requestService.removeByHrefSubstring(item.self);
|
this.requestService.removeByHrefSubstring(item.uuid);
|
||||||
combineLatest(
|
combineLatest(
|
||||||
this.objectCache.hasBySelfLinkObservable(item.self),
|
this.objectCache.hasBySelfLinkObservable(item.self),
|
||||||
this.requestService.hasByHrefObservable(item.self)
|
this.requestService.hasByHrefObservable(item.uuid)
|
||||||
).pipe(
|
).pipe(
|
||||||
filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC),
|
filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC),
|
||||||
take(1),
|
take(1),
|
||||||
@@ -374,7 +375,7 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
* @param nameVariant The name variant to set for the matching relationship
|
* @param nameVariant The name variant to set for the matching relationship
|
||||||
*/
|
*/
|
||||||
public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable<RemoteData<Relationship>> {
|
public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable<RemoteData<Relationship>> {
|
||||||
return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel)
|
const update$: Observable<RemoteData<Relationship>> = this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel)
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((relation: Relationship) =>
|
switchMap((relation: Relationship) =>
|
||||||
relation.relationshipType.pipe(
|
relation.relationshipType.pipe(
|
||||||
@@ -395,14 +396,44 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
}
|
}
|
||||||
return this.update(updatedRelationship);
|
return this.update(updatedRelationship);
|
||||||
}),
|
}),
|
||||||
// skipWhile((relationshipRD: RemoteData<Relationship>) => !relationshipRD.isSuccessful)
|
);
|
||||||
tap((relationshipRD: RemoteData<Relationship>) => {
|
|
||||||
if (relationshipRD.hasSucceeded) {
|
update$.pipe(
|
||||||
this.removeRelationshipItemsFromCache(item1);
|
filter((relationshipRD: RemoteData<Relationship>) => relationshipRD.state === RemoteDataState.RequestPending),
|
||||||
this.removeRelationshipItemsFromCache(item2);
|
take(1),
|
||||||
}
|
).subscribe(() => {
|
||||||
}),
|
this.removeRelationshipItemsFromCache(item1);
|
||||||
)
|
this.removeRelationshipItemsFromCache(item2);
|
||||||
|
});
|
||||||
|
|
||||||
|
return update$
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to update the the right or left place of a relationship
|
||||||
|
* The useLeftItem field in the reorderable relationship determines which place should be updated
|
||||||
|
* @param reoRel
|
||||||
|
*/
|
||||||
|
public updatePlace(reoRel: ReorderableRelationship): Observable<RemoteData<Relationship>> {
|
||||||
|
let updatedRelationship;
|
||||||
|
if (reoRel.useLeftItem) {
|
||||||
|
updatedRelationship = Object.assign(new Relationship(), reoRel.relationship, { rightPlace: reoRel.newIndex });
|
||||||
|
} else {
|
||||||
|
updatedRelationship = Object.assign(new Relationship(), reoRel.relationship, { leftPlace: reoRel.newIndex });
|
||||||
|
}
|
||||||
|
|
||||||
|
const update$ = this.update(updatedRelationship);
|
||||||
|
|
||||||
|
update$.pipe(
|
||||||
|
filter((relationshipRD: RemoteData<Relationship>) => relationshipRD.state === RemoteDataState.ResponsePending),
|
||||||
|
take(1),
|
||||||
|
).subscribe((relationshipRD: RemoteData<Relationship>) => {
|
||||||
|
if (relationshipRD.state === RemoteDataState.ResponsePending) {
|
||||||
|
this.removeRelationshipItemsFromCacheByRelationship(reoRel.relationship.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return update$;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -18,6 +18,7 @@ import { MetadataschemaParsingService } from './metadataschema-parsing.service';
|
|||||||
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
|
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
||||||
|
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
||||||
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@@ -378,6 +379,26 @@ export class CreateRequest extends PostRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ContentSourceRequest extends GetRequest {
|
||||||
|
constructor(uuid: string, href: string) {
|
||||||
|
super(uuid, href);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return ContentSourceResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateContentSourceRequest extends PutRequest {
|
||||||
|
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||||
|
super(uuid, href, body, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return ContentSourceResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request to delete an object based on its identifier
|
* Request to delete an object based on its identifier
|
||||||
*/
|
*/
|
||||||
|
@@ -19,12 +19,12 @@ import { PaginatedList } from './paginated-list';
|
|||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
|
|
||||||
describe('SiteDataService', () => {
|
describe('SiteDataService', () => {
|
||||||
let scheduler:TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service:SiteDataService;
|
let service: SiteDataService;
|
||||||
let halService:HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
let requestService:RequestService;
|
let requestService: RequestService;
|
||||||
let rdbService:RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
let objectCache:ObjectCacheService;
|
let objectCache: ObjectCacheService;
|
||||||
|
|
||||||
const testObject = Object.assign(new Site(), {
|
const testObject = Object.assign(new Site(), {
|
||||||
uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746',
|
uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746',
|
||||||
@@ -33,7 +33,7 @@ describe('SiteDataService', () => {
|
|||||||
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
|
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
|
||||||
const options = Object.assign(new FindListOptions(), {});
|
const options = Object.assign(new FindListOptions(), {});
|
||||||
|
|
||||||
const getRequestEntry$ = (successful:boolean, statusCode:number, statusText:string) => {
|
const getRequestEntry$ = (successful: boolean, statusCode: number, statusText: string) => {
|
||||||
return observableOf({
|
return observableOf({
|
||||||
response: new RestResponse(successful, statusCode, statusText)
|
response: new RestResponse(successful, statusCode, statusText)
|
||||||
} as RequestEntry);
|
} as RequestEntry);
|
||||||
|
@@ -22,47 +22,41 @@ import { getSucceededRemoteData } from '../shared/operators';
|
|||||||
* Service responsible for handling requests related to the Site object
|
* Service responsible for handling requests related to the Site object
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SiteDataService extends DataService<Site> {
|
export class SiteDataService extends DataService<Site> {
|
||||||
|
|
||||||
protected linkPath = 'sites';
|
protected linkPath = 'sites';
|
||||||
protected forceBypassCache = false;
|
protected forceBypassCache = false;
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService:RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService:RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected dataBuildService:NormalizedObjectBuildService,
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
protected store:Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
protected objectCache:ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService:HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService:NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http:HttpClient,
|
protected http: HttpClient,
|
||||||
protected comparator:DSOChangeAnalyzer<Site>,
|
protected comparator: DSOChangeAnalyzer<Site>,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the endpoint for browsing the site object
|
* Get the endpoint for browsing the site object
|
||||||
* @param {FindListOptions} options
|
* @param {FindListOptions} options
|
||||||
* @param {Observable<string>} linkPath
|
* @param {Observable<string>} linkPath
|
||||||
*/
|
*/
|
||||||
getBrowseEndpoint(options:FindListOptions, linkPath?:string):Observable<string> {
|
getBrowseEndpoint(options: FindListOptions, linkPath?: string): Observable<string> {
|
||||||
return this.halService.getEndpoint(this.linkPath);
|
return this.halService.getEndpoint(this.linkPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the Site Object
|
* Retrieve the Site Object
|
||||||
*/
|
*/
|
||||||
find():Observable<Site> {
|
find(): Observable<Site> {
|
||||||
return this.findAll().pipe(
|
return this.findAll().pipe(
|
||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
map((remoteData:RemoteData<PaginatedList<Site>>) => remoteData.payload),
|
map((remoteData: RemoteData<PaginatedList<Site>>) => remoteData.payload),
|
||||||
map((list:PaginatedList<Site>) => list.page[0])
|
map((list: PaginatedList<Site>) => list.page[0])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,8 @@
|
|||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { createSelector } from '@ngrx/store';
|
||||||
|
import { coreSelector } from '../core.selectors';
|
||||||
|
|
||||||
export const historySelector = (state: CoreState) => state.history;
|
export const historySelector = createSelector(
|
||||||
|
coreSelector,
|
||||||
|
(state: CoreState) => state.history
|
||||||
|
);
|
||||||
|
@@ -142,7 +142,11 @@ describe('RouteService', () => {
|
|||||||
|
|
||||||
describe('getHistory', () => {
|
describe('getHistory', () => {
|
||||||
it('should dispatch AddUrlToHistoryAction on NavigationEnd event', () => {
|
it('should dispatch AddUrlToHistoryAction on NavigationEnd event', () => {
|
||||||
serviceAsAny.store = observableOf({ history: ['url', 'newurl'] });
|
serviceAsAny.store = observableOf({
|
||||||
|
core: {
|
||||||
|
history: ['url', 'newurl']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
service.getHistory().subscribe((history) => {
|
service.getHistory().subscribe((history) => {
|
||||||
expect(history).toEqual(['url', 'newurl']);
|
expect(history).toEqual(['url', 'newurl']);
|
||||||
|
@@ -176,10 +176,20 @@ export class RouteService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a parameter to the current route
|
||||||
|
* @param key The parameter name
|
||||||
|
* @param value The parameter value
|
||||||
|
*/
|
||||||
public addParameter(key, value) {
|
public addParameter(key, value) {
|
||||||
this.store.dispatch(new AddParameterAction(key, value));
|
this.store.dispatch(new AddParameterAction(key, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a parameter in the current route (overriding the previous value)
|
||||||
|
* @param key The parameter name
|
||||||
|
* @param value The parameter value
|
||||||
|
*/
|
||||||
public setParameter(key, value) {
|
public setParameter(key, value) {
|
||||||
this.store.dispatch(new SetParameterAction(key, value));
|
this.store.dispatch(new SetParameterAction(key, value));
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
}
|
48
src/app/core/shared/external-source-entry.model.ts
Normal file
48
src/app/core/shared/external-source-entry.model.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { MetadataMap } from './metadata.models';
|
||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||||
|
import { GenericConstructor } from './generic-constructor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model class for a single entry from an external source
|
||||||
|
*/
|
||||||
|
export class ExternalSourceEntry extends ListableObject {
|
||||||
|
static type = new ResourceType('externalSourceEntry');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique identifier
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value to display
|
||||||
|
*/
|
||||||
|
display: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value to store the entry with
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the external source this entry originates from
|
||||||
|
*/
|
||||||
|
externalSource: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata of the entry
|
||||||
|
*/
|
||||||
|
metadata: MetadataMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link to the rest endpoint where this External Source Entry can be found
|
||||||
|
*/
|
||||||
|
self: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that returns as which type of object this object should be rendered
|
||||||
|
*/
|
||||||
|
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
|
||||||
|
return [this.constructor as GenericConstructor<ListableObject>];
|
||||||
|
}
|
||||||
|
}
|
29
src/app/core/shared/external-source.model.ts
Normal file
29
src/app/core/shared/external-source.model.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model class for an external source
|
||||||
|
*/
|
||||||
|
export class ExternalSource extends CacheableObject {
|
||||||
|
static type = new ResourceType('externalsource');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique identifier
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of this external source
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the source hierarchical?
|
||||||
|
*/
|
||||||
|
hierarchical: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link to the rest endpoint where this External Source can be found
|
||||||
|
*/
|
||||||
|
self: string;
|
||||||
|
}
|
19
src/app/core/shared/metadata-config.model.ts
Normal file
19
src/app/core/shared/metadata-config.model.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* A model class that holds information about a certain metadata configuration
|
||||||
|
*/
|
||||||
|
export class MetadataConfig {
|
||||||
|
/**
|
||||||
|
* A unique indentifier
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label used for display
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The namespace of the metadata
|
||||||
|
*/
|
||||||
|
nameSpace: string;
|
||||||
|
}
|
@@ -89,9 +89,9 @@ export class SearchService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getEndpoint(searchOptions?:PaginatedSearchOptions):Observable<string> {
|
getEndpoint(searchOptions?: PaginatedSearchOptions): Observable<string> {
|
||||||
return this.halService.getEndpoint(this.searchLinkPath).pipe(
|
return this.halService.getEndpoint(this.searchLinkPath).pipe(
|
||||||
map((url:string) => {
|
map((url: string) => {
|
||||||
if (hasValue(searchOptions)) {
|
if (hasValue(searchOptions)) {
|
||||||
return (searchOptions as PaginatedSearchOptions).toRestUrl(url);
|
return (searchOptions as PaginatedSearchOptions).toRestUrl(url);
|
||||||
} else {
|
} else {
|
||||||
@@ -117,16 +117,15 @@ export class SearchService implements OnDestroy {
|
|||||||
* @param responseMsToLive The amount of milliseconds for the response to live in cache
|
* @param responseMsToLive The amount of milliseconds for the response to live in cache
|
||||||
* @returns {Observable<RequestEntry>} Emits an observable with the request entries
|
* @returns {Observable<RequestEntry>} Emits an observable with the request entries
|
||||||
*/
|
*/
|
||||||
searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?:number)
|
searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number): Observable<{searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry}> {
|
||||||
:Observable<{searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry}> {
|
|
||||||
|
|
||||||
const hrefObs = this.getEndpoint(searchOptions);
|
const hrefObs = this.getEndpoint(searchOptions);
|
||||||
|
|
||||||
const requestObs = hrefObs.pipe(
|
const requestObs = hrefObs.pipe(
|
||||||
map((url:string) => {
|
map((url: string) => {
|
||||||
const request = new this.request(this.requestService.generateRequestId(), url);
|
const request = new this.request(this.requestService.generateRequestId(), url);
|
||||||
|
|
||||||
const getResponseParserFn:() => GenericConstructor<ResponseParsingService> = () => {
|
const getResponseParserFn: () => GenericConstructor<ResponseParsingService> = () => {
|
||||||
return this.parser;
|
return this.parser;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,8 +138,8 @@ export class SearchService implements OnDestroy {
|
|||||||
configureRequest(this.requestService),
|
configureRequest(this.requestService),
|
||||||
);
|
);
|
||||||
return requestObs.pipe(
|
return requestObs.pipe(
|
||||||
switchMap((request:RestRequest) => this.requestService.getByHref(request.href)),
|
switchMap((request: RestRequest) => this.requestService.getByHref(request.href)),
|
||||||
map(((requestEntry:RequestEntry) => ({
|
map(((requestEntry: RequestEntry) => ({
|
||||||
searchOptions: searchOptions,
|
searchOptions: searchOptions,
|
||||||
requestEntry: requestEntry
|
requestEntry: requestEntry
|
||||||
})))
|
})))
|
||||||
@@ -152,16 +151,15 @@ export class SearchService implements OnDestroy {
|
|||||||
* @param searchEntries: The request entries from the search method
|
* @param searchEntries: The request entries from the search method
|
||||||
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
||||||
*/
|
*/
|
||||||
getPaginatedResults(searchEntries:Observable<{ searchOptions:PaginatedSearchOptions, requestEntry:RequestEntry }>)
|
getPaginatedResults(searchEntries: Observable<{ searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry }>): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||||
:Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
const requestEntryObs: Observable<RequestEntry> = searchEntries.pipe(
|
||||||
const requestEntryObs:Observable<RequestEntry> = searchEntries.pipe(
|
|
||||||
map((entry) => entry.requestEntry),
|
map((entry) => entry.requestEntry),
|
||||||
);
|
);
|
||||||
|
|
||||||
// get search results from response cache
|
// get search results from response cache
|
||||||
const sqrObs:Observable<SearchQueryResponse> = requestEntryObs.pipe(
|
const sqrObs: Observable<SearchQueryResponse> = requestEntryObs.pipe(
|
||||||
filterSuccessfulResponses(),
|
filterSuccessfulResponses(),
|
||||||
map((response:SearchSuccessResponse) => response.results),
|
map((response: SearchSuccessResponse) => response.results),
|
||||||
);
|
);
|
||||||
|
|
||||||
// turn dspace href from search results to effective list of DSpaceObjects
|
// turn dspace href from search results to effective list of DSpaceObjects
|
||||||
|
@@ -20,6 +20,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../cache/response.models';
|
import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../cache/response.models';
|
||||||
import { getResponseFromEntry } from '../shared/operators';
|
import { getResponseFromEntry } from '../shared/operators';
|
||||||
|
import {URLCombiner} from '../url-combiner/url-combiner';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The service handling all submission REST requests
|
* The service handling all submission REST requests
|
||||||
@@ -65,9 +66,15 @@ export class SubmissionRestService {
|
|||||||
* The base endpoint for the type of object
|
* The base endpoint for the type of object
|
||||||
* @param resourceID
|
* @param resourceID
|
||||||
* The identifier for the object
|
* The identifier for the object
|
||||||
|
* @param collectionId
|
||||||
|
* The owning collection for the object
|
||||||
*/
|
*/
|
||||||
protected getEndpointByIDHref(endpoint, resourceID): string {
|
protected getEndpointByIDHref(endpoint, resourceID, collectionId?: string): string {
|
||||||
return isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`;
|
let url = isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`;
|
||||||
|
if (collectionId) {
|
||||||
|
url = new URLCombiner(url, `?owningCollection=${collectionId}`).toString();
|
||||||
|
}
|
||||||
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,12 +137,14 @@ export class SubmissionRestService {
|
|||||||
* The [HttpOptions] object
|
* The [HttpOptions] object
|
||||||
* @return Observable<SubmitDataResponseDefinitionObject>
|
* @return Observable<SubmitDataResponseDefinitionObject>
|
||||||
* server response
|
* server response
|
||||||
|
* @param collectionId
|
||||||
|
* The owning collection id
|
||||||
*/
|
*/
|
||||||
public postToEndpoint(linkName: string, body: any, scopeId?: string, options?: HttpOptions): Observable<SubmitDataResponseDefinitionObject> {
|
public postToEndpoint(linkName: string, body: any, scopeId?: string, options?: HttpOptions, collectionId?: string): Observable<SubmitDataResponseDefinitionObject> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
return this.halService.getEndpoint(linkName).pipe(
|
return this.halService.getEndpoint(linkName).pipe(
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)),
|
map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId, collectionId)),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
map((endpointURL: string) => new SubmissionPostRequest(requestId, endpointURL, body, options)),
|
map((endpointURL: string) => new SubmissionPostRequest(requestId, endpointURL, body, options)),
|
||||||
tap((request: PostRequest) => this.requestService.configure(request)),
|
tap((request: PostRequest) => this.requestService.configure(request)),
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { metadataRepresentationComponent } from '../../../../shared/metadata-representation/metadata-representation.decorator';
|
import { metadataRepresentationComponent } from '../../../../shared/metadata-representation/metadata-representation.decorator';
|
||||||
import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';
|
import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';
|
||||||
|
@@ -25,6 +25,7 @@ import { PersonInputSuggestionsComponent } from './submission/item-list-elements
|
|||||||
import { NameVariantModalComponent } from './submission/name-variant-modal/name-variant-modal.component';
|
import { NameVariantModalComponent } from './submission/name-variant-modal/name-variant-modal.component';
|
||||||
import { OrgUnitInputSuggestionsComponent } from './submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component';
|
import { OrgUnitInputSuggestionsComponent } from './submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component';
|
||||||
import { OrgUnitSearchResultListSubmissionElementComponent } from './submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component';
|
import { OrgUnitSearchResultListSubmissionElementComponent } from './submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component';
|
||||||
|
import { ExternalSourceEntryListSubmissionElementComponent } from './submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
OrgUnitComponent,
|
OrgUnitComponent,
|
||||||
@@ -48,7 +49,8 @@ const ENTRY_COMPONENTS = [
|
|||||||
PersonInputSuggestionsComponent,
|
PersonInputSuggestionsComponent,
|
||||||
NameVariantModalComponent,
|
NameVariantModalComponent,
|
||||||
OrgUnitSearchResultListSubmissionElementComponent,
|
OrgUnitSearchResultListSubmissionElementComponent,
|
||||||
OrgUnitInputSuggestionsComponent
|
OrgUnitInputSuggestionsComponent,
|
||||||
|
ExternalSourceEntryListSubmissionElementComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="d-inline-block">
|
||||||
|
<div>{{object.display}}</div>
|
||||||
|
<div *ngIf="uri"><a target="_blank" [href]="uri.value">{{uri.value}}</a></div>
|
||||||
|
</div>
|
@@ -0,0 +1,47 @@
|
|||||||
|
import { ExternalSourceEntryListSubmissionElementComponent } from './external-source-entry-list-submission-element.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ExternalSourceEntry } from '../../../../../core/shared/external-source-entry.model';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
|
||||||
|
describe('ExternalSourceEntryListSubmissionElementComponent', () => {
|
||||||
|
let component: ExternalSourceEntryListSubmissionElementComponent;
|
||||||
|
let fixture: ComponentFixture<ExternalSourceEntryListSubmissionElementComponent>;
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ExternalSourceEntryListSubmissionElementComponent],
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ExternalSourceEntryListSubmissionElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = entry;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the entry\'s display value', () => {
|
||||||
|
expect(fixture.nativeElement.textContent).toContain(entry.display);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the entry\'s uri', () => {
|
||||||
|
expect(fixture.nativeElement.textContent).toContain(uri);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,28 @@
|
|||||||
|
import { AbstractListableElementComponent } from '../../../../../shared/object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||||
|
import { ExternalSourceEntry } from '../../../../../core/shared/external-source-entry.model';
|
||||||
|
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
|
import { Metadata } from '../../../../../core/shared/metadata.utils';
|
||||||
|
import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||||
|
|
||||||
|
@listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.SubmissionModal)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-external-source-entry-list-submission-element',
|
||||||
|
styleUrls: ['./external-source-entry-list-submission-element.component.scss'],
|
||||||
|
templateUrl: './external-source-entry-list-submission-element.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a list element of an external source entry
|
||||||
|
*/
|
||||||
|
export class ExternalSourceEntryListSubmissionElementComponent extends AbstractListableElementComponent<ExternalSourceEntry> implements OnInit {
|
||||||
|
/**
|
||||||
|
* The metadata value for the object's uri
|
||||||
|
*/
|
||||||
|
uri: MetadataValue;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.uri = Metadata.first(this.object.metadata, 'dc.identifier.uri');
|
||||||
|
}
|
||||||
|
}
|
@@ -10,7 +10,13 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
templateUrl: './name-variant-modal.component.html',
|
templateUrl: './name-variant-modal.component.html',
|
||||||
styleUrls: ['./name-variant-modal.component.scss']
|
styleUrls: ['./name-variant-modal.component.scss']
|
||||||
})
|
})
|
||||||
|
/**
|
||||||
|
* The component for the modal to add a name variant to an item
|
||||||
|
*/
|
||||||
export class NameVariantModalComponent {
|
export class NameVariantModalComponent {
|
||||||
|
/**
|
||||||
|
* The name variant
|
||||||
|
*/
|
||||||
@Input() value: string;
|
@Input() value: string;
|
||||||
|
|
||||||
constructor(public modal: NgbActiveModal) {
|
constructor(public modal: NgbActiveModal) {
|
||||||
|
@@ -1,20 +1,20 @@
|
|||||||
<header>
|
<header>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand my-2" routerLink="/home">
|
<a class="navbar-brand my-2" routerLink="/home">
|
||||||
<img src="assets/images/dspace-logo.svg"/>
|
<img src="assets/images/dspace-logo.svg"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<nav class="navbar navbar-light navbar-expand-md float-right px-0">
|
<nav class="navbar navbar-light navbar-expand-md float-right px-0">
|
||||||
<a routerLink="/search" class="px-1"><i class="fas fa-search fa-lg fa-fw" [title]="'nav.search' | translate"></i></a>
|
<ds-search-navbar></ds-search-navbar>
|
||||||
<ds-lang-switch></ds-lang-switch>
|
<ds-lang-switch></ds-lang-switch>
|
||||||
<ds-auth-nav-menu></ds-auth-nav-menu>
|
<ds-auth-nav-menu></ds-auth-nav-menu>
|
||||||
<div class="pl-2">
|
<div class="pl-2">
|
||||||
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
|
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
|
||||||
aria-controls="collapsingNav"
|
aria-controls="collapsingNav"
|
||||||
aria-expanded="false" aria-label="Toggle navigation">
|
aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
|
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
12
src/app/search-navbar/search-navbar.component.html
Normal file
12
src/app/search-navbar/search-navbar.component.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<div id="search-navbar-container" [title]="'nav.search' | translate" (dsClickOutside)="collapse()">
|
||||||
|
<div class="d-inline-block position-relative">
|
||||||
|
<form [formGroup]="searchForm" (ngSubmit)="onSubmit(searchForm.value)" autocomplete="on">
|
||||||
|
<input #searchInput [@toggleAnimation]="isExpanded" id="query" name="query"
|
||||||
|
formControlName="query" type="text" placeholder="{{searchExpanded ? ('nav.search' | translate) : ''}}"
|
||||||
|
class="d-inline-block bg-transparent position-absolute form-control dropdown-menu-right p-1">
|
||||||
|
<a class="sticky-top submit-icon" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()">
|
||||||
|
<em class="fas fa-search fa-lg fa-fw"></em>
|
||||||
|
</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
25
src/app/search-navbar/search-navbar.component.scss
Normal file
25
src/app/search-navbar/search-navbar.component.scss
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
input[type="text"] {
|
||||||
|
margin-top: -0.5 * $font-size-base;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: rgba(255, 255, 255, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.submit-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: map-get($grid-breakpoints, sm)) {
|
||||||
|
#query:focus {
|
||||||
|
max-width: 250px !important;
|
||||||
|
width: 40vw !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
121
src/app/search-navbar/search-navbar.component.spec.ts
Normal file
121
src/app/search-navbar/search-navbar.component.spec.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
|
import { MockTranslateLoader } from '../shared/mocks/mock-translate-loader';
|
||||||
|
|
||||||
|
import { SearchNavbarComponent } from './search-navbar.component';
|
||||||
|
|
||||||
|
describe('SearchNavbarComponent', () => {
|
||||||
|
let component: SearchNavbarComponent;
|
||||||
|
let fixture: ComponentFixture<SearchNavbarComponent>;
|
||||||
|
let mockSearchService: any;
|
||||||
|
let router: Router;
|
||||||
|
let routerStub;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
mockSearchService = {
|
||||||
|
getSearchLink() {
|
||||||
|
return '/search';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
routerStub = {
|
||||||
|
navigate: (commands) => commands
|
||||||
|
};
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [SearchNavbarComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: SearchService, useValue: mockSearchService },
|
||||||
|
{ provide: Router, useValue: routerStub }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SearchNavbarComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
router = (component as any).router;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when you click on search icon', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
spyOn(component, 'expand').and.callThrough();
|
||||||
|
spyOn(component, 'onSubmit').and.callThrough();
|
||||||
|
spyOn(router, 'navigate').and.callThrough();
|
||||||
|
const searchIcon = fixture.debugElement.query(By.css('#search-navbar-container form .submit-icon'));
|
||||||
|
searchIcon.triggerEventHandler('click', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('input expands', () => {
|
||||||
|
expect(component.expand).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('empty query', () => {
|
||||||
|
describe('press submit button', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
const searchIcon = fixture.debugElement.query(By.css('#search-navbar-container form .submit-icon'));
|
||||||
|
searchIcon.triggerEventHandler('click', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('to search page with empty query', () => {
|
||||||
|
expect(component.onSubmit).toHaveBeenCalledWith({ query: '' });
|
||||||
|
expect(router.navigate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fill in some query', () => {
|
||||||
|
let searchInput;
|
||||||
|
beforeEach(async () => {
|
||||||
|
await fixture.whenStable();
|
||||||
|
fixture.detectChanges();
|
||||||
|
searchInput = fixture.debugElement.query(By.css('#search-navbar-container form input'));
|
||||||
|
searchInput.nativeElement.value = 'test';
|
||||||
|
searchInput.nativeElement.dispatchEvent(new Event('input'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
describe('press submit button', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
const searchIcon = fixture.debugElement.query(By.css('#search-navbar-container form .submit-icon'));
|
||||||
|
searchIcon.triggerEventHandler('click', null);
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('to search page with query', async () => {
|
||||||
|
expect(component.onSubmit).toHaveBeenCalledWith({ query: 'test' });
|
||||||
|
expect(router.navigate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
71
src/app/search-navbar/search-navbar.component.ts
Normal file
71
src/app/search-navbar/search-navbar.component.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { Component, ElementRef, ViewChild } from '@angular/core';
|
||||||
|
import { FormBuilder } from '@angular/forms';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
|
import { expandSearchInput } from '../shared/animations/slide';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The search box in the header that expands on focus and collapses on focus out
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-search-navbar',
|
||||||
|
templateUrl: './search-navbar.component.html',
|
||||||
|
styleUrls: ['./search-navbar.component.scss'],
|
||||||
|
animations: [expandSearchInput]
|
||||||
|
})
|
||||||
|
export class SearchNavbarComponent {
|
||||||
|
|
||||||
|
// The search form
|
||||||
|
searchForm;
|
||||||
|
// Whether or not the search bar is expanded, boolean for html ngIf, string fo AngularAnimation state change
|
||||||
|
searchExpanded = false;
|
||||||
|
isExpanded = 'collapsed';
|
||||||
|
|
||||||
|
// Search input field
|
||||||
|
@ViewChild('searchInput') searchField: ElementRef;
|
||||||
|
|
||||||
|
constructor(private formBuilder: FormBuilder, private router: Router, private searchService: SearchService) {
|
||||||
|
this.searchForm = this.formBuilder.group(({
|
||||||
|
query: '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands search bar by angular animation, see expandSearchInput
|
||||||
|
*/
|
||||||
|
expand() {
|
||||||
|
this.searchExpanded = true;
|
||||||
|
this.isExpanded = 'expanded';
|
||||||
|
this.editSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapses & blurs search bar by angular animation, see expandSearchInput
|
||||||
|
*/
|
||||||
|
collapse() {
|
||||||
|
this.searchField.nativeElement.blur();
|
||||||
|
this.searchExpanded = false;
|
||||||
|
this.isExpanded = 'collapsed';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focuses on input search bar so search can be edited
|
||||||
|
*/
|
||||||
|
editSearch(): void {
|
||||||
|
this.searchField.nativeElement.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the search (on enter or on search icon click)
|
||||||
|
* @param data Data for the searchForm, containing the search query
|
||||||
|
*/
|
||||||
|
onSubmit(data: any) {
|
||||||
|
this.collapse();
|
||||||
|
const linkToNavigateTo = this.searchService.getSearchLink().split('/');
|
||||||
|
this.searchForm.reset();
|
||||||
|
this.router.navigate(linkToNavigateTo, {
|
||||||
|
queryParams: Object.assign({}, { page: 1 }, data),
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -1,13 +1,4 @@
|
|||||||
import {
|
import { animate, animateChild, group, query, state, style, transition, trigger } from '@angular/animations';
|
||||||
animate,
|
|
||||||
animateChild,
|
|
||||||
group,
|
|
||||||
query,
|
|
||||||
state,
|
|
||||||
style,
|
|
||||||
transition,
|
|
||||||
trigger
|
|
||||||
} from '@angular/animations';
|
|
||||||
|
|
||||||
export const slide = trigger('slide', [
|
export const slide = trigger('slide', [
|
||||||
state('expanded', style({ height: '*' })),
|
state('expanded', style({ height: '*' })),
|
||||||
@@ -70,3 +61,30 @@ export const slideSidebarPadding = trigger('slideSidebarPadding', [
|
|||||||
transition('hidden <=> expanded', [animate('200ms')]),
|
transition('hidden <=> expanded', [animate('200ms')]),
|
||||||
transition('shown <=> expanded', [animate('200ms')]),
|
transition('shown <=> expanded', [animate('200ms')]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const expandSearchInput = trigger('toggleAnimation', [
|
||||||
|
state('collapsed', style({
|
||||||
|
width: '30px',
|
||||||
|
opacity: '0'
|
||||||
|
})),
|
||||||
|
state('expanded', style({
|
||||||
|
width: '250px',
|
||||||
|
opacity: '1'
|
||||||
|
})),
|
||||||
|
transition('* => collapsed', group([
|
||||||
|
animate('300ms ease-in-out', style({
|
||||||
|
width: '30px'
|
||||||
|
})),
|
||||||
|
animate('300ms ease-in', style({
|
||||||
|
opacity: '0'
|
||||||
|
}))
|
||||||
|
])),
|
||||||
|
transition('* => expanded', group([
|
||||||
|
animate('300ms ease-out', style({
|
||||||
|
opacity: '1'
|
||||||
|
})),
|
||||||
|
animate('300ms ease-in-out', style({
|
||||||
|
width: '250px'
|
||||||
|
}))
|
||||||
|
]))
|
||||||
|
]);
|
||||||
|
@@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -59,9 +59,8 @@ describe('CreateItemParentSelectorComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call navigate on the router with the correct create path when navigate is called', () => {
|
it('should call navigate on the router with the correct create path when navigate is called', () => {
|
||||||
/* TODO when there is a specific submission path */
|
component.navigate(collection);
|
||||||
// component.navigate(item);
|
expect(router.navigate).toHaveBeenCalledWith(['/submit'], { queryParams: { collection: collection.uuid } });
|
||||||
// expect(router.navigate).toHaveBeenCalledWith([createPath]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
|
||||||
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
|
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
|
||||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
@@ -28,6 +28,11 @@ export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperCo
|
|||||||
* Navigate to the item create page
|
* Navigate to the item create page
|
||||||
*/
|
*/
|
||||||
navigate(dso: DSpaceObject) {
|
navigate(dso: DSpaceObject) {
|
||||||
// There's no submit path per collection yet...
|
const navigationExtras: NavigationExtras = {
|
||||||
|
queryParams: {
|
||||||
|
['collection']: dso.uuid,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.router.navigate(['/submit'], navigationExtras);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -53,16 +53,15 @@
|
|||||||
|
|
||||||
|
|
||||||
<div *ngIf="hasRelationLookup" class="mt-3">
|
<div *ngIf="hasRelationLookup" class="mt-3">
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled" cdkDropList (cdkDropListDropped)="moveSelection($event)">
|
||||||
<li *ngFor="let value of ( selectedValues$ | async)">
|
<ds-existing-metadata-list-element cdkDrag
|
||||||
<button type="button" class="close float-left" aria-label="Close button"
|
*ngFor="let reorderable of reorderables; trackBy: trackReorderable"
|
||||||
(click)="removeSelection(value.selectedResult)">
|
[reoRel]="reorderable"
|
||||||
<span aria-hidden="true">×</span>
|
[submissionItem]="item"
|
||||||
</button>
|
[listId]="listId"
|
||||||
<span class="d-inline-block align-middle ml-1">
|
[metadataFields]="model.metadataFields"
|
||||||
<ds-metadata-representation-loader [mdRepresentation]="value.mdRep"></ds-metadata-representation-loader>
|
[relationshipOptions]="model.relationship">
|
||||||
</span>
|
</ds-existing-metadata-list-element>
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy, ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
ComponentFactoryResolver,
|
ComponentFactoryResolver,
|
||||||
ContentChildren,
|
ContentChildren,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
NgZone,
|
NgZone,
|
||||||
OnChanges, OnDestroy,
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
QueryList,
|
QueryList,
|
||||||
@@ -49,7 +50,10 @@ import {
|
|||||||
DynamicNGBootstrapTimePickerComponent
|
DynamicNGBootstrapTimePickerComponent
|
||||||
} from '@ng-dynamic-forms/ui-ng-bootstrap';
|
} from '@ng-dynamic-forms/ui-ng-bootstrap';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
import {
|
||||||
|
Reorderable,
|
||||||
|
ReorderableRelationship
|
||||||
|
} from './existing-metadata-list-element/existing-metadata-list-element.component';
|
||||||
|
|
||||||
import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model';
|
import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model';
|
||||||
import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||||
@@ -71,9 +75,8 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a
|
|||||||
import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components';
|
import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components';
|
||||||
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-group/dynamic-relation-group.model';
|
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-group/dynamic-relation-group.model';
|
||||||
import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component';
|
import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component';
|
||||||
import { map, switchMap, take, tap } from 'rxjs/operators';
|
import { map, startWith, switchMap, find } from 'rxjs/operators';
|
||||||
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||||
import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer';
|
|
||||||
import { SearchResult } from '../../../search/search-result.model';
|
import { SearchResult } from '../../../search/search-result.model';
|
||||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
@@ -82,23 +85,19 @@ import { SelectableListService } from '../../../object-list/selectable-list/sele
|
|||||||
import { DsDynamicDisabledComponent } from './models/disabled/dynamic-disabled.component';
|
import { DsDynamicDisabledComponent } from './models/disabled/dynamic-disabled.component';
|
||||||
import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-disabled.model';
|
import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-disabled.model';
|
||||||
import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
||||||
import {
|
import { getAllSucceededRemoteData, getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
getAllSucceededRemoteData,
|
|
||||||
getRemoteDataPayload,
|
|
||||||
getSucceededRemoteData
|
|
||||||
} from '../../../../core/shared/operators';
|
|
||||||
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 { ItemDataService } from '../../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
import { RemoveRelationshipAction } from './relation-lookup-modal/relationship.actions';
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '../../../../app.reducer';
|
import { AppState } from '../../../../app.reducer';
|
||||||
import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service';
|
import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service';
|
||||||
import { SubmissionObject } from '../../../../core/submission/models/submission-object.model';
|
import { SubmissionObject } from '../../../../core/submission/models/submission-object.model';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
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 { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
import { MetadataValue } from '../../../../core/shared/metadata.models';
|
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) {
|
||||||
@@ -182,16 +181,15 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
|||||||
@Input() hasErrorMessaging = false;
|
@Input() hasErrorMessaging = false;
|
||||||
@Input() layout = null as DynamicFormLayout;
|
@Input() layout = null as DynamicFormLayout;
|
||||||
@Input() model: any;
|
@Input() model: any;
|
||||||
relationships$: Observable<Array<SearchResult<Item>>>;
|
reorderables$: Observable<ReorderableRelationship[]>;
|
||||||
|
reorderables: ReorderableRelationship[];
|
||||||
hasRelationLookup: boolean;
|
hasRelationLookup: boolean;
|
||||||
modalRef: NgbModalRef;
|
modalRef: NgbModalRef;
|
||||||
item: Item;
|
item: Item;
|
||||||
|
collection: Collection;
|
||||||
listId: string;
|
listId: string;
|
||||||
searchConfig: string;
|
searchConfig: string;
|
||||||
selectedValues$: Observable<Array<{
|
|
||||||
selectedResult: SearchResult<Item>,
|
|
||||||
mdRep: MetadataRepresentation
|
|
||||||
}>>;
|
|
||||||
/**
|
/**
|
||||||
* List of subscriptions to unsubscribe from
|
* List of subscriptions to unsubscribe from
|
||||||
*/
|
*/
|
||||||
@@ -224,7 +222,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
|||||||
private relationshipService: RelationshipService,
|
private relationshipService: RelationshipService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private store: Store<AppState>,
|
private store: Store<AppState>,
|
||||||
private submissionObjectService: SubmissionObjectDataService
|
private submissionObjectService: SubmissionObjectDataService,
|
||||||
|
private ref: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
|
|
||||||
super(componentFactoryResolver, layoutService, validationService, dynamicFormInstanceService);
|
super(componentFactoryResolver, layoutService, validationService, dynamicFormInstanceService);
|
||||||
@@ -235,44 +234,58 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.hasRelationLookup = hasValue(this.model.relationship);
|
this.hasRelationLookup = hasValue(this.model.relationship);
|
||||||
|
this.reorderables = [];
|
||||||
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(
|
||||||
|
switchMap((item) => this.relationService.getItemRelationshipsByLabel(item, this.model.relationship.relationshipType)
|
||||||
|
.pipe(
|
||||||
|
getAllSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((relationshipList: PaginatedList<Relationship>) => relationshipList.page),
|
||||||
|
startWith([]),
|
||||||
|
switchMap((relationships: Relationship[]) =>
|
||||||
|
observableCombineLatest(
|
||||||
|
relationships.map((relationship: Relationship) =>
|
||||||
|
relationship.leftItem.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((leftItem: Item) => {
|
||||||
|
return new ReorderableRelationship(relationship, leftItem.uuid !== this.item.uuid)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
))),
|
||||||
|
map((relationships: ReorderableRelationship[]) =>
|
||||||
|
relationships
|
||||||
|
.sort((a: Reorderable, b: Reorderable) => {
|
||||||
|
return Math.sign(a.getPlace() - b.getPlace());
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subs.push(this.reorderables$.subscribe((rs) => {
|
||||||
|
this.reorderables = rs;
|
||||||
|
this.ref.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
this.relationService.getRelatedItemsByLabel(this.item, this.model.relationship.relationshipType).pipe(
|
this.relationService.getRelatedItemsByLabel(this.item, this.model.relationship.relationshipType).pipe(
|
||||||
map((items: RemoteData<PaginatedList<Item>>) => items.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))),
|
map((items: RemoteData<PaginatedList<Item>>) => items.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))),
|
||||||
).subscribe((relatedItems: Array<SearchResult<Item>>) => this.selectableListService.select(this.listId, relatedItems));
|
).subscribe((relatedItems: Array<SearchResult<Item>>) => this.selectableListService.select(this.listId, relatedItems));
|
||||||
|
|
||||||
this.relationships$ = this.selectableListService.getSelectableList(this.listId).pipe(
|
|
||||||
map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : []),
|
|
||||||
) as Observable<Array<SearchResult<Item>>>;
|
|
||||||
this.selectedValues$ =
|
|
||||||
observableCombineLatest(item$, this.relationships$).pipe(
|
|
||||||
map(([item, relatedItems]: [Item, Array<SearchResult<DSpaceObject>>]) => {
|
|
||||||
return relatedItems
|
|
||||||
.map((element: SearchResult<Item>) => {
|
|
||||||
const relationMD: MetadataValue = item.firstMetadata(this.model.relationship.metadataField, { value: element.indexableObject.uuid });
|
|
||||||
if (hasValue(relationMD)) {
|
|
||||||
const metadataRepresentationMD: MetadataValue = item.firstMetadata(this.model.metadataFields, { authority: relationMD.authority });
|
|
||||||
return {
|
|
||||||
selectedResult: element,
|
|
||||||
mdRep: Object.assign(
|
|
||||||
new ItemMetadataRepresentation(metadataRepresentationMD),
|
|
||||||
element.indexableObject
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}).filter(hasValue)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,15 +344,33 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to remove a selected relationship from the item
|
* Method to move a relationship inside the list of relationships
|
||||||
* @param object The second item in the relationship, the submitted item being the first
|
* This will update the view and update the right or left place field of the relationships in the list
|
||||||
|
* @param event
|
||||||
*/
|
*/
|
||||||
removeSelection(object: SearchResult<Item>) {
|
moveSelection(event: CdkDragDrop<Relationship>) {
|
||||||
this.selectableListService.deselectSingle(this.listId, object);
|
this.zone.runOutsideAngular(() => {
|
||||||
this.store.dispatch(new RemoveRelationshipAction(this.item, object.indexableObject, this.model.relationship.relationshipType))
|
moveItemInArray(this.reorderables, event.previousIndex, event.currentIndex);
|
||||||
|
const reorderables: Reorderable[] = this.reorderables.map((reo: Reorderable, index: number) => {
|
||||||
|
reo.oldIndex = reo.getPlace();
|
||||||
|
reo.newIndex = index;
|
||||||
|
return reo;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
observableCombineLatest(
|
||||||
|
reorderables.map((rel: ReorderableRelationship) => {
|
||||||
|
if (rel.oldIndex !== rel.newIndex) {
|
||||||
|
return this.relationshipService.updatePlace(rel);
|
||||||
|
} else {
|
||||||
|
return observableOf(undefined) as Observable<RemoteData<Relationship>>;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -350,4 +381,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
|||||||
.filter((sub) => hasValue(sub))
|
.filter((sub) => hasValue(sub))
|
||||||
.forEach((sub) => sub.unsubscribe());
|
.forEach((sub) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent unnecessary rerendering so fields don't lose focus
|
||||||
|
*/
|
||||||
|
trackReorderable(index, reorderable: Reorderable) {
|
||||||
|
return hasValue(reorderable) ? reorderable.getId() : undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
<li *ngIf="metadataRepresentation">
|
||||||
|
<button type="button" class="close float-left" aria-label="Move button" cdkDragHandle>
|
||||||
|
<i aria-hidden="true" class="fas fa-arrows-alt fa-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="close float-left" aria-label="Close button" (click)="removeSelection()">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<span class="d-inline-block align-middle ml-1">
|
||||||
|
<ds-metadata-representation-loader [mdRepresentation]="metadataRepresentation"></ds-metadata-representation-loader>
|
||||||
|
</span>
|
||||||
|
</li>
|
@@ -0,0 +1,92 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ExistingMetadataListElementComponent, Reorderable, ReorderableRelationship } from './existing-metadata-list-element.component';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
|
||||||
|
import { select, Store } from '@ngrx/store';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../testing/utils';
|
||||||
|
import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions';
|
||||||
|
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
|
||||||
|
|
||||||
|
describe('ExistingMetadataListElementComponent', () => {
|
||||||
|
let component: ExistingMetadataListElementComponent;
|
||||||
|
let fixture: ComponentFixture<ExistingMetadataListElementComponent>;
|
||||||
|
let selectionService;
|
||||||
|
let store;
|
||||||
|
let listID;
|
||||||
|
let submissionItem;
|
||||||
|
let relationship;
|
||||||
|
let reoRel;
|
||||||
|
let metadataFields;
|
||||||
|
let relationshipOptions;
|
||||||
|
let uuid1;
|
||||||
|
let uuid2;
|
||||||
|
let relatedItem;
|
||||||
|
let leftItemRD$;
|
||||||
|
let rightItemRD$;
|
||||||
|
let relatedSearchResult;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
uuid1 = '91ce578d-2e63-4093-8c73-3faafd716000';
|
||||||
|
uuid2 = '0e9dba1c-e1c3-4e05-a539-446f08ef57a7';
|
||||||
|
selectionService = jasmine.createSpyObj('selectionService', ['deselectSingle']);
|
||||||
|
store = jasmine.createSpyObj('store', ['dispatch']);
|
||||||
|
listID = '1234-listID';
|
||||||
|
submissionItem = Object.assign(new Item(), { uuid: uuid1 });
|
||||||
|
metadataFields = ['dc.contributor.author'];
|
||||||
|
relationshipOptions = Object.assign(new RelationshipOptions(), { relationshipType: 'isPublicationOfAuthor', filter: 'test.filter', searchConfiguration: 'personConfiguration', nameVariants: true })
|
||||||
|
relatedItem = Object.assign(new Item(), { uuid: uuid2 });
|
||||||
|
leftItemRD$ = createSuccessfulRemoteDataObject$(relatedItem);
|
||||||
|
rightItemRD$ = createSuccessfulRemoteDataObject$(submissionItem);
|
||||||
|
relatedSearchResult = Object.assign(new ItemSearchResult(), { indexableObject: relatedItem });
|
||||||
|
|
||||||
|
relationship = Object.assign(new Relationship(), { leftItem: leftItemRD$, rightItem: rightItemRD$ });
|
||||||
|
reoRel = new ReorderableRelationship(relationship, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ExistingMetadataListElementComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: SelectableListService, useValue: selectionService },
|
||||||
|
{ provide: Store, useValue: store },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ExistingMetadataListElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.listId = listID;
|
||||||
|
component.submissionItem = submissionItem;
|
||||||
|
component.reoRel = reoRel;
|
||||||
|
component.metadataFields = metadataFields;
|
||||||
|
component.relationshipOptions = relationshipOptions;
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.ngOnChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeSelection', () => {
|
||||||
|
it('should deselect the object in the selectable list service', () => {
|
||||||
|
component.removeSelection();
|
||||||
|
expect(selectionService.deselectSingle).toHaveBeenCalledWith(listID, relatedSearchResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch a RemoveRelationshipAction', () => {
|
||||||
|
component.removeSelection();
|
||||||
|
const action = new RemoveRelationshipAction(submissionItem, relatedItem, relationshipOptions.relationshipType);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(action);
|
||||||
|
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
@@ -0,0 +1,123 @@
|
|||||||
|
import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { MetadataRepresentation } from '../../../../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
|
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||||
|
import { hasValue, isNotEmpty } from '../../../../empty.util';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { filter } from 'rxjs/operators';
|
||||||
|
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||||
|
import { ItemMetadataRepresentation } from '../../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||||
|
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||||
|
import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions';
|
||||||
|
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { AppState } from '../../../../../app.reducer';
|
||||||
|
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
|
||||||
|
|
||||||
|
// tslint:disable:max-classes-per-file
|
||||||
|
/**
|
||||||
|
* Abstract class that defines objects that can be reordered
|
||||||
|
*/
|
||||||
|
export abstract class Reorderable {
|
||||||
|
constructor(public oldIndex?: number, public newIndex?: number) {
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getId(): string;
|
||||||
|
|
||||||
|
abstract getPlace(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single relationship that can be reordered in a list of multiple relationships
|
||||||
|
*/
|
||||||
|
export class ReorderableRelationship extends Reorderable {
|
||||||
|
relationship: Relationship;
|
||||||
|
useLeftItem: boolean;
|
||||||
|
|
||||||
|
constructor(relationship: Relationship, useLeftItem: boolean, oldIndex?: number, newIndex?: number) {
|
||||||
|
super(oldIndex, newIndex);
|
||||||
|
this.relationship = relationship;
|
||||||
|
this.useLeftItem = useLeftItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): string {
|
||||||
|
return this.relationship.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlace(): number {
|
||||||
|
if (this.useLeftItem) {
|
||||||
|
return this.relationship.rightPlace
|
||||||
|
} else {
|
||||||
|
return this.relationship.leftPlace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single existing relationship value as metadata in submission
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-existing-metadata-list-element',
|
||||||
|
templateUrl: './existing-metadata-list-element.component.html',
|
||||||
|
styleUrls: ['./existing-metadata-list-element.component.scss']
|
||||||
|
})
|
||||||
|
export class ExistingMetadataListElementComponent implements OnChanges, OnDestroy {
|
||||||
|
@Input() listId: string;
|
||||||
|
@Input() submissionItem: Item;
|
||||||
|
@Input() reoRel: ReorderableRelationship;
|
||||||
|
@Input() metadataFields: string[];
|
||||||
|
@Input() relationshipOptions: RelationshipOptions;
|
||||||
|
metadataRepresentation: MetadataRepresentation;
|
||||||
|
relatedItem: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of subscriptions to unsubscribe from
|
||||||
|
*/
|
||||||
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private selectableListService: SelectableListService,
|
||||||
|
private store: Store<AppState>
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges() {
|
||||||
|
const item$ = this.reoRel.useLeftItem ?
|
||||||
|
this.reoRel.relationship.leftItem : this.reoRel.relationship.rightItem;
|
||||||
|
this.subs.push(item$.pipe(
|
||||||
|
getAllSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid))
|
||||||
|
).subscribe((item: Item) => {
|
||||||
|
this.relatedItem = item;
|
||||||
|
const relationMD: MetadataValue = this.submissionItem.firstMetadata(this.relationshipOptions.metadataField, { value: this.relatedItem.uuid });
|
||||||
|
if (hasValue(relationMD)) {
|
||||||
|
const metadataRepresentationMD: MetadataValue = this.submissionItem.firstMetadata(this.metadataFields, { authority: relationMD.authority });
|
||||||
|
this.metadataRepresentation = Object.assign(
|
||||||
|
new ItemMetadataRepresentation(metadataRepresentationMD),
|
||||||
|
this.relatedItem
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the selected relationship from the list
|
||||||
|
*/
|
||||||
|
removeSelection() {
|
||||||
|
this.selectableListService.deselectSingle(this.listId, Object.assign(new ItemSearchResult(), { indexableObject: this.relatedItem }));
|
||||||
|
this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, this.relatedItem, this.relationshipOptions.relationshipType))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from all subscriptions
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs
|
||||||
|
.filter((sub) => hasValue(sub))
|
||||||
|
.forEach((sub) => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// tslint:enable:max-classes-per-file
|
@@ -11,6 +11,9 @@ import { DynamicDisabledModel } from './dynamic-disabled.model';
|
|||||||
selector: 'ds-dynamic-disabled',
|
selector: 'ds-dynamic-disabled',
|
||||||
templateUrl: './dynamic-disabled.component.html'
|
templateUrl: './dynamic-disabled.component.html'
|
||||||
})
|
})
|
||||||
|
/**
|
||||||
|
* Component for displaying a form input with a disabled property
|
||||||
|
*/
|
||||||
export class DsDynamicDisabledComponent extends DynamicFormControlComponent {
|
export class DsDynamicDisabledComponent extends DynamicFormControlComponent {
|
||||||
|
|
||||||
@Input() formId: string;
|
@Input() formId: string;
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<ngb-tabset>
|
<ngb-tabset>
|
||||||
<ngb-tab [title]="'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + label | translate">
|
<ngb-tab [title]="'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + label | translate : {count: (totalInternal$ | async)}">
|
||||||
<ng-template ngbTabContent>
|
<ng-template ngbTabContent>
|
||||||
<ds-dynamic-lookup-relation-search-tab
|
<ds-dynamic-lookup-relation-search-tab
|
||||||
[selection$]="selection$"
|
[selection$]="selection$"
|
||||||
@@ -21,6 +21,22 @@
|
|||||||
</ds-dynamic-lookup-relation-search-tab>
|
</ds-dynamic-lookup-relation-search-tab>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ngb-tab>
|
</ngb-tab>
|
||||||
|
<ngb-tab *ngFor="let source of (externalSourcesRD$ | async)?.payload?.page; let idx = index"
|
||||||
|
[title]="'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + source.id | translate : {count: (totalExternal$ | async)[idx]}">
|
||||||
|
<ng-template ngbTabContent>
|
||||||
|
<ds-dynamic-lookup-relation-external-source-tab
|
||||||
|
[label]="label"
|
||||||
|
[listId]="listId"
|
||||||
|
[item]="item"
|
||||||
|
[collection]="collection"
|
||||||
|
[relationship]="relationshipOptions"
|
||||||
|
[context]="context"
|
||||||
|
[externalSource]="source"
|
||||||
|
(importedObject)="imported($event)"
|
||||||
|
class="d-block pt-3">
|
||||||
|
</ds-dynamic-lookup-relation-external-source-tab>
|
||||||
|
</ng-template>
|
||||||
|
</ngb-tab>
|
||||||
<ngb-tab [title]="'submission.sections.describe.relationship-lookup.selection-tab.tab-title' | translate : {count: (selection$ | async)?.length}">
|
<ngb-tab [title]="'submission.sections.describe.relationship-lookup.selection-tab.tab-title' | translate : {count: (selection$ | async)?.length}">
|
||||||
<ng-template ngbTabContent>
|
<ng-template ngbTabContent>
|
||||||
<ds-dynamic-lookup-relation-selection-tab
|
<ds-dynamic-lookup-relation-selection-tab
|
||||||
@@ -42,4 +58,4 @@
|
|||||||
<div>
|
<div>
|
||||||
<button type="button" class="btn btn-danger" (click)="close()">{{ ('submission.sections.describe.relationship-lookup.close' | translate) }}</button>
|
<button type="button" class="btn btn-danger" (click)="close()">{{ ('submission.sections.describe.relationship-lookup.close' | translate) }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -13,6 +13,12 @@ import { Item } from '../../../../../core/shared/item.model';
|
|||||||
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
|
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
|
||||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||||
import { AddRelationshipAction, RemoveRelationshipAction } from './relationship.actions';
|
import { AddRelationshipAction, RemoveRelationshipAction } from './relationship.actions';
|
||||||
|
import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service';
|
||||||
|
import { PaginatedSearchOptions } from '../../../../search/paginated-search-options.model';
|
||||||
|
import { ExternalSource } from '../../../../../core/shared/external-source.model';
|
||||||
|
import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../../../testing/utils';
|
||||||
|
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
||||||
|
import { LookupRelationService } from '../../../../../core/data/lookup-relation.service';
|
||||||
|
|
||||||
describe('DsDynamicLookupRelationModalComponent', () => {
|
describe('DsDynamicLookupRelationModalComponent', () => {
|
||||||
let component: DsDynamicLookupRelationModalComponent;
|
let component: DsDynamicLookupRelationModalComponent;
|
||||||
@@ -28,6 +34,24 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
|||||||
let relationship;
|
let relationship;
|
||||||
let nameVariant;
|
let nameVariant;
|
||||||
let metadataField;
|
let metadataField;
|
||||||
|
let pSearchOptions;
|
||||||
|
let externalSourceService;
|
||||||
|
let lookupRelationService;
|
||||||
|
|
||||||
|
const externalSources = [
|
||||||
|
Object.assign(new ExternalSource(), {
|
||||||
|
id: 'orcidV2',
|
||||||
|
name: 'orcidV2',
|
||||||
|
hierarchical: false
|
||||||
|
}),
|
||||||
|
Object.assign(new ExternalSource(), {
|
||||||
|
id: 'sherpaPublisher',
|
||||||
|
name: 'sherpaPublisher',
|
||||||
|
hierarchical: false
|
||||||
|
})
|
||||||
|
];
|
||||||
|
const totalLocal = 10;
|
||||||
|
const totalExternal = 8;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
item = Object.assign(new Item(), { uuid: '7680ca97-e2bd-4398-bfa7-139a8673dc42', metadata: {} });
|
item = Object.assign(new Item(), { uuid: '7680ca97-e2bd-4398-bfa7-139a8673dc42', metadata: {} });
|
||||||
@@ -41,6 +65,14 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
|||||||
relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions;
|
relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions;
|
||||||
nameVariant = 'Doe, J.';
|
nameVariant = 'Doe, J.';
|
||||||
metadataField = 'dc.contributor.author';
|
metadataField = 'dc.contributor.author';
|
||||||
|
pSearchOptions = new PaginatedSearchOptions({});
|
||||||
|
externalSourceService = jasmine.createSpyObj('externalSourceService', {
|
||||||
|
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(externalSources))
|
||||||
|
});
|
||||||
|
lookupRelationService = jasmine.createSpyObj('lookupRelationService', {
|
||||||
|
getTotalLocalResults: observableOf(totalLocal),
|
||||||
|
getTotalExternalResults: observableOf(totalExternal)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
@@ -49,6 +81,13 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
|||||||
declarations: [DsDynamicLookupRelationModalComponent],
|
declarations: [DsDynamicLookupRelationModalComponent],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule.forRoot()],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule.forRoot()],
|
||||||
providers: [
|
providers: [
|
||||||
|
{
|
||||||
|
provide: SearchConfigurationService, useValue: {
|
||||||
|
paginatedSearchOptions: observableOf(pSearchOptions)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ provide: ExternalSourceService, useValue: externalSourceService },
|
||||||
|
{ provide: LookupRelationService, useValue: lookupRelationService },
|
||||||
{
|
{
|
||||||
provide: SelectableListService, useValue: selectableListService
|
provide: SelectableListService, useValue: selectableListService
|
||||||
},
|
},
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
|
import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { combineLatest, Observable, Subscription } from 'rxjs';
|
import { combineLatest, Observable, Subscription, zip as observableZip } from 'rxjs';
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { hasValue } from '../../../../empty.util';
|
import { hasValue } from '../../../../empty.util';
|
||||||
import { map, skip, switchMap, take } from 'rxjs/operators';
|
import { map, skip, switchMap, take } from 'rxjs/operators';
|
||||||
@@ -11,7 +11,11 @@ import { ListableObject } from '../../../../object-collection/shared/listable-ob
|
|||||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||||
import { SearchResult } from '../../../../search/search-result.model';
|
import { SearchResult } from '../../../../search/search-result.model';
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators';
|
import {
|
||||||
|
getAllSucceededRemoteData,
|
||||||
|
getRemoteDataPayload,
|
||||||
|
getSucceededRemoteData
|
||||||
|
} from '../../../../../core/shared/operators';
|
||||||
import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions';
|
import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions';
|
||||||
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
||||||
import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service';
|
import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service';
|
||||||
@@ -20,6 +24,11 @@ import { AppState } from '../../../../../app.reducer';
|
|||||||
import { Context } from '../../../../../core/shared/context.model';
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
|
||||||
import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||||
|
import { LookupRelationService } from '../../../../../core/data/lookup-relation.service';
|
||||||
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../../../core/data/paginated-list';
|
||||||
|
import { ExternalSource } from '../../../../../core/shared/external-source.model';
|
||||||
|
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-dynamic-lookup-relation-modal',
|
selector: 'ds-dynamic-lookup-relation-modal',
|
||||||
@@ -37,23 +46,81 @@ import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
|||||||
* Represents a modal where the submitter can select items to be added as a certain relationship type to the object being submitted
|
* Represents a modal where the submitter can select items to be added as a certain relationship type to the object being submitted
|
||||||
*/
|
*/
|
||||||
export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy {
|
export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy {
|
||||||
|
/**
|
||||||
|
* The label to use to display i18n messages (describing the type of relationship)
|
||||||
|
*/
|
||||||
label: string;
|
label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for searching related items
|
||||||
|
*/
|
||||||
relationshipOptions: RelationshipOptions;
|
relationshipOptions: RelationshipOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the list to add/remove selected items to/from
|
||||||
|
*/
|
||||||
listId: string;
|
listId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item we're adding relationships to
|
||||||
|
*/
|
||||||
item;
|
item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The collection we're submitting an item to
|
||||||
|
*/
|
||||||
|
collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the selection repeatable?
|
||||||
|
*/
|
||||||
repeatable: boolean;
|
repeatable: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of selected items
|
||||||
|
*/
|
||||||
selection$: Observable<ListableObject[]>;
|
selection$: Observable<ListableObject[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context to display lists
|
||||||
|
*/
|
||||||
context: Context;
|
context: Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata-fields describing these relationships
|
||||||
|
*/
|
||||||
metadataFields: string;
|
metadataFields: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of subscriptions within this component
|
||||||
|
*/
|
||||||
subMap: {
|
subMap: {
|
||||||
[uuid: string]: Subscription
|
[uuid: string]: Subscription
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of the available external sources configured for this relationship
|
||||||
|
*/
|
||||||
|
externalSourcesRD$: Observable<RemoteData<PaginatedList<ExternalSource>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total amount of internal items for the current options
|
||||||
|
*/
|
||||||
|
totalInternal$: Observable<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total amount of results for each external source using the current options
|
||||||
|
*/
|
||||||
|
totalExternal$: Observable<number[]>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public modal: NgbActiveModal,
|
public modal: NgbActiveModal,
|
||||||
private selectableListService: SelectableListService,
|
private selectableListService: SelectableListService,
|
||||||
private relationshipService: RelationshipService,
|
private relationshipService: RelationshipService,
|
||||||
private relationshipTypeService: RelationshipTypeService,
|
private relationshipTypeService: RelationshipTypeService,
|
||||||
|
private externalSourceService: ExternalSourceService,
|
||||||
|
private lookupRelationService: LookupRelationService,
|
||||||
|
private searchConfigService: SearchConfigurationService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private store: Store<AppState>
|
private store: Store<AppState>
|
||||||
) {
|
) {
|
||||||
@@ -70,13 +137,19 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
this.context = Context.SubmissionModal;
|
this.context = Context.SubmissionModal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.setExistingNameVariants();
|
this.externalSourcesRD$ = this.externalSourceService.findAll();
|
||||||
|
|
||||||
|
this.setTotals();
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.modal.close();
|
this.modal.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select (a list of) objects and add them to the store
|
||||||
|
* @param selectableObjects
|
||||||
|
*/
|
||||||
select(...selectableObjects: Array<SearchResult<Item>>) {
|
select(...selectableObjects: Array<SearchResult<Item>>) {
|
||||||
this.zone.runOutsideAngular(
|
this.zone.runOutsideAngular(
|
||||||
() => {
|
() => {
|
||||||
@@ -104,6 +177,10 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a subscription updating relationships with name variants
|
||||||
|
* @param sri The search result to track name variants for
|
||||||
|
*/
|
||||||
private addNameVariantSubscription(sri: SearchResult<Item>) {
|
private addNameVariantSubscription(sri: SearchResult<Item>) {
|
||||||
const nameVariant$ = this.relationshipService.getNameVariant(this.listId, sri.indexableObject.uuid);
|
const nameVariant$ = this.relationshipService.getNameVariant(this.listId, sri.indexableObject.uuid);
|
||||||
this.subMap[sri.indexableObject.uuid] = nameVariant$.pipe(
|
this.subMap[sri.indexableObject.uuid] = nameVariant$.pipe(
|
||||||
@@ -111,6 +188,10 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
).subscribe((nameVariant: string) => this.store.dispatch(new UpdateRelationshipAction(this.item, sri.indexableObject, this.relationshipOptions.relationshipType, nameVariant)))
|
).subscribe((nameVariant: string) => this.store.dispatch(new UpdateRelationshipAction(this.item, sri.indexableObject, this.relationshipOptions.relationshipType, nameVariant)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deselect (a list of) objects and remove them from the store
|
||||||
|
* @param selectableObjects
|
||||||
|
*/
|
||||||
deselect(...selectableObjects: Array<SearchResult<Item>>) {
|
deselect(...selectableObjects: Array<SearchResult<Item>>) {
|
||||||
this.zone.runOutsideAngular(
|
this.zone.runOutsideAngular(
|
||||||
() => selectableObjects.forEach((object) => {
|
() => selectableObjects.forEach((object) => {
|
||||||
@@ -120,6 +201,9 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set existing name variants for items by the item's virtual metadata
|
||||||
|
*/
|
||||||
private setExistingNameVariants() {
|
private setExistingNameVariants() {
|
||||||
const virtualMDs: MetadataValue[] = this.item.allMetadata(this.metadataFields).filter((mdValue) => mdValue.isVirtual);
|
const virtualMDs: MetadataValue[] = this.item.allMetadata(this.metadataFields).filter((mdValue) => mdValue.isVirtual);
|
||||||
|
|
||||||
@@ -154,6 +238,37 @@ 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
|
||||||
|
*/
|
||||||
|
setTotals() {
|
||||||
|
this.totalInternal$ = this.searchConfigService.paginatedSearchOptions.pipe(
|
||||||
|
switchMap((options) => this.lookupRelationService.getTotalLocalResults(this.relationshipOptions, options))
|
||||||
|
);
|
||||||
|
|
||||||
|
const externalSourcesAndOptions$ = combineLatest(
|
||||||
|
this.externalSourcesRD$.pipe(
|
||||||
|
getAllSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload()
|
||||||
|
),
|
||||||
|
this.searchConfigService.paginatedSearchOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
this.totalExternal$ = externalSourcesAndOptions$.pipe(
|
||||||
|
switchMap(([sources, options]) =>
|
||||||
|
observableZip(...sources.page.map((source: ExternalSource) => this.lookupRelationService.getTotalExternalResults(source, options))))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe());
|
Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,31 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
<h3>{{ 'submission.sections.describe.relationship-lookup.selection-tab.settings' | translate}}</h3>
|
||||||
|
<ds-page-size-selector></ds-page-size-selector>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<ds-search-form [query]="(searchConfigService.paginatedSearchOptions | async)?.query" [inPlaceSearch]="true"></ds-search-form>
|
||||||
|
<div>
|
||||||
|
<h3>{{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + externalSource.id | translate}}</h3>
|
||||||
|
<ng-container *ngVar="(entriesRD$ | async) as entriesRD">
|
||||||
|
<ds-viewable-collection *ngIf="entriesRD?.hasSucceeded && !entriesRD?.isLoading && entriesRD?.payload?.page?.length > 0" @fadeIn
|
||||||
|
[objects]="entriesRD"
|
||||||
|
[selectionConfig]="{ repeatable: repeatable, listId: listId }"
|
||||||
|
[config]="initialPagination"
|
||||||
|
[hideGear]="true"
|
||||||
|
[context]="context"
|
||||||
|
[importable]="true"
|
||||||
|
[importConfig]="importConfig"
|
||||||
|
(importObject)="import($event)">
|
||||||
|
</ds-viewable-collection>
|
||||||
|
<ds-loading *ngIf="!entriesRD || !entriesRD?.payload || entriesRD?.isLoading"
|
||||||
|
message="{{'loading.search-results' | translate}}"></ds-loading>
|
||||||
|
<ds-error *ngIf="entriesRD?.hasFailed && (!entriesRD?.error || entriesRD?.error?.statusCode != 400)"
|
||||||
|
message="{{'error.search-results' | translate}}"></ds-error>
|
||||||
|
<div *ngIf="entriesRD?.payload?.page?.length == 0 || entriesRD?.error?.statusCode == 400" id="empty-external-entry-list">
|
||||||
|
{{ 'search.results.empty' | translate }}
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user