Merge branch 'master' into w2p-65272_Edit-collection-item-template

Conflicts:
	resources/i18n/en.json5
	src/app/+collection-page/collection-page.module.ts
	src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html
	src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts
	src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts
	src/app/core/core.module.ts
	src/app/core/data/data.service.ts
	src/app/shared/shared.module.ts
This commit is contained in:
Kristof De Langhe
2020-01-23 14:30:44 +01:00
587 changed files with 17794 additions and 4048 deletions

View File

@@ -1,5 +1,5 @@
sudo: required
dist: trusty
dist: bionic
env:
# Install the latest docker-compose version for ci testing.
@@ -12,6 +12,9 @@ env:
DSPACE_REST_NAMESPACE: '/server/api'
DSPACE_REST_SSL: false
services:
- xvfb
before_install:
# Docker Compose Install
- 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:
- docker-compose -f ./docker/docker-compose-travis.yml down
addons:
apt:
sources:
- google-chrome
packages:
- dpkg
- google-chrome-stable
language: node_js
node_js:
@@ -53,8 +48,6 @@ cache:
bundler_args: --retry 5
script:
# Use Chromium instead of Chrome.
- export CHROME_BIN=chromium-browser
- yarn run build
- yarn run ci
- cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js

View File

@@ -2,7 +2,8 @@ import { browser, element, by } from 'protractor';
export class ProtractorPage {
navigateTo() {
return browser.get('/');
return browser.get('/')
.then(() => browser.waitForAngular());
}
getPageTitleText() {

View 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;
}
});

View 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);
}
}

View File

@@ -1,4 +1,4 @@
import { browser, element, by, protractor } from 'protractor';
import { browser, by, element, protractor } from 'protractor';
import { promise } from 'selenium-webdriver';
export class ProtractorPage {
@@ -27,15 +27,15 @@ export class ProtractorPage {
}
setCurrentScope(scope: string) {
element(by.css('option[value="' + scope + '"]')).click();
element(by.css('#search-form option[value="' + scope + '"]')).click();
}
setCurrentQuery(query: string) {
element(by.css('input[name="query"]')).sendKeys(query);
element(by.css('#search-form input[name="query"]')).sendKeys(query);
}
submitSearchForm() {
element(by.css('button.search-button')).click();
element(by.css('#search-form button.search-button')).click();
}
getRandomScopeOption(): promise.Promise<string> {

View File

@@ -15,7 +15,11 @@ module.exports = function (config) {
};
var configuration = {
client: {
jasmine: {
random: false
}
},
// base path that will be used to resolve all patterns (e.g. files, exclude)
basePath: '',

View File

@@ -11,6 +11,7 @@
"node": "8.* || >= 10.*"
},
"resolutions": {
"serialize-javascript": ">= 2.1.2",
"set-value": ">= 2.0.1"
},
"scripts": {
@@ -74,37 +75,38 @@
"sync-i18n": "node ./scripts/sync-i18n-files.js"
},
"dependencies": {
"@angular/animations": "^6.1.4",
"@angular/cli": "^6.1.5",
"@angular/common": "^6.1.4",
"@angular/core": "^6.1.4",
"@angular/forms": "^6.1.4",
"@angular/http": "^6.1.4",
"@angular/platform-browser": "^6.1.4",
"@angular/platform-browser-dynamic": "^6.1.4",
"@angular/platform-server": "^6.1.4",
"@angular/router": "^6.1.4",
"@angular/animations": "^7.2.15",
"@angular/cdk": "7.3.7",
"@angular/cli": "^7.3.5",
"@angular/common": "^7.2.15",
"@angular/core": "^7.2.15",
"@angular/forms": "^7.2.15",
"@angular/http": "^7.2.15",
"@angular/platform-browser": "^7.2.15",
"@angular/platform-browser-dynamic": "^7.2.15",
"@angular/platform-server": "^7.2.15",
"@angular/router": "^7.2.15",
"@angularclass/bootloader": "1.0.1",
"@ng-bootstrap/ng-bootstrap": "^2.0.0",
"@ng-dynamic-forms/core": "6.2.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "6.2.0",
"@ngrx/effects": "^6.1.0",
"@ngrx/router-store": "^6.1.0",
"@ngrx/store": "^6.1.0",
"@nguniversal/express-engine": "6.1.0",
"@ngx-translate/core": "10.0.2",
"@ngx-translate/http-loader": "3.0.1",
"@ng-bootstrap/ng-bootstrap": "^4.1.0",
"@ng-dynamic-forms/core": "^7.1.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "^7.1.0",
"@ngrx/effects": "^7.3.0",
"@ngrx/router-store": "^7.3.0",
"@ngrx/store": "^7.3.0",
"@nguniversal/express-engine": "^7.1.1",
"@ngx-translate/core": "11.0.1",
"@ngx-translate/http-loader": "4.0.0",
"@nicky-lenaers/ngx-scroll-to": "^1.0.0",
"angular-idle-preload": "3.0.0",
"angular-sortablejs": "^2.5.0",
"angular2-text-mask": "9.0.0",
"angulartics2": "^6.2.0",
"angulartics2": "7.5.2",
"body-parser": "1.18.2",
"bootstrap": "4.3.1",
"cerialize": "0.1.18",
"compression": "1.7.1",
"cookie-parser": "1.4.3",
"core-js": "^2.5.7",
"core-js": "^2.6.5",
"debug-loader": "^0.0.1",
"express": "4.16.2",
"express-session": "1.15.6",
@@ -112,6 +114,7 @@
"file-saver": "^1.3.8",
"font-awesome": "4.7.0",
"fork-ts-checker-webpack-plugin": "^0.4.10",
"hammerjs": "^2.0.8",
"http-server": "0.11.1",
"https": "1.0.0",
"js-cookie": "2.2.0",
@@ -121,37 +124,40 @@
"jwt-decode": "^2.2.0",
"methods": "1.1.2",
"moment": "^2.22.1",
"moment-range": "^4.0.2",
"morgan": "^1.9.1",
"ng-mocks": "^6.2.1",
"ng-mocks": "^7.6.0",
"ng2-file-upload": "1.2.1",
"ng2-nouislider": "^1.7.11",
"ng2-nouislider": "^1.8.2",
"ngx-bootstrap": "^3.2.0",
"ngx-infinite-scroll": "6.0.1",
"ngx-moment": "^3.1.0",
"ngx-moment": "^3.4.0",
"ngx-pagination": "3.0.3",
"nouislider": "^11.0.0",
"pem": "1.13.2",
"reflect-metadata": "0.1.12",
"rxjs": "6.2.2",
"rxjs": "6.4.0",
"rxjs-spy": "^7.5.1",
"sass-resources-loader": "^2.0.0",
"sortablejs": "1.7.0",
"text-mask-core": "5.0.1",
"ts-loader": "^5.2.1",
"ts-md5": "^1.2.4",
"url-parse": "^1.4.7",
"uuid": "^3.2.1",
"webfontloader": "1.6.28",
"webpack-cli": "^3.1.0",
"zone.js": "^0.8.26"
"webpack-cli": "^3.2.0",
"zone.js": "^0.8.29"
},
"devDependencies": {
"@angular/compiler": "^6.1.4",
"@angular/compiler-cli": "^6.1.4",
"@angular-devkit/build-angular": "^0.13.5",
"@angular/compiler": "^7.2.15",
"@angular/compiler-cli": "^7.2.15",
"@fortawesome/fontawesome-free": "^5.5.0",
"@ngrx/entity": "^6.1.0",
"@ngrx/schematics": "^6.1.0",
"@ngrx/store-devtools": "^6.1.0",
"@ngtools/webpack": "^6.1.5",
"@ngrx/entity": "^7.3.0",
"@ngrx/schematics": "^7.3.0",
"@ngrx/store-devtools": "^7.3.0",
"@ngtools/webpack": "^7.3.9",
"@schematics/angular": "^0.7.5",
"@types/acorn": "^4.0.3",
"@types/cookie-parser": "1.4.1",
@@ -160,44 +166,47 @@
"@types/express-serve-static-core": "4.16.0",
"@types/file-saver": "^1.3.0",
"@types/hammerjs": "2.0.35",
"@types/jasmine": "^2.8.6",
"@types/jasmine": "^3.3.9",
"@types/js-cookie": "2.1.0",
"@types/json5": "^0.0.30",
"@types/lodash": "^4.14.110",
"@types/memory-cache": "0.2.0",
"@types/mime": "2.0.0",
"@types/node": "^10.9.4",
"@types/node": "^11.11.2",
"@types/serve-static": "1.13.2",
"@types/uuid": "^3.4.3",
"@types/webfontloader": "1.6.29",
"@typescript-eslint/eslint-plugin": "^2.12.0",
"@typescript-eslint/parser": "^2.12.0",
"ajv": "^6.1.1",
"ajv-keywords": "^3.1.0",
"angular2-template-loader": "0.6.2",
"autoprefixer": "^9.1.3",
"caniuse-lite": "^1.0.30000697",
"cli-progress": "^3.3.1",
"codelyzer": "^4.4.4",
"codelyzer": "^5.1.0",
"commander": "^3.0.2",
"compression-webpack-plugin": "^1.1.6",
"copy-webpack-plugin": "^4.4.1",
"compression-webpack-plugin": "^3.0.1",
"copy-webpack-plugin": "^5.1.1",
"copyfiles": "^2.1.1",
"coveralls": "3.0.0",
"css-loader": "1.0.0",
"css-loader": "3.4.0",
"cssnano": "^4.1.10",
"deep-freeze": "0.0.1",
"eslint": "^6.7.2",
"exports-loader": "^0.7.0",
"html-webpack-plugin": "^4.0.0-alpha",
"html-webpack-plugin": "3.2.0",
"imports-loader": "0.8.0",
"istanbul-instrumenter-loader": "3.0.1",
"jasmine-core": "^3.2.1",
"jasmine-core": "^3.3.0",
"jasmine-marbles": "0.3.1",
"jasmine-spec-reporter": "4.2.1",
"karma": "3.0.0",
"karma": "4.0.1",
"karma-chrome-launcher": "2.2.0",
"karma-cli": "1.0.1",
"karma-cli": "2.0.0",
"karma-coverage": "1.1.2",
"karma-istanbul-preprocessor": "0.0.2",
"karma-jasmine": "1.1.2",
"karma-jasmine": "2.0.1",
"karma-mocha-reporter": "2.2.5",
"karma-phantomjs-launcher": "1.0.4",
"karma-remap-coverage": "^0.1.5",
@@ -221,26 +230,26 @@
"protractor": "^5.4.2",
"protractor-istanbul-plugin": "2.0.0",
"raw-loader": "0.5.1",
"resolve-url-loader": "^2.3.0",
"rimraf": "2.6.2",
"rollup": "^0.65.0",
"rollup-plugin-commonjs": "^9.1.6",
"rollup-plugin-node-globals": "1.2.1",
"rollup-plugin-node-resolve": "^3.0.3",
"rollup-plugin-terser": "^2.0.2",
"sass-loader": "7.1.0",
"script-ext-html-webpack-plugin": "2.0.1",
"sass-loader": "7.3.1",
"script-ext-html-webpack-plugin": "2.1.4",
"source-map": "0.7.3",
"source-map-loader": "0.2.4",
"string-replace-loader": "^2.1.1",
"terser-webpack-plugin": "^2.3.1",
"to-string-loader": "1.1.5",
"ts-helpers": "1.1.2",
"ts-node": "4.1.0",
"tslint": "5.11.0",
"typedoc": "^0.9.0",
"typescript": "^2.9.1",
"typescript": "3.1.6",
"webdriver-manager": "^12.1.7",
"webpack": "^4.17.1",
"webpack": "^4.29.6",
"webpack-bundle-analyzer": "^3.3.2",
"webpack-dev-middleware": "3.2.0",
"webpack-dev-server": "^3.1.11",

View File

@@ -5,7 +5,7 @@
var SpecReporter = require('jasmine-spec-reporter').SpecReporter;
exports.config = {
allScriptsTimeout: 11000,
allScriptsTimeout: 600000,
// -----------------------------------------------------------------
// Uncomment to run tests using a remote Selenium server
//seleniumAddress: 'http://selenium.address:4444/wd/hub',
@@ -73,7 +73,7 @@ exports.config = {
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
defaultTimeoutInterval: 600000,
print: function () {}
},
useAllAngular2AppRoots: true,

View File

@@ -0,0 +1,3 @@
# Supported font formats
DSpace supports EOT, TTF, OTF, SVG, WOFF and WOFF2 fonts.

View File

@@ -244,6 +244,8 @@
"collection.create.head": "Create a Collection",
"collection.create.notifications.success": "Successfully created the Collection",
"collection.create.sub-head": "Create a Collection for Community {{ parent }}",
"collection.delete.cancel": "Cancel",
@@ -264,6 +266,62 @@
"collection.edit.head": "Edit Collection",
"collection.edit.item-mapper.cancel": "Cancel",
"collection.edit.item-mapper.collection": "Collection: \"<b>{{name}}</b>\"",
"collection.edit.item-mapper.confirm": "Map selected items",
"collection.edit.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.",
"collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections",
"collection.edit.item-mapper.no-search": "Please enter a query to search",
"collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.",
"collection.edit.item-mapper.notifications.map.error.head": "Mapping errors",
"collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.",
"collection.edit.item-mapper.notifications.map.success.head": "Mapping completed",
"collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.",
"collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors",
"collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.",
"collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed",
"collection.edit.item-mapper.remove": "Remove selected item mappings",
"collection.edit.item-mapper.tabs.browse": "Browse mapped items",
"collection.edit.item-mapper.tabs.map": "Map new items",
"collection.edit.logo.label": "Collection logo",
"collection.edit.logo.notifications.add.error": "Uploading Collection logo failed. Please verify the content before retrying.",
"collection.edit.logo.notifications.add.success": "Upload Collection logo successful.",
"collection.edit.logo.notifications.delete.success.title": "Logo deleted",
"collection.edit.logo.notifications.delete.success.content": "Successfully deleted the collection's logo",
"collection.edit.logo.notifications.delete.error.title": "Error deleting logo",
"collection.edit.logo.upload": "Drop a Collection Logo to upload",
"collection.edit.notifications.success": "Successfully edited the Collection",
"collection.edit.return": "Return",
@@ -304,42 +362,6 @@
"collection.edit.item-mapper.cancel": "Cancel",
"collection.edit.item-mapper.collection": "Collection: \"<b>{{name}}</b>\"",
"collection.edit.item-mapper.confirm": "Map selected items",
"collection.edit.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.",
"collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections",
"collection.edit.item-mapper.no-search": "Please enter a query to search",
"collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.",
"collection.edit.item-mapper.notifications.map.error.head": "Mapping errors",
"collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.",
"collection.edit.item-mapper.notifications.map.success.head": "Mapping completed",
"collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.",
"collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors",
"collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.",
"collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed",
"collection.edit.item-mapper.remove": "Remove selected item mappings",
"collection.edit.item-mapper.tabs.browse": "Browse mapped items",
"collection.edit.item-mapper.tabs.map": "Map new items",
"collection.form.abstract": "Short Description",
"collection.form.description": "Introductory text (HTML)",
@@ -378,8 +400,18 @@
"communityList.tabTitle": "DSpace - Community List",
"communityList.title": "List of Communities",
"communityList.showMore": "Show More",
"community.create.head": "Create a Community",
"community.create.notifications.success": "Successfully created the Community",
"community.create.sub-head": "Create a Sub-Community for Community {{ parent }}",
"community.delete.cancel": "Cancel",
@@ -398,8 +430,30 @@
"community.edit.head": "Edit Community",
"community.edit.logo.label": "Community logo",
"community.edit.logo.notifications.add.error": "Uploading Community logo failed. Please verify the content before retrying.",
"community.edit.logo.notifications.add.success": "Upload Community logo successful.",
"community.edit.logo.notifications.delete.success.title": "Logo deleted",
"community.edit.logo.notifications.delete.success.content": "Successfully deleted the community's logo",
"community.edit.logo.notifications.delete.error.title": "Error deleting logo",
"community.edit.logo.upload": "Drop a Community Logo to upload",
"community.edit.notifications.success": "Successfully edited the Community",
"community.edit.return": "Return",
"community.edit.tabs.curate.head": "Curate",
"community.edit.tabs.curate.title": "Community Edit - Curate",
@@ -412,6 +466,8 @@
"community.edit.tabs.roles.title": "Community Edit - Roles",
"community.form.abstract": "Short Description",
"community.form.description": "Introductory text (HTML)",
@@ -503,6 +559,9 @@
"footer.link.duraspace": "DuraSpace",
"form.add": "Add",
"form.add-help": "Click here to add the current entry and to add another one",
"form.cancel": "Cancel",
@@ -528,6 +587,10 @@
"form.loading": "Loading...",
"form.lookup": "Lookup",
"form.lookup-help": "Click here to look up an existing relation",
"form.no-results": "No results found",
"form.no-value": "No value entered",
@@ -768,7 +831,7 @@
"item.edit.tabs.relationships.head": "Item Relationships",
"item.edit.tabs.relationships.title": "Item Edit - Relationships",
"item.edit.tabs.relationships.title": "Item Edit - Relationships",
"item.edit.tabs.status.buttons.authorizations.button": "Authorizations...",
@@ -866,9 +929,17 @@
"item.page.person.search.title": "Articles by this author",
"item.page.related-items.view-more": "View more",
"item.page.related-items.view-more": "Show {{ amount }} more",
"item.page.related-items.view-less": "View less",
"item.page.related-items.view-less": "Hide last {{ amount }}",
"item.page.relationships.isAuthorOfPublication": "Publications",
"item.page.relationships.isJournalOfPublication": "Publications",
"item.page.relationships.isOrgUnitOfPerson": "Authors",
"item.page.relationships.isOrgUnitOfProject": "Research Projects",
"item.page.subject": "Keywords",
@@ -1320,6 +1391,8 @@
"project.page.titleprefix": "Research Project: ",
"project.search.results.head": "Project Search Results",
"publication.listelement.badge": "Publication",
@@ -1395,6 +1468,9 @@
"search.filters.applied.f.subject": "Subject",
"search.filters.applied.f.submitter": "Submitter",
"search.filters.applied.f.jobTitle": "Job Title",
"search.filters.applied.f.birthDate.max": "End birth date",
"search.filters.applied.f.birthDate.min": "Start birth date",
@@ -1508,6 +1584,8 @@
"search.results.no-results-link": "quotes around it",
"search.results.empty": "Your search returned no results.",
"search.sidebar.close": "Back to results",
@@ -1563,6 +1641,85 @@
"submission.general.save-later": "Save for later",
"submission.sections.describe.relationship-lookup.close": "Close",
"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.loading": "Loading...",
"submission.sections.describe.relationship-lookup.search-tab.placeholder": "Search query",
"submission.sections.describe.relationship-lookup.search-tab.search": "Go",
"submission.sections.describe.relationship-lookup.search-tab.select-all": "Select all",
"submission.sections.describe.relationship-lookup.search-tab.select-page": "Select page",
"submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items",
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Local Authors ({{ count }})",
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})",
"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": "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": "Search for Funding",
"submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Current Selection ({{ count }})",
"submission.sections.describe.relationship-lookup.title.Journal Issue": "Journal Issues",
"submission.sections.describe.relationship-lookup.title.Journal Volume": "Journal Volumes",
"submission.sections.describe.relationship-lookup.title.Journal": "Journals",
"submission.sections.describe.relationship-lookup.title.Author": "Authors",
"submission.sections.describe.relationship-lookup.title.Funding Agency": "Funding Agency",
"submission.sections.describe.relationship-lookup.title.Funding": "Funding",
"submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Toggle dropdown",
"submission.sections.describe.relationship-lookup.selection-tab.settings": "Settings",
"submission.sections.describe.relationship-lookup.selection-tab.no-selection": "Your selection is currently empty.",
"submission.sections.describe.relationship-lookup.selection-tab.title.Author": "Selected Authors",
"submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Selected Journals",
"submission.sections.describe.relationship-lookup.selection-tab.title.Journal Volume": "Selected Journal Volume",
"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.confirm": "Save a new name variant",
"submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Use only for this submission",
"submission.sections.general.add-more": "Add more",
@@ -1732,7 +1889,7 @@
"uploader.drag-message": "Drag & Drop your files here",
"uploader.or": ", or",
"uploader.or": ", or ",
"uploader.processing": "Processing",

View File

@@ -5,7 +5,7 @@ import { PaginatedList } from '../../../core/data/paginated-list';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
import { FindAllOptions } from '../../../core/data/request.models';
import { FindListOptions } from '../../../core/data/request.models';
import { map, switchMap, take } from 'rxjs/operators';
import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
@@ -35,7 +35,7 @@ export class BitstreamFormatsComponent implements OnInit {
* The current pagination configuration for the page used by the FindAll method
* Currently simply renders all bitstream formats
*/
config: FindAllOptions = Object.assign(new FindAllOptions(), {
config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 20
});
@@ -145,7 +145,7 @@ export class BitstreamFormatsComponent implements OnInit {
* @param event The page change event
*/
onPageChange(event) {
this.config = Object.assign(new FindAllOptions(), this.config, {
this.config = Object.assign(new FindListOptions(), this.config, {
currentPage: event,
});
this.pageConfig.currentPage = event;

View File

@@ -1,7 +1,6 @@
import { MetadataRegistryComponent } from './metadata-registry.component';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
import { TranslateModule } from '@ngx-translate/core';
import { By } from '@angular/platform-browser';
@@ -18,6 +17,7 @@ import { NotificationsService } from '../../../shared/notifications/notification
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { RestResponse } from '../../../core/cache/response.models';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
describe('MetadataRegistryComponent', () => {
let comp: MetadataRegistryComponent;
@@ -101,12 +101,12 @@ describe('MetadataRegistryComponent', () => {
it('should start editing the selected schema', async(() => {
fixture.whenStable().then(() => {
expect(registryService.editMetadataSchema).toHaveBeenCalledWith(mockSchemasList[0]);
expect(registryService.editMetadataSchema).toHaveBeenCalledWith(mockSchemasList[0] as MetadataSchema);
});
}));
it('should cancel editing the selected schema when clicked again', async(() => {
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0]));
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0] as MetadataSchema));
spyOn(registryService, 'cancelEditMetadataSchema');
row.click();
fixture.detectChanges();
@@ -121,7 +121,7 @@ describe('MetadataRegistryComponent', () => {
beforeEach(() => {
spyOn(registryService, 'deleteMetadataSchema').and.callThrough();
spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas));
spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas as MetadataSchema[]));
comp.deleteSchemas();
fixture.detectChanges();
});

View File

@@ -1,7 +1,6 @@
import { MetadataSchemaComponent } from './metadata-schema.component';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
import { TranslateModule } from '@ngx-translate/core';
import { CommonModule } from '@angular/common';
@@ -22,6 +21,7 @@ import { NotificationsServiceStub } from '../../../shared/testing/notifications-
import { RestResponse } from '../../../core/cache/response.models';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { MetadataField } from '../../../core/metadata/metadata-field.model';
describe('MetadataSchemaComponent', () => {
let comp: MetadataSchemaComponent;
@@ -152,12 +152,12 @@ describe('MetadataSchemaComponent', () => {
it('should start editing the selected field', async(() => {
fixture.whenStable().then(() => {
expect(registryService.editMetadataField).toHaveBeenCalledWith(mockFieldsList[2]);
expect(registryService.editMetadataField).toHaveBeenCalledWith(mockFieldsList[2] as MetadataField);
});
}));
it('should cancel editing the selected field when clicked again', async(() => {
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2]));
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2] as MetadataField));
spyOn(registryService, 'cancelEditMetadataField');
row.click();
fixture.detectChanges();
@@ -172,7 +172,7 @@ describe('MetadataSchemaComponent', () => {
beforeEach(() => {
spyOn(registryService, 'deleteMetadataField').and.callThrough();
spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields));
spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields as MetadataField[]));
comp.deleteFields();
fixture.detectChanges();
});

View File

@@ -13,11 +13,11 @@ import { combineLatest as combineLatestObservable } from 'rxjs';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-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 { 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
@@ -138,18 +138,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
parentID: 'new',
active: false,
visible: true,
// model: {
// type: MenuItemType.ONCLICK,
// text: 'menu.section.new_item',
// function: () => {
// this.modalService.open(CreateItemParentSelectorComponent);
// }
// } as OnClickMenuItemModel,
model: {
type: MenuItemType.LINK,
type: MenuItemType.ONCLICK,
text: 'menu.section.new_item',
link: '/submit'
} as LinkMenuItemModel,
function: () => {
this.modalService.open(CreateItemParentSelectorComponent);
}
} as OnClickMenuItemModel,
// model: {
// type: MenuItemType.LINK,
// text: 'menu.section.new_item',
// link: '/submit'
// } as LinkMenuItemModel,
},
{
id: 'new_item_version',

View File

@@ -1,9 +1,19 @@
import { Component, Input } from '@angular/core';
import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
import {
DynamicFormControlModel,
DynamicFormService,
DynamicInputModel,
DynamicTextAreaModel
} from '@ng-dynamic-forms/core';
import { Collection } from '../../core/shared/collection.model';
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
import { Location } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { CommunityDataService } from '../../core/data/community-data.service';
import { AuthService } from '../../core/auth/auth.service';
import { RequestService } from '../../core/data/request.service';
import { ObjectCacheService } from '../../core/cache/object-cache.service';
/**
* Form used for creating and editing collections
@@ -22,7 +32,7 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> {
/**
* @type {Collection.type} This is a collection-type form
*/
protected type = Collection.type;
type = Collection.type;
/**
* The dynamic form fields used for creating/editing a collection
@@ -65,4 +75,15 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> {
name: 'dc.description.provenance',
}),
];
public constructor(protected location: Location,
protected formService: DynamicFormService,
protected translate: TranslateService,
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected dsoService: CommunityDataService,
protected requestService: RequestService,
protected objectCache: ObjectCacheService) {
super(location, formService, translate, notificationsService, authService, requestService, objectCache);
}
}

View File

@@ -1,29 +1,23 @@
import { CollectionItemMapperComponent } from './collection-item-mapper.component';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { CommonModule } from '@angular/common';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { SearchFormComponent } from '../../shared/search-form/search-form.component';
import { SearchPageModule } from '../../+search-page/search-page.module';
import { ObjectCollectionComponent } from '../../shared/object-collection/object-collection.component';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
import { RouterStub } from '../../shared/testing/router-stub';
import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service';
import { SearchService } from '../../+search-page/search-service/search.service';
import { SearchServiceStub } from '../../shared/testing/search-service-stub';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
import { ItemDataService } from '../../core/data/item-data.service';
import { FormsModule } from '@angular/forms';
import { SharedModule } from '../../shared/shared.module';
import { Collection } from '../../core/shared/collection.model';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { EventEmitter, NgModule } from '@angular/core';
import { EventEmitter } from '@angular/core';
import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub';
import { By } from '@angular/platform-browser';
@@ -36,13 +30,14 @@ import { ItemSelectComponent } from '../../shared/object-select/item-select/item
import { ObjectSelectService } from '../../shared/object-select/object-select.service';
import { ObjectSelectServiceStub } from '../../shared/testing/object-select-service-stub';
import { VarDirective } from '../../shared/utils/var.directive';
import { Observable } from 'rxjs/internal/Observable';
import { of as observableOf, of } from 'rxjs/internal/observable/of';
import { RestResponse } from '../../core/cache/response.models';
import { SearchFixedFilterService } from '../../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { RouteService } from '../../core/services/route.service';
import { ErrorComponent } from '../../shared/error/error.component';
import { LoadingComponent } from '../../shared/loading/loading.component';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { SearchService } from '../../core/shared/search/search.service';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
describe('CollectionItemMapperComponent', () => {
let comp: CollectionItemMapperComponent;
@@ -135,7 +130,6 @@ describe('CollectionItemMapperComponent', () => {
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() },
{ provide: RouteService, useValue: routeServiceStub },
{ provide: SearchFixedFilterService, useValue: fixedFilterServiceStub }
]
}).compileComponents();
}));

View File

@@ -5,12 +5,9 @@ import { fadeIn, fadeInOut } from '../../shared/animations/fade';
import { ActivatedRoute, Router } from '@angular/router';
import { RemoteData } from '../../core/data/remote-data';
import { Collection } from '../../core/shared/collection.model';
import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service';
import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model';
import { PaginatedList } from '../../core/data/paginated-list';
import { map, startWith, switchMap, take, tap } from 'rxjs/operators';
import { map, startWith, switchMap, take } from 'rxjs/operators';
import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators';
import { SearchService } from '../../+search-page/search-service/search.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
@@ -22,6 +19,9 @@ import { isNotEmpty } from '../../shared/empty.util';
import { RestResponse } from '../../core/cache/response.models';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { SearchService } from '../../core/shared/search/search.service';
@Component({
selector: 'ds-collection-item-mapper',

View File

@@ -3,16 +3,19 @@
*ngVar="(collectionRD$ | async) as collectionRD">
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
<div *ngIf="collectionRD?.payload as collection">
<ds-view-tracker [object]="collection"></ds-view-tracker>
<header class="comcol-header border-bottom mb-4 pb-4">
<!-- Collection logo -->
<ds-comcol-page-logo *ngIf="logoRD$"
[logo]="(logoRD$ | async)?.payload" [alternateText]="'Collection Logo'">
[alternateText]="'Collection Logo'">
</ds-comcol-page-logo>
<!-- Collection Name -->
<!-- Collection Name -->
<ds-comcol-page-header
[name]="collection.name">
</ds-comcol-page-header>
<!-- Collection logo -->
<ds-comcol-page-logo *ngIf="logoRD$"
[logo]="(logoRD$ | async)?.payload"
[alternateText]="'Collection Logo'"
[alternateText]="'Collection Logo'">
</ds-comcol-page-logo>
<!-- Handle -->
<ds-comcol-page-handle
[content]="collection.handle"

View File

@@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, of as observableOf, Observable, Subject } from 'rxjs';
import { filter, flatMap, map, startWith, switchMap, take, tap } from 'rxjs/operators';
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
import { SearchService } from '../+search-page/search-service/search.service';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { SearchService } from '../core/shared/search/search.service';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { CollectionDataService } from '../core/data/collection-data.service';
import { PaginatedList } from '../core/data/paginated-list';

View File

@@ -8,17 +8,18 @@ import { CollectionPageRoutingModule } from './collection-page-routing.module';
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
import { CollectionFormComponent } from './collection-form/collection-form.component';
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
import { SearchService } from '../+search-page/search-service/search.service';
import { EditItemTemplatePageComponent } from './edit-item-template-page/edit-item-template-page.component';
import { EditItemPageModule } from '../+item-page/edit-item-page/edit-item-page.module';
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { SearchService } from '../core/shared/search/search.service';
import { StatisticsModule } from '../statistics/statistics.module';
@NgModule({
imports: [
CommonModule,
SharedModule,
CollectionPageRoutingModule,
StatisticsModule.forRoot(),
EditItemPageModule
],
declarations: [
@@ -34,7 +35,6 @@ import { SearchFixedFilterService } from '../+search-page/search-filters/search-
],
providers: [
SearchService,
SearchFixedFilterService
]
})
export class CollectionPageModule {

View File

@@ -4,5 +4,5 @@
<h2 id="sub-header" class="border-bottom pb-2">{{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}</h2>
</div>
</div>
<ds-collection-form (submitForm)="onSubmit($event)"></ds-collection-form>
<ds-collection-form (submitForm)="onSubmit($event)" (finish)="navigateToNewPage()"></ds-collection-form>
</div>

View File

@@ -10,6 +10,8 @@ import { CollectionDataService } from '../../core/data/collection-data.service';
import { of as observableOf } from 'rxjs';
import { CommunityDataService } from '../../core/data/community-data.service';
import { CreateCollectionPageComponent } from './create-collection-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
describe('CreateCollectionPageComponent', () => {
let comp: CreateCollectionPageComponent;
@@ -27,6 +29,7 @@ describe('CreateCollectionPageComponent', () => {
},
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
{ provide: Router, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -5,6 +5,8 @@ import { Router } from '@angular/router';
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
import { Collection } from '../../core/shared/collection.model';
import { CollectionDataService } from '../../core/data/collection-data.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
/**
* Component that represents the page where a user can create a new Collection
@@ -16,13 +18,16 @@ import { CollectionDataService } from '../../core/data/collection-data.service';
})
export class CreateCollectionPageComponent extends CreateComColPageComponent<Collection> {
protected frontendURL = '/collections/';
protected type = Collection.type;
public constructor(
protected communityDataService: CommunityDataService,
protected collectionDataService: CollectionDataService,
protected routeService: RouteService,
protected router: Router
protected router: Router,
protected notificationsService: NotificationsService,
protected translate: TranslateService
) {
super(collectionDataService, communityDataService, routeService, router);
super(collectionDataService, communityDataService, routeService, router, notificationsService, translate);
}
}

View File

@@ -16,7 +16,9 @@
</button>
</div>
</div>
<ds-collection-form (submitForm)="onSubmit($event)" [dso]="(dsoRD$ | async)?.payload"></ds-collection-form>
<ds-collection-form (submitForm)="onSubmit($event)"
[dso]="(dsoRD$ | async)?.payload"
(finish)="navigateToHomePage()"></ds-collection-form>
<a class="btn btn-danger"
[routerLink]="'/collections/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'collection.edit.delete'
| translate}}</a>

View File

@@ -8,10 +8,10 @@ import { ActivatedRoute, Router } from '@angular/router';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { CollectionMetadataComponent } from './collection-metadata.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { Item } from '../../../core/shared/item.model';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { ItemTemplateDataService } from '../../../core/data/item-template-data.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { Collection } from '../../../core/shared/collection.model';
describe('CollectionMetadataComponent', () => {

View File

@@ -22,6 +22,7 @@ import { TranslateService } from '@ngx-translate/core';
})
export class CollectionMetadataComponent extends ComcolMetadataComponent<Collection> {
protected frontendURL = '/collections/';
protected type = Collection.type;
/**
* The collection's item template
@@ -36,7 +37,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
protected notificationsService: NotificationsService,
protected translate: TranslateService
) {
super(collectionDataService, router, route);
super(collectionDataService, router, route, notificationsService, translate);
}
ngOnInit(): void {

View File

@@ -12,7 +12,7 @@ import { getCollectionPageRoute } from '../collection-page-routing.module';
templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html'
})
export class EditCollectionPageComponent extends EditComColPageComponent<Collection> {
protected type = 'collection';
type = 'collection';
public constructor(
protected router: Router,

View File

@@ -1,9 +1,19 @@
import { Component, Input } from '@angular/core';
import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
import {
DynamicFormControlModel,
DynamicFormService,
DynamicInputModel,
DynamicTextAreaModel
} from '@ng-dynamic-forms/core';
import { Community } from '../../core/shared/community.model';
import { ResourceType } from '../../core/shared/resource-type';
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
import { Location } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { CommunityDataService } from '../../core/data/community-data.service';
import { AuthService } from '../../core/auth/auth.service';
import { RequestService } from '../../core/data/request.service';
import { ObjectCacheService } from '../../core/cache/object-cache.service';
/**
* Form used for creating and editing communities
@@ -22,7 +32,7 @@ export class CommunityFormComponent extends ComColFormComponent<Community> {
/**
* @type {Community.type} This is a community-type form
*/
protected type = Community.type;
type = Community.type;
/**
* The dynamic form fields used for creating/editing a community
@@ -57,4 +67,15 @@ export class CommunityFormComponent extends ComColFormComponent<Community> {
name: 'dc.description.tableofcontents',
}),
];
public constructor(protected location: Location,
protected formService: DynamicFormService,
protected translate: TranslateService,
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected dsoService: CommunityDataService,
protected requestService: RequestService,
protected objectCache: ObjectCacheService) {
super(location, formService, translate, notificationsService, authService, requestService, objectCache);
}
}

View File

@@ -1,13 +1,13 @@
<div class="container" *ngVar="(communityRD$ | async) as communityRD">
<div class="community-page" *ngIf="communityRD?.hasSucceeded" @fadeInOut>
<div *ngIf="communityRD?.payload; let communityPayload">
<ds-view-tracker [object]="communityPayload"></ds-view-tracker>
<header class="comcol-header border-bottom mb-4 pb-4">
<!-- Community name -->
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
<!-- Community logo -->
<ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'Community Logo'">
</ds-comcol-page-logo>
<!-- Community name -->
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
<!-- Handle -->
<ds-comcol-page-handle [content]="communityPayload.handle" [title]="'community.page.handle'">
</ds-comcol-page-handle>

View File

@@ -6,16 +6,18 @@ import { SharedModule } from '../shared/shared.module';
import { CommunityPageComponent } from './community-page.component';
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
import { CommunityPageRoutingModule } from './community-page-routing.module';
import {CommunityPageSubCommunityListComponent} from './sub-community-list/community-page-sub-community-list.component';
import { CommunityPageSubCommunityListComponent } from './sub-community-list/community-page-sub-community-list.component';
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
import { CommunityFormComponent } from './community-form/community-form.component';
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
import { StatisticsModule } from '../statistics/statistics.module';
@NgModule({
imports: [
CommonModule,
SharedModule,
CommunityPageRoutingModule
CommunityPageRoutingModule,
StatisticsModule.forRoot()
],
declarations: [
CommunityPageComponent,

View File

@@ -7,5 +7,5 @@
</ng-container>
</div>
</div>
<ds-community-form (submitForm)="onSubmit($event)"></ds-community-form>
<ds-community-form (submitForm)="onSubmit($event)" (finish)="navigateToNewPage()"></ds-community-form>
</div>

View File

@@ -10,6 +10,8 @@ import { CollectionDataService } from '../../core/data/collection-data.service';
import { of as observableOf } from 'rxjs';
import { CommunityDataService } from '../../core/data/community-data.service';
import { CreateCommunityPageComponent } from './create-community-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
describe('CreateCommunityPageComponent', () => {
let comp: CreateCommunityPageComponent;
@@ -23,6 +25,7 @@ describe('CreateCommunityPageComponent', () => {
{ provide: CommunityDataService, useValue: { findById: () => observableOf({}) } },
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
{ provide: Router, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -4,6 +4,8 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { RouteService } from '../../core/services/route.service';
import { Router } from '@angular/router';
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
/**
* Component that represents the page where a user can create a new Community
@@ -15,12 +17,15 @@ import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comc
})
export class CreateCommunityPageComponent extends CreateComColPageComponent<Community> {
protected frontendURL = '/communities/';
protected type = Community.type;
public constructor(
protected communityDataService: CommunityDataService,
protected routeService: RouteService,
protected router: Router
protected router: Router,
protected notificationsService: NotificationsService,
protected translate: TranslateService
) {
super(communityDataService, communityDataService, routeService, router);
super(communityDataService, communityDataService, routeService, router, notificationsService, translate);
}
}

View File

@@ -1,4 +1,6 @@
<ds-community-form (submitForm)="onSubmit($event)" [dso]="(dsoRD$ | async)?.payload"></ds-community-form>
<ds-community-form (submitForm)="onSubmit($event)"
[dso]="(dsoRD$ | async)?.payload"
(finish)="navigateToHomePage()"></ds-community-form>
<a class="btn btn-danger"
[routerLink]="'/communities/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'community.edit.delete'
| translate}}</a>

View File

@@ -8,6 +8,8 @@ import { of as observableOf } from 'rxjs/internal/observable/of';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { CommunityMetadataComponent } from './community-metadata.component';
import { CommunityDataService } from '../../../core/data/community-data.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
describe('CommunityMetadataComponent', () => {
let comp: CommunityMetadataComponent;
@@ -20,6 +22,7 @@ describe('CommunityMetadataComponent', () => {
providers: [
{ provide: CommunityDataService, useValue: {} },
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -3,6 +3,8 @@ import { ComcolMetadataComponent } from '../../../shared/comcol-forms/edit-comco
import { ActivatedRoute, Router } from '@angular/router';
import { Community } from '../../../core/shared/community.model';
import { CommunityDataService } from '../../../core/data/community-data.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
/**
* Component for editing a community's metadata
@@ -13,12 +15,15 @@ import { CommunityDataService } from '../../../core/data/community-data.service'
})
export class CommunityMetadataComponent extends ComcolMetadataComponent<Community> {
protected frontendURL = '/communities/';
protected type = Community.type;
public constructor(
protected communityDataService: CommunityDataService,
protected router: Router,
protected route: ActivatedRoute
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
protected translate: TranslateService
) {
super(communityDataService, router, route);
super(communityDataService, router, route, notificationsService, translate);
}
}

View File

@@ -12,7 +12,7 @@ import { getCommunityPageRoute } from '../community-page-routing.module';
templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html'
})
export class EditCommunityPageComponent extends EditComColPageComponent<Community> {
protected type = 'community';
type = 'community';
public constructor(
protected router: Router,

View File

@@ -27,7 +27,10 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co
{
path: 'metadata',
component: CommunityMetadataComponent,
data: { title: 'community.edit.tabs.metadata.title' }
data: {
title: 'community.edit.tabs.metadata.title',
hideReturnButton: true
}
},
{
path: 'roles',

View File

@@ -1,14 +1,13 @@
<ng-container *ngVar="(subCollectionsRDObs | async) as subCollectionsRD">
<div *ngIf="subCollectionsRD?.hasSucceeded && subCollectionsRD?.payload.totalElements > 0" @fadeIn>
<h2>{{'community.sub-collection-list.head' | translate}}</h2>
<ul>
<li *ngFor="let collection of subCollectionsRD?.payload.page">
<p>
<span class="lead"><a [routerLink]="['/collections', collection.id]">{{collection.name}}</a></span><br>
<span class="text-muted">{{collection.shortDescription}}</span>
</p>
</li>
</ul>
<ds-viewable-collection
[config]="config"
[sortConfig]="sortConfig"
[objects]="subCollectionsRD"
[hideGear]="false"
(paginationChange)="onPaginationChange($event)">
</ds-viewable-collection>
</div>
<ds-error *ngIf="subCollectionsRD?.hasFailed" message="{{'error.sub-collections' | translate}}"></ds-error>
<ds-loading *ngIf="subCollectionsRD?.isLoading" message="{{'loading.sub-collections' | translate}}"></ds-loading>

View File

@@ -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');
});
});

View File

@@ -1,12 +1,16 @@
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 { Collection } from '../../core/shared/collection.model';
import { Community } from '../../core/shared/community.model';
import { fadeIn } from '../../shared/animations/fade';
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({
selector: 'ds-community-page-sub-collection-list',
@@ -16,9 +20,60 @@ import { PaginatedList } from '../../core/data/paginated-list';
})
export class CommunityPageSubCollectionListComponent implements OnInit {
@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 {
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);
});
}
}

View File

@@ -1,14 +1,13 @@
<ng-container *ngVar="(subCommunitiesRDObs | async) as subCommunitiesRD">
<div *ngIf="subCommunitiesRD?.hasSucceeded && subCommunitiesRD?.payload.totalElements > 0" @fadeIn>
<h2>{{'community.sub-community-list.head' | translate}}</h2>
<ul>
<li *ngFor="let community of subCommunitiesRD?.payload.page">
<p>
<span class="lead"><a [routerLink]="['/communities', community.id]">{{community.name}}</a></span><br>
<span class="text-muted">{{community.shortDescription}}</span>
</p>
</li>
</ul>
<ds-viewable-collection
[config]="config"
[sortConfig]="sortConfig"
[objects]="subCommunitiesRD"
[hideGear]="false"
(paginationChange)="onPaginationChange($event)">
</ds-viewable-collection>
</div>
<ds-error *ngIf="subCommunitiesRD?.hasFailed" message="{{'error.sub-communities' | translate}}"></ds-error>
<ds-loading *ngIf="subCommunitiesRD?.isLoading" message="{{'loading.sub-communities' | translate}}"></ds-loading>

View File

@@ -1,21 +1,29 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {TranslateModule} from '@ngx-translate/core';
import {NO_ERRORS_SCHEMA} from '@angular/core';
import {CommunityPageSubCommunityListComponent} from './community-page-sub-community-list.component';
import {Community} from '../../core/shared/community.model';
import {RemoteData} from '../../core/data/remote-data';
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';
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';
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 fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
let communityDataServiceStub: any;
let subCommList = [];
const subcommunities = [Object.assign(new Community(), {
id: '123456789-1',
@@ -32,34 +40,92 @@ describe('SubCommunityList Component', () => {
{ 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: {
'dc.title': [
{ language: 'en_US', value: 'Test title' }
]
},
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), []))
}
});
const mockCommunity = Object.assign(new Community(), {
metadata: {
'dc.title': [
{ language: 'en_US', value: 'Test title' }
]
},
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subcommunities))
})
;
communityDataServiceStub = {
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 > subCommList.length) {
endPageIndex = subCommList.length;
}
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subCommList.slice(startPageIndex, endPageIndex)));
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), SharedModule,
imports: [
TranslateModule.forRoot(),
SharedModule,
RouterTestingModule.withRoutes([]),
NoopAnimationsModule],
NgbModule.forRoot(),
NoopAnimationsModule
],
declarations: [CommunityPageSubCommunityListComponent],
providers: [
{ provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: SelectableListService, useValue: {} },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
@@ -67,23 +133,52 @@ describe('SubCommunityList Component', () => {
beforeEach(() => {
fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent);
comp = fixture.componentInstance;
comp.community = mockCommunity;
});
it('should display a list of subCommunities', () => {
comp.community = mockCommunity;
it('should display a list of sub-communities', () => {
subCommList = subcommunities;
fixture.detectChanges();
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[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', () => {
comp.community = emptySubCommunitiesCommunity;
it('should not display the header when list of sub-communities is empty', () => {
subCommList = [];
fixture.detectChanges();
const subComHead = fixture.debugElement.queryAll(By.css('h2'));
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');
});
});

View File

@@ -1,26 +1,82 @@
import { Component, Input, OnInit } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { take } from 'rxjs/operators';
import { RemoteData } from '../../core/data/remote-data';
import { Community } from '../../core/shared/community.model';
import { fadeIn } from '../../shared/animations/fade';
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({
selector: 'ds-community-page-sub-community-list',
styleUrls: ['./community-page-sub-community-list.component.scss'],
templateUrl: './community-page-sub-community-list.component.html',
animations:[fadeIn]
animations: [fadeIn]
})
/**
* Component to render the sub-communities of a Community
*/
export class CommunityPageSubCommunityListComponent implements OnInit {
@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 {
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);
});
}
}

View File

@@ -2,12 +2,25 @@ import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { HomePageComponent } from './home-page.component';
import { HomePageResolver } from './home-page.resolver';
@NgModule({
imports: [
RouterModule.forChild([
{ path: '', component: HomePageComponent, pathMatch: 'full', data: { title: 'home.title' } }
{
path: '',
component: HomePageComponent,
pathMatch: 'full',
data: {title: 'home.title'},
resolve: {
site: HomePageResolver
}
}
])
],
providers: [
HomePageResolver
]
})
export class HomePageRoutingModule { }
export class HomePageRoutingModule {
}

View File

@@ -1,5 +1,8 @@
<ds-home-news></ds-home-news>
<div class="container">
<ng-container *ngIf="(site$ | async) as site">
<ds-view-tracker [object]="site"></ds-view-tracker>
</ng-container>
<ds-search-form [inPlaceSearch]="false"></ds-search-form>
<ds-top-level-community-list></ds-top-level-community-list>
</div>

View File

@@ -1,9 +1,26 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { map } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { Site } from '../core/shared/site.model';
@Component({
selector: 'ds-home-page',
styleUrls: ['./home-page.component.scss'],
templateUrl: './home-page.component.html'
})
export class HomePageComponent {
export class HomePageComponent implements OnInit {
site$: Observable<Site>;
constructor(
private route: ActivatedRoute,
) {
}
ngOnInit(): void {
this.site$ = this.route.data.pipe(
map((data) => data.site as Site),
);
}
}

View File

@@ -6,12 +6,14 @@ import { HomePageRoutingModule } from './home-page-routing.module';
import { HomePageComponent } from './home-page.component';
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
import { StatisticsModule } from '../statistics/statistics.module';
@NgModule({
imports: [
CommonModule,
SharedModule,
HomePageRoutingModule
HomePageRoutingModule,
StatisticsModule.forRoot()
],
declarations: [
HomePageComponent,

View File

@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { SiteDataService } from '../core/data/site-data.service';
import { Site } from '../core/shared/site.model';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
/**
* The class that resolve the Site object for a route
*/
@Injectable()
export class HomePageResolver implements Resolve<Site> {
constructor(private siteService: SiteDataService) {
}
/**
* Method for resolving a site object
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @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 {
return this.siteService.find().pipe(take(1));
}
}

View File

@@ -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');
});
});

View File

@@ -1,15 +1,15 @@
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 { CommunityDataService } from '../../core/data/community-data.service';
import { PaginatedList } from '../../core/data/paginated-list';
import { RemoteData } from '../../core/data/remote-data';
import { Community } from '../../core/shared/community.model';
import { fadeInOut } from '../../shared/animations/fade';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { take } from 'rxjs/operators';
/**
* this component renders the Top-Level Community list
@@ -33,6 +33,11 @@ export class TopLevelCommunityListComponent implements OnInit {
*/
config: PaginationComponentOptions;
/**
* The pagination id
*/
pageId = 'top-level-pagination';
/**
* The sorting configuration
*/
@@ -40,7 +45,7 @@ export class TopLevelCommunityListComponent implements OnInit {
constructor(private cds: CommunityDataService) {
this.config = new PaginationComponentOptions();
this.config.id = 'top-level-pagination';
this.config.id = this.pageId;
this.config.pageSize = 5;
this.config.currentPage = 1;
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
@@ -55,10 +60,10 @@ export class TopLevelCommunityListComponent implements OnInit {
* @param event The new pagination data
*/
onPaginationChange(event) {
this.config.currentPage = event.page;
this.config.pageSize = event.pageSize;
this.sortConfig.field = event.sortField;
this.sortConfig.direction = event.sortDirection;
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();
}

View File

@@ -1,15 +1,12 @@
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { CommonModule } from '@angular/common';
import { ItemCollectionMapperComponent } from './item-collection-mapper.component';
import { ActivatedRoute, Router } from '@angular/router';
import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service';
import { SearchService } from '../../../+search-page/search-service/search.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { ItemDataService } from '../../../core/data/item-data.service';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { RouterStub } from '../../../shared/testing/router-stub';
@@ -19,7 +16,6 @@ import { SearchServiceStub } from '../../../shared/testing/search-service-stub';
import { PaginatedList } from '../../../core/data/paginated-list';
import { PageInfo } from '../../../core/shared/page-info.model';
import { FormsModule } from '@angular/forms';
import { SharedModule } from '../../../shared/shared.module';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { HostWindowService } from '../../../shared/host-window.service';
@@ -28,7 +24,6 @@ import { By } from '@angular/platform-browser';
import { Item } from '../../../core/shared/item.model';
import { ObjectSelectService } from '../../../shared/object-select/object-select.service';
import { ObjectSelectServiceStub } from '../../../shared/testing/object-select-service-stub';
import { Observable } from 'rxjs/internal/Observable';
import { of } from 'rxjs/internal/observable/of';
import { RestResponse } from '../../../core/cache/response.models';
import { CollectionSelectComponent } from '../../../shared/object-select/collection-select/collection-select.component';
@@ -39,6 +34,9 @@ import { SearchFormComponent } from '../../../shared/search-form/search-form.com
import { Collection } from '../../../core/shared/collection.model';
import { ErrorComponent } from '../../../shared/error/error.component';
import { LoadingComponent } from '../../../shared/loading/loading.component';
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { SearchService } from '../../../core/shared/search/search.service';
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
describe('ItemCollectionMapperComponent', () => {
let comp: ItemCollectionMapperComponent;

View File

@@ -2,15 +2,12 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core';
import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
import { Collection } from '../../../core/shared/collection.model';
import { Item } from '../../../core/shared/item.model';
import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { SearchService } from '../../../+search-page/search-service/search.service';
import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service';
import { map, startWith, switchMap, take } from 'rxjs/operators';
import { ItemDataService } from '../../../core/data/item-data.service';
import { TranslateService } from '@ngx-translate/core';
@@ -19,6 +16,9 @@ import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'
import { isNotEmpty } from '../../../shared/empty.util';
import { RestResponse } from '../../../core/cache/response.models';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { SearchService } from '../../../core/shared/search/search.service';
@Component({
selector: 'ds-item-collection-mapper',

View File

@@ -9,7 +9,6 @@ import { ActivatedRoute, Router } from '@angular/router';
import { ItemMoveComponent } from './item-move.component';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { SearchService } from '../../../+search-page/search-service/search.service';
import { of as observableOf } from 'rxjs';
import { FormsModule } from '@angular/forms';
import { ItemDataService } from '../../../core/data/item-data.service';
@@ -18,6 +17,7 @@ import { PaginatedList } from '../../../core/data/paginated-list';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RestResponse } from '../../../core/cache/response.models';
import { Collection } from '../../../core/shared/collection.model';
import { SearchService } from '../../../core/shared/search/search.service';
describe('ItemMoveComponent', () => {
let comp: ItemMoveComponent;

View File

@@ -1,12 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { SearchService } from '../../../+search-page/search-service/search.service';
import { first, map } from 'rxjs/operators';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { SearchOptions } from '../../../+search-page/search-options.model';
import { RemoteData } from '../../../core/data/remote-data';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { PaginatedList } from '../../../core/data/paginated-list';
import { SearchResult } from '../../../+search-page/search-result.model';
import { Item } from '../../../core/shared/item.model';
import { ActivatedRoute, Router } from '@angular/router';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
@@ -17,9 +14,10 @@ import { getItemEditPath } from '../../item-page-routing.module';
import { Observable, of as observableOf } from 'rxjs';
import { RestResponse } from '../../../core/cache/response.models';
import { Collection } from '../../../core/shared/collection.model';
import { tap } from 'rxjs/internal/operators/tap';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model';
import { SearchService } from '../../../core/shared/search/search.service';
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
import { SearchResult } from '../../../shared/search/search-result.model';
@Component({
selector: 'ds-item-move',

View File

@@ -70,5 +70,4 @@ export class EditRelationshipComponent implements OnChanges {
canUndo(): boolean {
return this.fieldUpdate.changeType >= 0;
}
}

View File

@@ -157,7 +157,9 @@ describe('ItemRelationshipsComponent', () => {
getRelatedItemsByLabel: observableOf([author1, author2]),
getItemRelationshipsArray: observableOf(relationships),
deleteRelationship: observableOf(new RestResponse(true, 200, 'OK')),
getItemResolvedRelatedItemsAndRelationships: observableCombineLatest(observableOf([author1, author2]), observableOf([item, item]), observableOf(relationships))
getItemResolvedRelatedItemsAndRelationships: observableCombineLatest(observableOf([author1, author2]), observableOf([item, item]), observableOf(relationships)),
getRelationshipsByRelatedItemIds: observableOf(relationships),
getRelationshipTypeLabelsByItem: observableOf([relationshipType.leftwardType])
}
);

View File

@@ -2,8 +2,8 @@ import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core';
import { Item } from '../../../core/shared/item.model';
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
import { Observable } from 'rxjs/internal/Observable';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs';
import { filter, flatMap, map, switchMap, take, tap } from 'rxjs/operators';
import { zip as observableZip } from 'rxjs';
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
import { ItemDataService } from '../../../core/data/item-data.service';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
@@ -21,7 +21,6 @@ import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { getSucceededRemoteData } from '../../../core/shared/operators';
import { RequestService } from '../../../core/data/request.service';
import { Subscription } from 'rxjs/internal/Subscription';
import { getRelationsByRelatedItemIds } from '../../simple/item-types/shared/item-relationships-utils';
@Component({
selector: 'ds-item-relationships',
@@ -65,7 +64,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
*/
ngOnInit(): void {
super.ngOnInit();
this.relationLabels$ = this.relationshipService.getItemRelationshipLabels(this.item);
this.relationLabels$ = this.relationshipService.getRelationshipTypeLabelsByItem(this.item);
this.initializeItemUpdate();
}
@@ -113,8 +112,9 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
);
// Get all the relationships that should be removed
const removedRelationships$ = removedItemIds$.pipe(
getRelationsByRelatedItemIds(this.item, this.relationshipService)
flatMap((uuids) => this.relationshipService.getRelationshipsByRelatedItemIds(this.item, uuids))
);
// const removedRelationships$ = removedItemIds$.pipe(flatMap((uuids: string[]) => this.relationshipService.getRelationshipsByRelatedItemIds(this.item, uuids)));
// Request a delete for every relationship found in the observable created above
removedRelationships$.pipe(
take(1),

View File

@@ -1,6 +1,7 @@
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
<div *ngIf="itemRD?.payload as item">
<ds-view-tracker [object]="item"></ds-view-tracker>
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
<div class="simple-view-link my-3">
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id]">

View File

@@ -26,6 +26,9 @@ import { MetadataRepresentationListComponent } from './simple/metadata-represent
import { RelatedEntitiesSearchComponent } from './simple/related-entities/related-entities-search/related-entities-search.component';
import { MetadataValuesComponent } from './field-components/metadata-values/metadata-values.component';
import { MetadataFieldWrapperComponent } from './field-components/metadata-field-wrapper/metadata-field-wrapper.component';
import { TabbedRelatedEntitiesSearchComponent } from './simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component';
import { StatisticsModule } from '../statistics/statistics.module';
import { AbstractIncrementalListComponent } from './simple/abstract-incremental-list/abstract-incremental-list.component';
@NgModule({
imports: [
@@ -33,7 +36,8 @@ import { MetadataFieldWrapperComponent } from './field-components/metadata-field
SharedModule,
ItemPageRoutingModule,
EditItemPageModule,
SearchPageModule
SearchPageModule,
StatisticsModule.forRoot()
],
declarations: [
ItemPageComponent,
@@ -53,7 +57,9 @@ import { MetadataFieldWrapperComponent } from './field-components/metadata-field
ItemComponent,
GenericItemPageFieldComponent,
MetadataRepresentationListComponent,
RelatedEntitiesSearchComponent
RelatedEntitiesSearchComponent,
TabbedRelatedEntitiesSearchComponent,
AbstractIncrementalListComponent
],
exports: [
ItemComponent,
@@ -63,7 +69,8 @@ import { MetadataFieldWrapperComponent } from './field-components/metadata-field
RelatedEntitiesSearchComponent,
RelatedItemsComponent,
MetadataRepresentationListComponent,
ItemPageTitleFieldComponent
ItemPageTitleFieldComponent,
TabbedRelatedEntitiesSearchComponent
],
entryComponents: [
PublicationComponent

View File

@@ -0,0 +1,73 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs/internal/Subscription';
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
@Component({
selector: 'ds-abstract-incremental-list',
template: ``,
})
/**
* An abstract component for displaying an incremental list of objects
*/
export class AbstractIncrementalListComponent<T> implements OnInit, OnDestroy {
/**
* The amount to increment the list by
* Define this amount in the child component overriding this component
*/
incrementBy: number;
/**
* All pages of objects to display as an array
*/
objects: T[];
/**
* A list of open subscriptions
*/
subscriptions: Subscription[];
ngOnInit(): void {
this.objects = [];
this.subscriptions = [];
this.increase();
}
/**
* Get a specific page
* > Override this method to return a specific page
* @param page The page to fetch
*/
getPage(page: number): T {
return undefined;
}
/**
* Increase the amount displayed
*/
increase() {
const page = this.getPage(this.objects.length + 1);
if (hasValue(page)) {
this.objects.push(page);
}
}
/**
* Decrease the amount displayed
*/
decrease() {
if (this.objects.length > 1) {
this.objects.pop();
}
}
/**
* Unsubscribe from any open subscriptions
*/
ngOnDestroy(): void {
if (isNotEmpty(this.subscriptions)) {
this.subscriptions.forEach((sub: Subscription) => {
sub.unsubscribe();
});
}
}
}

View File

@@ -1,6 +1,7 @@
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
<div *ngIf="itemRD?.payload as item">
<ds-view-tracker [object]="item"></ds-view-tracker>
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
</div>
</div>

View File

@@ -4,7 +4,6 @@ import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loa
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
import { ItemDataService } from '../../../../core/data/item-data.service';
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { Item } from '../../../../core/shared/item.model';
@@ -15,6 +14,7 @@ import { createRelationshipsObservable } from '../shared/item.component.spec';
import { PublicationComponent } from './publication.component';
import { MetadataMap } from '../../../../core/shared/metadata.models';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
import { RelationshipService } from '../../../../core/data/relationship.service';
const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
@@ -26,12 +26,6 @@ describe('PublicationComponent', () => {
let comp: PublicationComponent;
let fixture: ComponentFixture<PublicationComponent>;
const searchFixedFilterServiceStub = {
/* tslint:disable:no-empty */
getQueryByRelations: () => {}
/* tslint:enable:no-empty */
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
@@ -43,8 +37,8 @@ describe('PublicationComponent', () => {
declarations: [PublicationComponent, GenericItemPageFieldComponent, TruncatePipe],
providers: [
{provide: ItemDataService, useValue: {}},
{provide: SearchFixedFilterService, useValue: searchFixedFilterServiceStub},
{provide: TruncatableService, useValue: {}}
{provide: TruncatableService, useValue: {}},
{provide: RelationshipService, useValue: {}}
],
schemas: [NO_ERRORS_SCHEMA]

View File

@@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ItemComponent } from '../shared/item.component';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Item } from '../../../../core/shared/item.model';
import { ItemComponent } from '../shared/item.component';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
/**
* Component that represents a publication Item page

View File

@@ -1,14 +1,12 @@
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasNoValue, hasValue } from '../../../../shared/empty.util';
import { getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasValue } from '../../../../shared/empty.util';
import { Observable } from 'rxjs/internal/Observable';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { distinctUntilChanged, flatMap, map, switchMap } from 'rxjs/operators';
import { zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs';
import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs';
import { Item } from '../../../../core/shared/item.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { RelationshipService } from '../../../../core/data/relationship.service';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { RemoteData } from '../../../../core/data/remote-data';
/**
* Operator for comparing arrays using a mapping function
@@ -37,36 +35,6 @@ export const compareArraysUsing = <T>(mapFn: (t: T) => any) =>
export const compareArraysUsingIds = <T extends { id: string }>() =>
compareArraysUsing((t: T) => hasValue(t) ? t.id : undefined);
/**
* Fetch the relationships which match the type label given
* @param {string} label Type label
* @param thisId The item's id of which the relations belong to
* @returns {(source: Observable<[Relationship[] , RelationshipType[]]>) => Observable<Relationship[]>}
*/
export const filterRelationsByTypeLabel = (label: string, thisId?: string) =>
(source: Observable<[Relationship[], RelationshipType[]]>): Observable<Relationship[]> =>
source.pipe(
switchMap(([relsCurrentPage, relTypesCurrentPage]) => {
const relatedItems$ = observableZip(...relsCurrentPage.map((rel: Relationship) =>
observableCombineLatest(
rel.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
rel.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()))
)
);
return relatedItems$.pipe(
map((arr) => relsCurrentPage.filter((rel: Relationship, idx: number) =>
hasValue(relTypesCurrentPage[idx]) && (
(hasNoValue(thisId) && (relTypesCurrentPage[idx].leftwardType === label ||
relTypesCurrentPage[idx].rightwardType === label)) ||
(thisId === arr[idx][0].id && relTypesCurrentPage[idx].leftwardType === label) ||
(thisId === arr[idx][1].id && relTypesCurrentPage[idx].rightwardType === label)
)
))
);
}),
distinctUntilChanged(compareArraysUsingIds())
);
/**
* Operator for turning a list of relationships into a list of the relevant items
* @param {string} thisId The item's id of which the relations belong to
@@ -128,17 +96,3 @@ export const paginatedRelationsToItems = (thisId: string) =>
)
})
);
/**
* Operator for fetching an item's relationships, but filtered by related item IDs (essentially performing a reverse lookup)
* Only relationships where leftItem or rightItem's ID is present in the list provided will be returned
* @param item
* @param relationshipService
*/
export const getRelationsByRelatedItemIds = (item: Item, relationshipService: RelationshipService) =>
(source: Observable<string[]>): Observable<Relationship[]> =>
source.pipe(
flatMap((relatedItemIds: string[]) => relationshipService.getItemResolvedRelatedItemsAndRelationships(item).pipe(
map(([leftItems, rightItems, rels]) => rels.filter((rel: Relationship, index: number) => relatedItemIds.indexOf(leftItems[index].uuid) > -1 || relatedItemIds.indexOf(rightItems[index].uuid) > -1))
))
);

View File

@@ -9,7 +9,6 @@ import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loa
import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
import { isNotEmpty } from '../../../../shared/empty.util';
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
@@ -24,6 +23,7 @@ import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-rep
import { MetadataMap, MetadataValue } from '../../../../core/shared/metadata.models';
import { compareArraysUsing, compareArraysUsingIds } from './item-relationships-utils';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
import { RelationshipService } from '../../../../core/data/relationship.service';
/**
* Create a generic test for an item-page-fields component using a mockItem and the type of component
@@ -37,12 +37,6 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
let comp: any;
let fixture: ComponentFixture<any>;
const searchFixedFilterServiceStub = {
/* tslint:disable:no-empty */
getQueryByRelations: () => {}
/* tslint:enable:no-empty */
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
@@ -54,8 +48,8 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
declarations: [component, GenericItemPageFieldComponent, TruncatePipe],
providers: [
{provide: ItemDataService, useValue: {}},
{provide: SearchFixedFilterService, useValue: searchFixedFilterServiceStub},
{provide: TruncatableService, useValue: {}}
{provide: TruncatableService, useValue: {}},
{provide: RelationshipService, useValue: {}}
],
schemas: [NO_ERRORS_SCHEMA]

View File

@@ -1,4 +1,4 @@
import { Component, Inject, Input } from '@angular/core';
import { Component, Input } from '@angular/core';
import { Item } from '../../../../core/shared/item.model';
@Component({

View File

@@ -1,11 +1,20 @@
<ds-metadata-field-wrapper *ngIf="representations$ && (representations$ | async)?.length > 0" [label]="label">
<ds-metadata-representation-loader *ngFor="let rep of (representations$ | async)"
[mdRepresentation]="rep">
</ds-metadata-representation-loader>
<div *ngIf="(representations$ | async)?.length < total" class="mt-2">
<a [routerLink]="" (click)="viewMore()">{{'item.page.related-items.view-more' | translate}}</a>
</div>
<div *ngIf="limit > originalLimit" class="mt-2">
<a [routerLink]="" (click)="viewLess()">{{'item.page.related-items.view-less' | translate}}</a>
</div>
<ds-metadata-field-wrapper [label]="label">
<ng-container *ngFor="let objectPage of objects; let i = index">
<ng-container *ngVar="(objectPage | async) as representations">
<ds-metadata-representation-loader *ngFor="let rep of representations"
[mdRepresentation]="rep">
</ds-metadata-representation-loader>
<ds-loading *ngIf="(i + 1) === objects.length && (i > 0) && (!representations || representations?.length === 0)" message="{{'loading.default' | translate}}"></ds-loading>
<div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && representations?.length > 0">
<div *ngIf="(objects.length * incrementBy) < total" class="float-left">
<a [routerLink]="" (click)="increase()">{{'item.page.related-items.view-more' |
translate:{ amount: (total - (objects.length * incrementBy) < incrementBy) ? total - (objects.length * incrementBy) : incrementBy } }}</a>
</div>
<div *ngIf="objects.length > 1" class="float-right">
<a [routerLink]="" (click)="decrease()">{{'item.page.related-items.view-less' |
translate:{ amount: representations?.length } }}</a>
</div>
</div>
</ng-container>
</ng-container>
</ds-metadata-field-wrapper>

View File

@@ -7,6 +7,8 @@ import { Item } from '../../../core/shared/item.model';
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { TranslateModule } from '@ngx-translate/core';
import { VarDirective } from '../../../shared/utils/var.directive';
import { of as observableOf } from 'rxjs';
const itemType = 'Person';
const metadataField = 'dc.contributor.author';
@@ -64,7 +66,7 @@ describe('MetadataRepresentationListComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [MetadataRepresentationListComponent],
declarations: [MetadataRepresentationListComponent, VarDirective],
providers: [
{ provide: RelationshipService, useValue: relationshipService }
],
@@ -88,33 +90,29 @@ describe('MetadataRepresentationListComponent', () => {
expect(fields.length).toBe(2);
});
it('should initialize the original limit', () => {
expect(comp.originalLimit).toEqual(comp.limit);
it('should contain one page of items', () => {
expect(comp.objects.length).toEqual(1);
});
describe('when viewMore is called', () => {
describe('when increase is called', () => {
beforeEach(() => {
comp.viewMore();
comp.increase();
});
it('should set the limit to a high number in order to retrieve all metadata representations', () => {
expect(comp.limit).toBeGreaterThanOrEqual(999);
it('should add a new page to the list', () => {
expect(comp.objects.length).toEqual(2);
});
});
describe('when viewLess is called', () => {
let originalLimit;
describe('when decrease is called', () => {
beforeEach(() => {
// Store the original value of limit
originalLimit = comp.limit;
// Set limit to a random number
comp.limit = 458;
comp.viewLess();
// Add a second page
comp.objects.push(observableOf(undefined));
comp.decrease();
});
it('should reset the limit to the original value', () => {
expect(comp.limit).toEqual(originalLimit);
it('should decrease the list of pages', () => {
expect(comp.objects.length).toEqual(1);
});
});

View File

@@ -1,16 +1,16 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input } from '@angular/core';
import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../../core/data/remote-data';
import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs';
import { RelationshipService } from '../../../core/data/relationship.service';
import { Item } from '../../../core/shared/item.model';
import { combineLatest as observableCombineLatest, of as observableOf, zip as observableZip } from 'rxjs';
import { MetadataValue } from '../../../core/shared/metadata.models';
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { filter, map, switchMap } from 'rxjs/operators';
import { getSucceededRemoteData } from '../../../core/shared/operators';
import { filter, map, switchMap } from 'rxjs/operators';
import { RemoteData } from '../../../core/data/remote-data';
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { Item } from '../../../core/shared/item.model';
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { AbstractIncrementalListComponent } from '../abstract-incremental-list/abstract-incremental-list.component';
@Component({
selector: 'ds-metadata-representation-list',
@@ -22,7 +22,7 @@ import { ItemMetadataRepresentation } from '../../../core/shared/metadata-repres
* It expects an itemType to resolve the metadata to a an item
* It expects a label to put on top of the list
*/
export class MetadataRepresentationListComponent implements OnInit {
export class MetadataRepresentationListComponent extends AbstractIncrementalListComponent<Observable<MetadataRepresentation[]>> {
/**
* The parent of the list of related items to display
*/
@@ -44,22 +44,11 @@ export class MetadataRepresentationListComponent implements OnInit {
@Input() label: string;
/**
* The max amount of representations to display
* The amount to increment the list by when clicking "view more"
* Defaults to 10
* The default can optionally be overridden by providing the limit as input to the component
*/
@Input() limit = 10;
/**
* A list of metadata-representations to display
*/
representations$: Observable<MetadataRepresentation[]>;
/**
* The originally provided limit
* Used for resetting the limit to the original value when collapsing the list
*/
originalLimit: number;
@Input() incrementBy = 10;
/**
* The total amount of metadata values available
@@ -67,30 +56,28 @@ export class MetadataRepresentationListComponent implements OnInit {
total: number;
constructor(public relationshipService: RelationshipService) {
}
ngOnInit(): void {
this.originalLimit = this.limit;
this.setRepresentations();
super();
}
/**
* Initialize the metadata representations
* Get a specific page
* @param page The page to fetch
*/
setRepresentations() {
getPage(page: number): Observable<MetadataRepresentation[]> {
const metadata = this.parentItem.findMetadataSortedByPlace(this.metadataField);
this.total = metadata.length;
this.representations$ = this.resolveMetadataRepresentations(metadata);
return this.resolveMetadataRepresentations(metadata, page);
}
/**
* Resolve a list of metadata values to a list of metadata representations
* @param metadata
* @param metadata The list of all metadata values
* @param page The page to return representations for
*/
resolveMetadataRepresentations(metadata: MetadataValue[]): Observable<MetadataRepresentation[]> {
resolveMetadataRepresentations(metadata: MetadataValue[], page: number): Observable<MetadataRepresentation[]> {
return observableZip(
...metadata
.slice(0, this.limit)
.slice((this.objects.length * this.incrementBy), (this.objects.length * this.incrementBy) + this.incrementBy)
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
.map((metadatum: MetadataValue) => {
if (metadatum.isVirtual) {
@@ -115,20 +102,4 @@ export class MetadataRepresentationListComponent implements OnInit {
})
);
}
/**
* Expand the list to display all metadata representations
*/
viewMore() {
this.limit = 9999;
this.setRepresentations();
}
/**
* Collapse the list to display the originally displayed metadata representations
*/
viewLess() {
this.limit = this.originalLimit;
this.setRepresentations();
}
}

View File

@@ -1,6 +1,7 @@
<ds-filtered-search-page
<ds-configuration-search-page
[fixedFilterQuery]="fixedFilter"
[configuration]="configuration"
[configuration$]="configuration$"
[searchEnabled]="searchEnabled"
[sideBarWidth]="sideBarWidth">
</ds-filtered-search-page>
</ds-configuration-search-page>

View File

@@ -4,31 +4,23 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { Item } from '../../../../core/shared/item.model';
describe('RelatedEntitiesSearchComponent', () => {
let comp: RelatedEntitiesSearchComponent;
let fixture: ComponentFixture<RelatedEntitiesSearchComponent>;
let fixedFilterService: SearchFixedFilterService;
const mockItem = Object.assign(new Item(), {
id: 'id1'
});
const mockRelationType = 'publicationsOfAuthor';
const mockRelationEntityType = 'publication';
const mockConfiguration = 'publication';
const mockFilter= `f.${mockRelationType}=${mockItem.id}`;
const fixedFilterServiceStub = {
getFilterByRelation: () => mockFilter
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
declarations: [RelatedEntitiesSearchComponent],
providers: [
{ provide: SearchFixedFilterService, useValue: fixedFilterServiceStub }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
@@ -36,10 +28,9 @@ describe('RelatedEntitiesSearchComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(RelatedEntitiesSearchComponent);
comp = fixture.componentInstance;
fixedFilterService = (comp as any).fixedFilterService;
comp.relationType = mockRelationType;
comp.item = mockItem;
comp.relationEntityType = mockRelationEntityType;
comp.configuration = mockConfiguration;
fixture.detectChanges();
});
@@ -49,7 +40,7 @@ describe('RelatedEntitiesSearchComponent', () => {
it('should create a configuration$', () => {
comp.configuration$.subscribe((configuration) => {
expect(configuration).toEqual(mockRelationEntityType);
expect(configuration).toEqual(mockConfiguration);
})
});

View File

@@ -1,9 +1,9 @@
import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Item } from '../../../../core/shared/item.model';
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { isNotEmpty } from '../../../../shared/empty.util';
import { of } from 'rxjs/internal/observable/of';
import { getFilterByRelation } from '../../../../shared/utils/relation-query.utils';
@Component({
selector: 'ds-related-entities-search',
@@ -22,18 +22,16 @@ export class RelatedEntitiesSearchComponent implements OnInit {
*/
@Input() relationType: string;
/**
* An optional configuration to use for the search options
*/
@Input() configuration: string;
/**
* The item to render relationships for
*/
@Input() item: Item;
/**
* The entity type of the relationship items to be displayed
* e.g. 'publication'
* This determines the title of the search results (if search is enabled)
*/
@Input() relationEntityType: string;
/**
* Whether or not the search bar and title should be displayed (defaults to true)
* @type {boolean}
@@ -49,15 +47,12 @@ export class RelatedEntitiesSearchComponent implements OnInit {
fixedFilter: string;
configuration$: Observable<string>;
constructor(private fixedFilterService: SearchFixedFilterService) {
}
ngOnInit(): void {
if (isNotEmpty(this.relationType) && isNotEmpty(this.item)) {
this.fixedFilter = this.fixedFilterService.getFilterByRelation(this.relationType, this.item.id);
this.fixedFilter = getFilterByRelation(this.relationType, this.item.id);
}
if (isNotEmpty(this.relationEntityType)) {
this.configuration$ = of(this.relationEntityType);
if (isNotEmpty(this.configuration)) {
this.configuration$ = of(this.configuration);
}
}

View File

@@ -0,0 +1,22 @@
<ngb-tabset *ngIf="relationTypes.length > 1" [destroyOnHide]="true" #tabs="ngbTabset" [activeId]="activeTab$ | async" (tabChange)="onTabChange($event)">
<ngb-tab *ngFor="let relationType of relationTypes" title="{{'item.page.relationships.' + relationType.label | translate}}" [id]="relationType.filter">
<ng-template ngbTabContent>
<div class="mt-4">
<ds-related-entities-search [item]="item"
[relationType]="relationType.filter"
[configuration]="relationType.configuration"
[searchEnabled]="searchEnabled"
[sideBarWidth]="sideBarWidth">
</ds-related-entities-search>
</div>
</ng-template>
</ngb-tab>
</ngb-tabset>
<div *ngIf="relationTypes.length === 1" class="mt-4">
<ds-related-entities-search *ngVar="relationTypes[0] as relationType" [item]="item"
[relationType]="relationType.filter"
[configuration]="relationType.configuration"
[searchEnabled]="searchEnabled"
[sideBarWidth]="sideBarWidth">
</ds-related-entities-search>
</div>

View File

@@ -0,0 +1,82 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Item } from '../../../../core/shared/item.model';
import { TranslateModule } from '@ngx-translate/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TabbedRelatedEntitiesSearchComponent } from './tabbed-related-entities-search.component';
import { ActivatedRoute, Router } from '@angular/router';
import { MockRouter } from '../../../../shared/mocks/mock-router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { VarDirective } from '../../../../shared/utils/var.directive';
import { of as observableOf } from 'rxjs';
describe('TabbedRelatedEntitiesSearchComponent', () => {
let comp: TabbedRelatedEntitiesSearchComponent;
let fixture: ComponentFixture<TabbedRelatedEntitiesSearchComponent>;
const mockItem = Object.assign(new Item(), {
id: 'id1'
});
const mockRelationType = 'publications';
const relationTypes = [
{
label: mockRelationType,
filter: mockRelationType
}
];
const router = new MockRouter();
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, NgbModule.forRoot()],
declarations: [TabbedRelatedEntitiesSearchComponent, VarDirective],
providers: [
{
provide: ActivatedRoute,
useValue: {
queryParams: observableOf({ tab: mockRelationType })
},
},
{ provide: Router, useValue: router }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TabbedRelatedEntitiesSearchComponent);
comp = fixture.componentInstance;
comp.item = mockItem;
comp.relationTypes = relationTypes;
fixture.detectChanges();
});
it('should initialize the activeTab depending on the current query parameters', () => {
comp.activeTab$.subscribe((activeTab) => {
expect(activeTab).toEqual(mockRelationType);
});
});
describe('onTabChange', () => {
const event = {
currentId: mockRelationType,
nextId: 'nextTab'
};
beforeEach(() => {
comp.onTabChange(event);
});
it('should call router natigate with the correct arguments', () => {
expect(router.navigate).toHaveBeenCalledWith([], {
relativeTo: (comp as any).route,
queryParams: {
tab: event.nextId
},
queryParamsHandling: 'merge'
});
});
});
});

View File

@@ -0,0 +1,76 @@
import { Component, Input, OnInit } from '@angular/core';
import { Item } from '../../../../core/shared/item.model';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs/internal/Observable';
import { map } from 'rxjs/operators';
@Component({
selector: 'ds-tabbed-related-entities-search',
templateUrl: './tabbed-related-entities-search.component.html'
})
/**
* A component to show related items as search results, split into tabs by relationship-type
* Related items can be facetted, or queried using an
* optional search box.
*/
export class TabbedRelatedEntitiesSearchComponent implements OnInit {
/**
* The types of relationships to fetch items for
* e.g. 'isAuthorOfPublication'
*/
@Input() relationTypes: Array<{
label: string,
filter: string,
configuration?: string
}>;
/**
* The item to render relationships for
*/
@Input() item: Item;
/**
* Whether or not the search bar and title should be displayed (defaults to true)
* @type {boolean}
*/
@Input() searchEnabled = true;
/**
* The ratio of the sidebar's width compared to the search results (1-12) (defaults to 4)
* @type {number}
*/
@Input() sideBarWidth = 4;
/**
* The active tab
*/
activeTab$: Observable<string>;
constructor(private route: ActivatedRoute,
private router: Router) {
}
/**
* If the url contains a "tab" query parameter, set this tab to be the active tab
*/
ngOnInit(): void {
this.activeTab$ = this.route.queryParams.pipe(
map((params) => params.tab)
);
}
/**
* Add a "tab" query parameter to the URL when changing tabs
* @param event
*/
onTabChange(event) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: {
tab: event.nextId
},
queryParamsHandling: 'merge'
});
}
}

View File

@@ -1,12 +1,12 @@
import { Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
import { Component, Input } from '@angular/core';
import { Item } from '../../../core/shared/item.model';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
import { RelationshipService } from '../../../core/data/relationship.service';
import { FindAllOptions } from '../../../core/data/request.models';
import { Subscription } from 'rxjs/internal/Subscription';
import { FindListOptions } from '../../../core/data/request.models';
import { ViewMode } from '../../../core/shared/view-mode.model';
import { RelationshipService } from '../../../core/data/relationship.service';
import { AbstractIncrementalListComponent } from '../abstract-incremental-list/abstract-incremental-list.component';
@Component({
selector: 'ds-related-items',
@@ -17,7 +17,7 @@ import { ViewMode } from '../../../core/shared/view-mode.model';
* This component is used for displaying relations between items
* It expects a parent item and relationship type, as well as a label to display on top
*/
export class RelatedItemsComponent implements OnInit, OnDestroy {
export class RelatedItemsComponent extends AbstractIncrementalListComponent<Observable<RemoteData<PaginatedList<Item>>>> {
/**
* The parent of the list of related items to display
*/
@@ -30,79 +30,38 @@ export class RelatedItemsComponent implements OnInit, OnDestroy {
@Input() relationType: string;
/**
* Default options to start a search request with
* Optional input, should you wish a different page size (or other options)
* The amount to increment the list by when clicking "view more"
* Defaults to 5
* The default can optionally be overridden by providing the limit as input to the component
*/
@Input() options = Object.assign(new FindAllOptions(), { elementsPerPage: 5 });
@Input() incrementBy = 5;
/**
* Default options to start a search request with
* Optional input
*/
@Input() options = new FindListOptions();
/**
* An i18n label to use as a title for the list (usually describes the relation)
*/
@Input() label: string;
/**
* Completely hide the component until there's at least one item visible
*/
@HostBinding('class.d-none') hidden = true;
/**
* The list of related items
*/
items$: Observable<RemoteData<PaginatedList<Item>>>;
/**
* Search options for displaying all elements in a list
*/
allOptions = Object.assign(new FindAllOptions(), { elementsPerPage: 9999 });
/**
* The view-mode we're currently on
* @type {ElementViewMode}
* @type {ViewMode}
*/
viewMode = ViewMode.ListElement;
/**
* Whether or not the list is currently expanded to show all related items
*/
showingAll = false;
/**
* Subscription on items used to update the "hidden" property of this component
*/
itemSub: Subscription;
constructor(public relationshipService: RelationshipService) {
}
ngOnInit(): void {
this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.options);
this.itemSub = this.items$.subscribe((itemsRD: RemoteData<PaginatedList<Item>>) => {
this.hidden = !(itemsRD.hasSucceeded && itemsRD.payload && itemsRD.payload.page.length > 0);
});
super();
}
/**
* Expand the list to display all related items
* Get a specific page
* @param page The page to fetch
*/
viewMore() {
this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.allOptions);
this.showingAll = true;
}
/**
* Collapse the list to display the originally displayed items
*/
viewLess() {
this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.options);
this.showingAll = false;
}
/**
* Unsubscribe from the item subscription
*/
ngOnDestroy(): void {
if (this.itemSub) {
this.itemSub.unsubscribe();
}
getPage(page: number): Observable<RemoteData<PaginatedList<Item>>> {
return this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, Object.assign(this.options, { elementsPerPage: this.incrementBy, currentPage: page }));
}
}

View File

@@ -1,11 +1,20 @@
<ds-metadata-field-wrapper *ngIf="(items$ | async)?.payload?.page?.length > 0" [label]="label">
<ds-listable-object-component-loader *ngFor="let item of (items$ | async)?.payload?.page"
[object]="item" [viewMode]="viewMode">
</ds-listable-object-component-loader>
<div *ngIf="(items$ | async)?.payload?.page?.length < (items$ | async)?.payload?.totalElements" class="mt-2" id="view-more">
<a [routerLink]="" (click)="viewMore()">{{'item.page.related-items.view-more' | translate}}</a>
</div>
<div *ngIf="showingAll" class="mt-2" id="view-less">
<a [routerLink]="" (click)="viewLess()">{{'item.page.related-items.view-less' | translate}}</a>
</div>
<ds-metadata-field-wrapper [label]="label">
<ng-container *ngFor="let objectPage of objects; let i = index">
<ng-container *ngVar="(objectPage | async) as itemsRD">
<ds-listable-object-component-loader *ngFor="let item of itemsRD?.payload?.page"
[object]="item" [viewMode]="viewMode">
</ds-listable-object-component-loader>
<ds-loading *ngIf="(i + 1) === objects.length && (itemsRD || i > 0) && !(itemsRD?.hasSucceeded && itemsRD?.payload && itemsRD?.payload?.page?.length > 0)" message="{{'loading.default' | translate}}"></ds-loading>
<div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && itemsRD?.payload?.page?.length > 0">
<div *ngIf="itemsRD?.payload?.totalPages > objects.length" class="float-left" id="view-more">
<a [routerLink]="" (click)="increase()">{{'item.page.related-items.view-more' |
translate:{ amount: (itemsRD?.payload?.totalElements - (incrementBy * objects.length) < incrementBy) ? itemsRD?.payload?.totalElements - (incrementBy * objects.length) : incrementBy } }}</a>
</div>
<div *ngIf="objects.length > 1" class="float-right" id="view-less">
<a [routerLink]="" (click)="decrease()">{{'item.page.related-items.view-less' |
translate:{ amount: itemsRD?.payload?.page?.length } }}</a>
</div>
</div>
</ng-container>
</ng-container>
</ds-metadata-field-wrapper>

View File

@@ -9,6 +9,8 @@ import { createRelationshipsObservable } from '../item-types/shared/item.compone
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { RelationshipService } from '../../../core/data/relationship.service';
import { TranslateModule } from '@ngx-translate/core';
import { VarDirective } from '../../../shared/utils/var.directive';
import { of as observableOf } from 'rxjs';
const parentItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
@@ -42,7 +44,7 @@ describe('RelatedItemsComponent', () => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [RelatedItemsComponent],
declarations: [RelatedItemsComponent, VarDirective],
providers: [
{ provide: RelationshipService, useValue: relationshipService }
],
@@ -65,31 +67,33 @@ describe('RelatedItemsComponent', () => {
expect(fields.length).toBe(mockItems.length);
});
describe('when viewMore is called', () => {
it('should contain one page of items', () => {
expect(comp.objects.length).toEqual(1);
});
describe('when increase is called', () => {
beforeEach(() => {
comp.viewMore();
comp.increase();
});
it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments', () => {
expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, comp.allOptions);
it('should add a new page to the list', () => {
expect(comp.objects.length).toEqual(2);
});
it('should set showingAll to true', () => {
expect(comp.showingAll).toEqual(true);
it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments (second page)', () => {
expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, Object.assign(comp.options, { elementsPerPage: comp.incrementBy, currentPage: 2 }));
});
});
describe('when viewLess is called', () => {
describe('when decrease is called', () => {
beforeEach(() => {
comp.viewLess();
// Add a second page
comp.objects.push(observableOf(undefined));
comp.decrease();
});
it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments', () => {
expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, comp.options);
});
it('should set showingAll to false', () => {
expect(comp.showingAll).toEqual(false);
it('should decrease the list of pages', () => {
expect(comp.objects.length).toEqual(1);
});
});

View File

@@ -18,7 +18,7 @@ export class LookupGuard implements CanActivate {
constructor(private dsoService: DsoRedirectDataService) {
}
canActivate(route: ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable<boolean> {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const params = this.getLookupParams(route);
return this.dsoService.findById(params.id, params.type).pipe(
map((response: RemoteData<FindByIDRequest>) => response.hasFailed)

View File

@@ -1,10 +1,10 @@
import { of as observableOf } from 'rxjs';
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { SearchFilter } from '../+search-page/search-filter.model';
import { SearchFilter } from '../shared/search/search-filter.model';
import { ActivatedRouteStub } from '../shared/testing/active-router-stub';
import { MockRoleService } from '../shared/mocks/mock-role-service';
import { cold, hot } from 'jasmine-marbles';
@@ -38,12 +38,8 @@ describe('MyDSpaceConfigurationService', () => {
const roleService: any = new MockRoleService();
const fixedFilterService = jasmine.createSpyObj('SearchFixedFilterService', {
getQueryByFilterName: observableOf(''),
});
beforeEach(() => {
service = new MyDSpaceConfigurationService(roleService, fixedFilterService, spy, activatedRoute);
service = new MyDSpaceConfigurationService(roleService, spy, activatedRoute);
});
describe('when the scope is called', () => {

View File

@@ -6,12 +6,11 @@ import { first, map } from 'rxjs/operators';
import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-type';
import { RoleService } from '../core/roles/role.service';
import { SearchConfigurationOption } from '../+search-page/search-switch-configuration/search-configuration-option.model';
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
import { RouteService } from '../core/services/route.service';
import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { RouteService } from '../core/services/route.service';
/**
* Service that performs all actions that have to do with the current mydspace configuration
@@ -55,16 +54,14 @@ export class MyDSpaceConfigurationService extends SearchConfigurationService {
* Initialize class
*
* @param {roleService} roleService
* @param {SearchFixedFilterService} fixedFilterService
* @param {RouteService} routeService
* @param {ActivatedRoute} route
*/
constructor(protected roleService: RoleService,
protected fixedFilterService: SearchFixedFilterService,
protected routeService: RouteService,
protected route: ActivatedRoute) {
super(routeService, fixedFilterService, route);
super(routeService, route);
// override parent class initialization
this._defaults = null;

View File

@@ -14,7 +14,7 @@ import { UploaderOptions } from '../../shared/uploader/uploader-options.model';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { NotificationType } from '../../shared/notifications/models/notification-type';
import { hasValue } from '../../shared/empty.util';
import { SearchResult } from '../../+search-page/search-result.model';
import { SearchResult } from '../../shared/search/search-result.model';
/**
* This component represents the whole mydspace page header
@@ -33,12 +33,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
/**
* The UploaderOptions object
*/
public uploadFilesOptions: UploaderOptions = {
url: '',
authToken: null,
disableMultipart: false,
itemAlias: null
};
public uploadFilesOptions: UploaderOptions = new UploaderOptions();
/**
* Subscription to unsubscribe from

View File

@@ -1 +1 @@
@import '../+search-page/search-page.component.scss';
@import '../+search-page/search.component.scss';

View File

@@ -19,15 +19,14 @@ import { MyDSpacePageComponent, SEARCH_CONFIG_SERVICE } from './my-dspace-page.c
import { RouteService } from '../core/services/route.service';
import { routeServiceStub } from '../shared/testing/route-service-stub';
import { SearchConfigurationServiceStub } from '../shared/testing/search-configuration-service-stub';
import { SearchService } from '../+search-page/search-service/search.service';
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
import { SearchService } from '../core/shared/search/search.service';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchFilterService } from '../+search-page/search-filters/search-filter/search-filter.service';
import { SearchFilterService } from '../core/shared/search/search-filter.service';
import { RoleDirective } from '../shared/roles/role.directive';
import { RoleService } from '../core/roles/role.service';
import { MockRoleService } from '../shared/mocks/mock-role-service';
import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { createSuccessfulRemoteDataObject$ } from '../shared/testing/utils';
describe('MyDSpacePageComponent', () => {
@@ -82,8 +81,6 @@ describe('MyDSpacePageComponent', () => {
collapse: () => this.isCollapsed = observableOf(true),
expand: () => this.isCollapsed = observableOf(false)
};
const mockFixedFilterService: SearchFixedFilterService = {
} as SearchFixedFilterService;
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -123,10 +120,6 @@ describe('MyDSpacePageComponent', () => {
provide: RoleService,
useValue: new MockRoleService()
},
{
provide: SearchFixedFilterService,
useValue: mockFixedFilterService
}
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(MyDSpacePageComponent, {

View File

@@ -15,19 +15,19 @@ import { RemoteData } from '../core/data/remote-data';
import { DSpaceObject } from '../core/shared/dspace-object.model';
import { pushInOut } from '../shared/animations/push';
import { HostWindowService } from '../shared/host-window.service';
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
import { SearchService } from '../+search-page/search-service/search.service';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { SearchService } from '../core/shared/search/search.service';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { hasValue } from '../shared/empty.util';
import { getSucceededRemoteData } from '../core/shared/operators';
import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service';
import { SearchConfigurationOption } from '../+search-page/search-switch-configuration/search-configuration-option.model';
import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model';
import { RoleType } from '../core/roles/role-types';
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
import { ViewMode } from '../core/shared/view-mode.model';
import { MyDSpaceRequest } from '../core/data/request.models';
import { SearchResult } from '../+search-page/search-result.model';
import { SearchResult } from '../shared/search/search-result.model';
import { Context } from '../core/shared/context.model';
export const MYDSPACE_ROUTE = '/mydspace';

View File

@@ -5,7 +5,6 @@ import { SharedModule } from '../shared/shared.module';
import { MyDspacePageRoutingModule } from './my-dspace-page-routing.module';
import { MyDSpacePageComponent } from './my-dspace-page.component';
import { SearchPageModule } from '../+search-page/search-page.module';
import { MyDSpaceResultsComponent } from './my-dspace-results/my-dspace-results.component';
import { WorkspaceItemSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component';
import { ClaimedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component';
@@ -27,7 +26,6 @@ import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/
CommonModule,
SharedModule,
MyDspacePageRoutingModule,
SearchPageModule
],
declarations: [
MyDSpacePageComponent,

View File

@@ -2,12 +2,12 @@ import { Component, Input } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
import { SearchOptions } from '../../+search-page/search-options.model';
import { SearchOptions } from '../../shared/search/search-options.model';
import { PaginatedList } from '../../core/data/paginated-list';
import { ViewMode } from '../../core/shared/view-mode.model';
import { isEmpty } from '../../shared/empty.util';
import { SearchResult } from '../../+search-page/search-result.model';
import { Context } from '../../core/shared/context.model';
import { SearchResult } from '../../shared/search/search-result.model';
/**
* Component that represents all results for mydspace page

View File

@@ -1,7 +1,7 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { configureSearchComponentTestingModule } from './search-page.component.spec';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { configureSearchComponentTestingModule } from './search.component.spec';
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
describe('ConfigurationSearchPageComponent', () => {
let comp: ConfigurationSearchPageComponent;

View File

@@ -1,23 +1,22 @@
import { HostWindowService } from '../shared/host-window.service';
import { SearchService } from './search-service/search.service';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchPageComponent } from './search-page.component';
import { SearchComponent } from './search.component';
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
import { pushInOut } from '../shared/animations/push';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { Observable } from 'rxjs';
import { PaginatedSearchOptions } from './paginated-search-options.model';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
import { map } from 'rxjs/operators';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { hasValue } from '../shared/empty.util';
import { RouteService } from '../core/services/route.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.
*/
@Component({
selector: 'ds-configuration-search-page',
styleUrls: ['./search-page.component.scss'],
templateUrl: './search-page.component.html',
styleUrls: ['./search.component.scss'],
templateUrl: './search.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [pushInOut],
providers: [
@@ -28,19 +27,26 @@ import { RouteService } from '../core/services/route.service';
]
})
export class ConfigurationSearchPageComponent extends SearchPageComponent implements OnInit {
export class ConfigurationSearchPageComponent extends SearchComponent implements OnInit {
/**
* The configuration to use for the search options
* If empty, the configuration will be determined by the route parameter called 'configuration'
*/
@Input() configuration: string;
/**
* The actual query for the fixed filter.
* If empty, the query will be determined by the route parameter called 'filter'
*/
@Input() fixedFilterQuery: string;
constructor(protected service: SearchService,
protected sidebarService: SidebarService,
protected windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
protected routeService: RouteService) {
super(service, sidebarService, windowService, searchConfigService, routeService);
protected routeService: RouteService,
protected router: Router) {
super(service, sidebarService, windowService, searchConfigService, routeService, router);
}
/**
@@ -52,20 +58,11 @@ export class ConfigurationSearchPageComponent extends SearchPageComponent implem
*/
ngOnInit(): void {
super.ngOnInit();
}
/**
* Get the current paginated search options after updating the configuration using the configuration input
* This is to make sure the configuration is included in the paginated search options, as it is not part of any
* query or route parameters
* @returns {Observable<PaginatedSearchOptions>}
*/
protected getSearchOptions(): Observable<PaginatedSearchOptions> {
return this.searchConfigService.paginatedSearchOptions.pipe(
map((options: PaginatedSearchOptions) => {
const config = this.configuration || options.configuration;
return Object.assign(options, { configuration: config });
})
);
if (hasValue(this.configuration)) {
this.routeService.setParameter('configuration', this.configuration);
}
if (hasValue(this.fixedFilterQuery)) {
this.routeService.setParameter('fixedFilterQuery', this.fixedFilterQuery);
}
}
}

View File

@@ -1,21 +0,0 @@
import { FilteredSearchPageComponent } from './filtered-search-page.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { configureSearchComponentTestingModule } from './search-page.component.spec';
import { SearchConfigurationService } from './search-service/search-configuration.service';
describe('FilteredSearchPageComponent', () => {
let comp: FilteredSearchPageComponent;
let fixture: ComponentFixture<FilteredSearchPageComponent>;
let searchConfigService: SearchConfigurationService;
beforeEach(async(() => {
configureSearchComponentTestingModule(FilteredSearchPageComponent);
}));
beforeEach(() => {
fixture = TestBed.createComponent(FilteredSearchPageComponent);
comp = fixture.componentInstance;
searchConfigService = (comp as any).searchConfigService;
fixture.detectChanges();
});
});

View File

@@ -1,73 +0,0 @@
import { HostWindowService } from '../shared/host-window.service';
import { SearchService } from './search-service/search.service';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchPageComponent } from './search-page.component';
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
import { pushInOut } from '../shared/animations/push';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { Observable } from 'rxjs';
import { PaginatedSearchOptions } from './paginated-search-options.model';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
import { map } from 'rxjs/operators';
import { RouteService } from '../core/services/route.service';
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
@Component({
selector: 'ds-filtered-search-page',
styleUrls: ['./search-page.component.scss'],
templateUrl: './search-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [pushInOut],
providers: [
{
provide: SEARCH_CONFIG_SERVICE,
useClass: SearchConfigurationService
}
]
})
export class FilteredSearchPageComponent extends SearchPageComponent implements OnInit {
/**
* The actual query for the fixed filter.
* If empty, the query will be determined by the route parameter called 'filter'
*/
@Input() fixedFilterQuery: string;
constructor(protected service: SearchService,
protected sidebarService: SidebarService,
protected windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
protected routeService: RouteService) {
super(service, sidebarService, windowService, searchConfigService, routeService);
}
/**
* Listening to changes in the paginated search options
* If something changes, update the search results
*
* Listen to changes in the scope
* If something changes, update the list of scopes for the dropdown
*/
ngOnInit(): void {
super.ngOnInit();
}
/**
* Get the current paginated search options after updating the fixed filter using the fixedFilterQuery input
* This is to make sure the fixed filter is included in the paginated search options, as it is not part of any
* query or route parameters
* @returns {Observable<PaginatedSearchOptions>}
*/
protected getSearchOptions(): Observable<PaginatedSearchOptions> {
return this.searchConfigService.paginatedSearchOptions.pipe(
map((options: PaginatedSearchOptions) => {
const filter = this.fixedFilterQuery || options.fixedFilter;
return Object.assign(options, { fixedFilter: filter });
})
);
}
}

View File

@@ -1,38 +0,0 @@
import { SearchFixedFilterService } from './search-fixed-filter.service';
import { RequestService } from '../../../core/data/request.service';
import { of as observableOf } from 'rxjs';
import { RequestEntry } from '../../../core/data/request.reducer';
import { FilteredDiscoveryQueryResponse } from '../../../core/cache/response.models';
describe('SearchFixedFilterService', () => {
let service: SearchFixedFilterService;
const filterQuery = 'filter:query';
const requestServiceStub = Object.assign({
/* tslint:disable:no-empty */
configure: () => {
},
/* tslint:enable:no-empty */
generateRequestId: () => 'fake-id',
getByHref: () => observableOf(Object.assign(new RequestEntry(), {
response: new FilteredDiscoveryQueryResponse(filterQuery, 200, 'OK')
}))
}) as RequestService;
beforeEach(() => {
service = new SearchFixedFilterService();
});
describe('when getQueryByRelations is called', () => {
const relationType = 'isRelationOf';
const itemUUID = 'c5b277e6-2477-48bb-8993-356710c285f3';
it('should contain the relationType and itemUUID', () => {
const query = service.getQueryByRelations(relationType, itemUUID);
expect(query.length).toBeGreaterThan(relationType.length + itemUUID.length);
expect(query).toContain(relationType);
expect(query).toContain(itemUUID);
});
});
});

View File

@@ -1,27 +0,0 @@
import { Injectable } from '@angular/core';
/**
* Service for performing actions on the filtered-discovery-pages REST endpoint
*/
@Injectable()
export class SearchFixedFilterService {
/**
* Get the query for looking up items by relation type
* @param {string} relationType Relation type
* @param {string} itemUUID Item UUID
* @returns {string} Query
*/
getQueryByRelations(relationType: string, itemUUID: string): string {
return `query=relation.${relationType}:${itemUUID}`;
}
/**
* Get the filter for a relation with the item's UUID
* @param relationType The type of relation e.g. 'isAuthorOfPublication'
* @param itemUUID The item's UUID
*/
getFilterByRelation(relationType: string, itemUUID: string): string {
return `f.${relationType}=${itemUUID}`;
}
}

View File

@@ -1,7 +0,0 @@
<div class="row mb-3 mb-md-1">
<div class="labels col-sm-9 offset-sm-3">
<ng-container *ngFor="let key of ((appliedFilters | async) | dsObjectKeys)">
<ds-search-label *ngFor="let value of (appliedFilters | async)[key]" [inPlaceSearch]="inPlaceSearch" [key]="key" [value]="value" [appliedFilters]="appliedFilters"></ds-search-label>
</ng-container>
</div>
</div>

View File

@@ -1,9 +1,9 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { SearchPageComponent } from './search-page.component';
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { SearchPageComponent } from './search-page.component';
@NgModule({
imports: [

View File

@@ -1,50 +1,2 @@
<div class="container" *ngIf="(isXsOrSm$ | async)">
<div class="row">
<div class="col-12">
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
</div>
</div>
</div>
<ds-page-with-sidebar [id]="'search-page'" [sidebarContent]="sidebarContent">
<div class="row">
<div class="col-12" *ngIf="!(isXsOrSm$ | async)">
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
</div>
<div id="search-content" class="col-12">
<div class="d-block d-md-none search-controls clearfix">
<ds-view-mode-switch [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
<button (click)="openSidebar()" aria-controls="#search-body"
class="btn btn-outline-primary float-right open-sidebar"><i
class="fas fa-sliders"></i> {{"search.sidebar.open"
| translate}}
</button>
</div>
<ds-search-results [searchResults]="resultsRD$ | async"
[searchConfig]="searchOptions$ | async"
[configuration]="configuration$ | async"
[disableHeader]="!searchEnabled"></ds-search-results>
</div>
</div>
</ds-page-with-sidebar>
<ng-template #sidebarContent>
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
(toggleSidebar)="closeSidebar()"
>
</ds-search-sidebar>
</ng-template>
<ng-template #searchForm>
<ds-search-form *ngIf="searchEnabled" id="search-form"
[query]="(searchOptions$ | async)?.query"
[scope]="(searchOptions$ | async)?.scope"
[currentUrl]="searchLink"
[scopes]="(scopeListRD$ | async)"
[inPlaceSearch]="inPlaceSearch">
</ds-search-form>
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
</ng-template>
<ds-search></ds-search>
<ds-search-tracker></ds-search-tracker>

View File

@@ -1,184 +1,12 @@
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
import { startWith, switchMap, } from 'rxjs/operators';
import { PaginatedList } from '../core/data/paginated-list';
import { RemoteData } from '../core/data/remote-data';
import { DSpaceObject } from '../core/shared/dspace-object.model';
import { pushInOut } from '../shared/animations/push';
import { HostWindowService } from '../shared/host-window.service';
import { PaginatedSearchOptions } from './paginated-search-options.model';
import { SearchResult } from './search-result.model';
import { SearchService } from './search-service/search.service';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { hasValue, isNotEmpty } from '../shared/empty.util';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { getSucceededRemoteData } from '../core/shared/operators';
import { RouteService } from '../core/services/route.service';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
export const SEARCH_ROUTE = '/search';
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
import { Component } from '@angular/core';
@Component({
selector: 'ds-search-page',
styleUrls: ['./search-page.component.scss'],
templateUrl: './search-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [pushInOut],
providers: [
{
provide: SEARCH_CONFIG_SERVICE,
useClass: SearchConfigurationService
}
]
})
/**
* This component represents the whole search page
* It renders search results depending on the current search options
*/
export class SearchPageComponent implements OnInit {
/**
* The current search results
*/
resultsRD$: BehaviorSubject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new BehaviorSubject(null);
/**
* The current paginated search options
*/
searchOptions$: Observable<PaginatedSearchOptions>;
/**
* The current relevant scopes
*/
scopeListRD$: Observable<DSpaceObject[]>;
/**
* Emits true if were on a small screen
*/
isXsOrSm$: Observable<boolean>;
/**
* Subscription to unsubscribe from
*/
sub: Subscription;
/**
* True when the search component should show results on the current page
*/
@Input() inPlaceSearch = true;
/**
* Whether or not the search bar should be visible
*/
@Input()
searchEnabled = true;
/**
* The width of the sidebar (bootstrap columns)
*/
@Input()
sideBarWidth = 3;
/**
* The currently applied configuration (determines title of search)
*/
@Input()
configuration$: Observable<string>;
/**
* Link to the search page
*/
searchLink: string;
/**
* Observable for whether or not the sidebar is currently collapsed
*/
isSidebarCollapsed$: Observable<boolean>;
constructor(protected service: SearchService,
protected sidebarService: SidebarService,
protected windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
protected routeService: RouteService) {
this.isXsOrSm$ = this.windowService.isXsOrSm();
}
/**
* Listening to changes in the paginated search options
* If something changes, update the search results
*
* Listen to changes in the scope
* If something changes, update the list of scopes for the dropdown
*/
ngOnInit(): void {
this.isSidebarCollapsed$ = this.isSidebarCollapsed();
this.searchLink = this.getSearchLink();
this.searchOptions$ = this.getSearchOptions();
this.sub = this.searchOptions$.pipe(
switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(undefined))))
.subscribe((results) => {
this.resultsRD$.next(results);
});
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
switchMap((scopeId) => this.service.getScopes(scopeId))
);
if (!isNotEmpty(this.configuration$)) {
this.configuration$ = this.routeService.getRouteParameterValue('configuration');
}
}
/**
* Get the current paginated search options
* @returns {Observable<PaginatedSearchOptions>}
*/
protected getSearchOptions(): Observable<PaginatedSearchOptions> {
return this.searchConfigService.paginatedSearchOptions;
}
/**
* Set the sidebar to a collapsed state
*/
public closeSidebar(): void {
this.sidebarService.collapse()
}
/**
* Set the sidebar to an expanded state
*/
public openSidebar(): void {
this.sidebarService.expand();
}
/**
* Check if the sidebar is collapsed
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
*/
private isSidebarCollapsed(): Observable<boolean> {
return this.sidebarService.isCollapsed;
}
/**
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
*/
private getSearchLink(): string {
if (this.inPlaceSearch) {
return './';
}
return this.service.getSearchLink();
}
/**
* Unsubscribe from the subscription
*/
ngOnDestroy(): void {
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
}
export class SearchPageComponent {
}

View File

@@ -3,65 +3,23 @@ import { CommonModule } from '@angular/common';
import { CoreModule } from '../core/core.module';
import { SharedModule } from '../shared/shared.module';
import { SearchPageRoutingModule } from './search-page-routing.module';
import { SearchPageComponent } from './search-page.component';
import { SearchResultsComponent } from './search-results/search-results.component';
import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'
import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component';
import { SearchComponent } from './search.component';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SidebarEffects } from '../shared/sidebar/sidebar-effects.service';
import { SearchSettingsComponent } from './search-settings/search-settings.component';
import { EffectsModule } from '@ngrx/effects';
import { SearchFiltersComponent } from './search-filters/search-filters.component';
import { SearchFilterComponent } from './search-filters/search-filter/search-filter.component';
import { SearchFacetFilterComponent } from './search-filters/search-filter/search-facet-filter/search-facet-filter.component';
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
import { SearchFixedFilterService } from './search-filters/search-filter/search-fixed-filter.service';
import { SearchLabelsComponent } from './search-labels/search-labels.component';
import { SearchRangeFilterComponent } from './search-filters/search-filter/search-range-filter/search-range-filter.component';
import { SearchTextFilterComponent } from './search-filters/search-filter/search-text-filter/search-text-filter.component';
import { SearchFacetFilterWrapperComponent } from './search-filters/search-filter/search-facet-filter-wrapper/search-facet-filter-wrapper.component';
import { SearchBooleanFilterComponent } from './search-filters/search-filter/search-boolean-filter/search-boolean-filter.component';
import { SearchHierarchyFilterComponent } from './search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { SearchFacetOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component';
import { SearchFacetSelectedOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component';
import { SearchFacetRangeOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component';
import { SearchSwitchConfigurationComponent } from './search-switch-configuration/search-switch-configuration.component';
import { SearchAuthorityFilterComponent } from './search-filters/search-filter/search-authority-filter/search-authority-filter.component';
import { SearchLabelComponent } from './search-labels/search-label/search-label.component';
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
import { FilteredSearchPageComponent } from './filtered-search-page.component';
import { SearchTrackerComponent } from './search-tracker.component';
import { StatisticsModule } from '../statistics/statistics.module';
import { SearchPageComponent } from './search-page.component';
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
const effects = [
SidebarEffects
];
import { SearchFilterService } from '../core/shared/search/search-filter.service';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
const components = [
SearchPageComponent,
SearchResultsComponent,
SearchSidebarComponent,
SearchSettingsComponent,
SearchFiltersComponent,
SearchFilterComponent,
SearchFacetFilterComponent,
SearchLabelsComponent,
SearchLabelComponent,
SearchFacetFilterComponent,
SearchFacetFilterWrapperComponent,
SearchRangeFilterComponent,
SearchTextFilterComponent,
SearchHierarchyFilterComponent,
SearchBooleanFilterComponent,
SearchFacetOptionComponent,
SearchFacetSelectedOptionComponent,
SearchFacetRangeOptionComponent,
SearchSwitchConfigurationComponent,
SearchAuthorityFilterComponent,
FilteredSearchPageComponent,
ConfigurationSearchPageComponent
SearchComponent,
ConfigurationSearchPageComponent,
SearchTrackerComponent
];
@NgModule({
@@ -69,29 +27,17 @@ const components = [
SearchPageRoutingModule,
CommonModule,
SharedModule,
EffectsModule.forFeature(effects),
CoreModule.forRoot(),
StatisticsModule.forRoot(),
],
declarations: components,
providers: [
SidebarService,
SidebarFilterService,
SearchFilterService,
SearchFixedFilterService,
ConfigurationSearchPageGuard,
SearchConfigurationService
],
entryComponents: [
SearchFacetFilterComponent,
SearchRangeFilterComponent,
SearchTextFilterComponent,
SearchHierarchyFilterComponent,
SearchBooleanFilterComponent,
SearchFacetOptionComponent,
SearchFacetSelectedOptionComponent,
SearchFacetRangeOptionComponent,
SearchAuthorityFilterComponent
],
exports: components
})

View File

@@ -1,26 +0,0 @@
import { DSpaceObject } from '../core/shared/dspace-object.model';
import { MetadataMap } from '../core/shared/metadata.models';
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
import { GenericConstructor } from '../core/shared/generic-constructor';
/**
* Represents a search result object of a certain (<T>) DSpaceObject
*/
export class SearchResult<T extends DSpaceObject> implements ListableObject {
/**
* The DSpaceObject that was found
*/
indexableObject: T;
/**
* The metadata that was used to find this item, hithighlighted
*/
hitHighlights: MetadataMap;
/**
* Method that returns as which type of object this object should be rendered
*/
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
return [this.constructor as GenericConstructor<ListableObject>];
}
}

Some files were not shown because too many files have changed in this diff Show More