mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 18:44:14 +00:00
Merge branch 'master' into metadata-and-relationships-combined-in-submission
This commit is contained in:
28
.github/pull_request_template.md
vendored
Normal file
28
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
## References
|
||||||
|
_Add references/links to any related tickets or PRs. These may include:_
|
||||||
|
* Link to [Angular issue or PR](https://github.com/DSpace/dspace-angular/issues) related to this PR, if any
|
||||||
|
* Link to [JIRA](https://jira.lyrasis.org/projects/DS/summary) ticket(s), if any
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Short summary of changes (1-2 sentences).
|
||||||
|
|
||||||
|
## Instructions for Reviewers
|
||||||
|
Please add a more detailed description of the changes made by your PR. At a minimum, providing a bulleted list of changes in your PR is helpful to reviewers.
|
||||||
|
|
||||||
|
List of changes in this PR:
|
||||||
|
* First, ...
|
||||||
|
* Second, ...
|
||||||
|
|
||||||
|
**Include guidance for how to test or review your PR.** This may include: steps to reproduce a bug, screenshots or description of a new feature, or reasons behind specific changes.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
_This checklist provides a reminder of what we are going to look for when reviewing your PR. You need not complete this checklist prior to creating your PR (draft PRs are always welcome). If you are unsure about an item in the checklist, don't hesitate to ask. We're here to help!_
|
||||||
|
|
||||||
|
- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
|
||||||
|
- [ ] My PR passes [TSLint](https://palantir.github.io/tslint/) validation using `yarn run lint`
|
||||||
|
- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
|
||||||
|
- [ ] My PR passes all specs/tests and includes new/updated specs for any bug fixes, improvements or new features. A few reminders about what constitutes good tests:
|
||||||
|
* Include tests for different user types (if behavior differs), including: (1) Anonymous user, (2) Logged in user (non-admin), and (3) Administrator.
|
||||||
|
* Include tests for error scenarios, e.g. when errors/warnings should appear (or buttons should be disabled).
|
||||||
|
* For bug fixes, include a test that reproduces the bug and proves it is fixed. For clarity, it may be useful to provide the test in a separate commit from the bug fix.
|
||||||
|
- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/master/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -34,3 +34,5 @@ yarn-error.log
|
|||||||
*.css
|
*.css
|
||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
|
.java-version
|
||||||
|
@@ -6,5 +6,7 @@ WORKDIR /app
|
|||||||
ADD . /app/
|
ADD . /app/
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
RUN yarn install
|
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
|
||||||
|
# See, for example https://github.com/yarnpkg/yarn/issues/5540
|
||||||
|
RUN yarn install --network-timeout 300000
|
||||||
CMD yarn run watch
|
CMD yarn run watch
|
||||||
|
39
LICENSE
Normal file
39
LICENSE
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
DSpace source code BSD License:
|
||||||
|
|
||||||
|
Copyright (c) 2002-2020, LYRASIS. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
- Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
- Neither the name DuraSpace nor the name of the DSpace Foundation
|
||||||
|
nor the names of its contributors may be used to endorse or promote
|
||||||
|
products derived from this software without specific prior written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||||
|
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
||||||
|
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
||||||
|
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||||
|
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
|
DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
DSpace uses third-party libraries which may be distributed under
|
||||||
|
different licenses to the above. Information about these licenses
|
||||||
|
is detailed in the LICENSES_THIRD_PARTY file at the root of the source
|
||||||
|
tree. You must agree to the terms of these licenses, in addition to
|
||||||
|
the above DSpace source code license, in order to use this software.
|
15
LICENSES_THIRD_PARTY
Normal file
15
LICENSES_THIRD_PARTY
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
DSpace uses third-party libraries which may be distributed under different licenses.
|
||||||
|
A summary of all third-party, production dependencies used by this user interface may be found by running:
|
||||||
|
|
||||||
|
npx license-checker --production --summary
|
||||||
|
|
||||||
|
(Additional license-checker options may be found in its documentation: https://github.com/davglass/license-checker)
|
||||||
|
|
||||||
|
You must agree to the terms of these licenses, in addition to the DSpace source code license, in order to use this
|
||||||
|
software.
|
||||||
|
|
||||||
|
PLEASE NOTE: Some third-party dependencies may be listed under multiple licenses if they are dual-licensed.
|
||||||
|
This is especially true of anything listed as GPL (or similar), as DSpace does NOT allow for the inclusion of
|
||||||
|
any dependencies that are solely released under GPL (or similar) terms. For more info see:
|
||||||
|
https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions
|
@@ -140,7 +140,7 @@ module.exports = {
|
|||||||
}, {
|
}, {
|
||||||
code: 'nl',
|
code: 'nl',
|
||||||
label: 'Nederlands',
|
label: 'Nederlands',
|
||||||
active: false,
|
active: true,
|
||||||
}, {
|
}, {
|
||||||
code: 'pt',
|
code: 'pt',
|
||||||
label: 'Português',
|
label: 'Português',
|
||||||
|
@@ -170,6 +170,98 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"admin.access-control.epeople.title": "DSpace Angular :: EPeople",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.head": "EPeople",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.search.head": "Search",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.search.scope.name": "Name",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.search.scope.email": "E-mail (exact)",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.search.scope.metadata": "Metadata",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.search.button": "Search",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.button.add": "Add EPerson",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.table.id": "ID",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.table.name": "Name",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.table.email": "E-mail",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.table.edit": "Edit",
|
||||||
|
|
||||||
|
"item.access-control.epeople.table.edit.buttons.edit": "Edit",
|
||||||
|
|
||||||
|
"item.access-control.epeople.table.edit.buttons.remove": "Remove",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.no-items": "No EPeople to show.",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.create": "Create EPerson",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.edit": "Edit EPerson",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.firstName": "First name",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.lastName": "Last name",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.email": "E-mail",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.emailHint": "Must be valid e-mail address",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.canLogIn": "Can log in",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.requireCertificate": "Requires certificate",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.notification.created.success": "Successfully created EPerson \"{{name}}\"",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.notification.created.failure": "Failed to create EPerson \"{{name}}\"",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.notification.created.failure.emailInUse": "Failed to create EPerson \"{{name}}\", email \"{{email}}\" already in use.",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.notification.edited.failure.emailInUse": "Failed to edit EPerson \"{{name}}\", email \"{{email}}\" already in use.",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.notification.edited.success": "Successfully edited EPerson \"{{name}}\"",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.form.notification.edited.failure": "Failed to edit EPerson \"{{name}}\"",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.notification.deleted.failure": "Failed to delete EPerson: \"{{name}}\"",
|
||||||
|
|
||||||
|
"admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"admin.search.breadcrumbs": "Administrative Search",
|
||||||
|
|
||||||
|
"admin.search.collection.edit": "Edit",
|
||||||
|
|
||||||
|
"admin.search.community.edit": "Edit",
|
||||||
|
|
||||||
|
"admin.search.item.delete": "Delete",
|
||||||
|
|
||||||
|
"admin.search.item.edit": "Edit",
|
||||||
|
|
||||||
|
"admin.search.item.make-private": "Make Private",
|
||||||
|
|
||||||
|
"admin.search.item.make-public": "Make Public",
|
||||||
|
|
||||||
|
"admin.search.item.move": "Move",
|
||||||
|
|
||||||
|
"admin.search.item.private": "Private",
|
||||||
|
|
||||||
|
"admin.search.item.reinstate": "Reinstate",
|
||||||
|
|
||||||
|
"admin.search.item.withdraw": "Withdraw",
|
||||||
|
|
||||||
|
"admin.search.item.withdrawn": "Withdrawn",
|
||||||
|
|
||||||
|
"admin.search.title": "Administrative Search",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"auth.errors.invalid-user": "Invalid email address or password.",
|
"auth.errors.invalid-user": "Invalid email address or password.",
|
||||||
|
|
||||||
"auth.messages.expired": "Your session has expired. Please log in again.",
|
"auth.messages.expired": "Your session has expired. Please log in again.",
|
||||||
@@ -196,6 +288,14 @@
|
|||||||
|
|
||||||
"browse.metadata.title": "Title",
|
"browse.metadata.title": "Title",
|
||||||
|
|
||||||
|
"browse.metadata.author.breadcrumbs": "Browse by Author",
|
||||||
|
|
||||||
|
"browse.metadata.dateissued.breadcrumbs": "Browse by Date",
|
||||||
|
|
||||||
|
"browse.metadata.subject.breadcrumbs": "Browse by Subject",
|
||||||
|
|
||||||
|
"browse.metadata.title.breadcrumbs": "Browse by Title",
|
||||||
|
|
||||||
"browse.startsWith.choose_start": "(Choose start)",
|
"browse.startsWith.choose_start": "(Choose start)",
|
||||||
|
|
||||||
"browse.startsWith.choose_year": "(Choose year)",
|
"browse.startsWith.choose_year": "(Choose year)",
|
||||||
@@ -237,7 +337,6 @@
|
|||||||
"browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}",
|
"browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"chips.remove": "Remove chip",
|
"chips.remove": "Remove chip",
|
||||||
|
|
||||||
|
|
||||||
@@ -266,6 +365,8 @@
|
|||||||
|
|
||||||
"collection.edit.head": "Edit Collection",
|
"collection.edit.head": "Edit Collection",
|
||||||
|
|
||||||
|
"collection.edit.breadcrumbs": "Edit Collection",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"collection.edit.item-mapper.cancel": "Cancel",
|
"collection.edit.item-mapper.cancel": "Cancel",
|
||||||
@@ -450,6 +551,7 @@
|
|||||||
|
|
||||||
"community.edit.head": "Edit Community",
|
"community.edit.head": "Edit Community",
|
||||||
|
|
||||||
|
"community.edit.breadcrumbs": "Edit Community",
|
||||||
|
|
||||||
|
|
||||||
"community.edit.logo.label": "Community logo",
|
"community.edit.logo.label": "Community logo",
|
||||||
@@ -657,6 +759,8 @@
|
|||||||
|
|
||||||
"item.edit.head": "Edit Item",
|
"item.edit.head": "Edit Item",
|
||||||
|
|
||||||
|
"item.edit.breadcrumbs": "Edit Item",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"item.edit.item-mapper.buttons.add": "Map item to selected collections",
|
"item.edit.item-mapper.buttons.add": "Map item to selected collections",
|
||||||
@@ -901,6 +1005,12 @@
|
|||||||
|
|
||||||
"item.edit.tabs.status.title": "Item Edit - Status",
|
"item.edit.tabs.status.title": "Item Edit - Status",
|
||||||
|
|
||||||
|
"item.edit.tabs.versionhistory.head": "Version History",
|
||||||
|
|
||||||
|
"item.edit.tabs.versionhistory.title": "Item Edit - Version History",
|
||||||
|
|
||||||
|
"item.edit.tabs.versionhistory.under-construction": "Editing or adding new versions is not yet possible in this user interface.",
|
||||||
|
|
||||||
"item.edit.tabs.view.head": "View Item",
|
"item.edit.tabs.view.head": "View Item",
|
||||||
|
|
||||||
"item.edit.tabs.view.title": "Item Edit - View",
|
"item.edit.tabs.view.title": "Item Edit - View",
|
||||||
@@ -980,6 +1090,29 @@
|
|||||||
"item.select.table.title": "Title",
|
"item.select.table.title": "Title",
|
||||||
|
|
||||||
|
|
||||||
|
"item.version.history.empty": "There are no other versions for this item yet.",
|
||||||
|
|
||||||
|
"item.version.history.head": "Version History",
|
||||||
|
|
||||||
|
"item.version.history.return": "Return",
|
||||||
|
|
||||||
|
"item.version.history.selected": "Selected version",
|
||||||
|
|
||||||
|
"item.version.history.table.version": "Version",
|
||||||
|
|
||||||
|
"item.version.history.table.item": "Item",
|
||||||
|
|
||||||
|
"item.version.history.table.editor": "Editor",
|
||||||
|
|
||||||
|
"item.version.history.table.date": "Date",
|
||||||
|
|
||||||
|
"item.version.history.table.summary": "Summary",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"item.version.notice": "This is not the latest version of this item. The latest version can be found <a href='{{destination}}'>here</a>.",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"journal.listelement.badge": "Journal",
|
"journal.listelement.badge": "Journal",
|
||||||
|
|
||||||
@@ -1071,12 +1204,18 @@
|
|||||||
|
|
||||||
"login.form.new-user": "New user? Click here to register.",
|
"login.form.new-user": "New user? Click here to register.",
|
||||||
|
|
||||||
|
"login.form.or-divider": "or",
|
||||||
|
|
||||||
"login.form.password": "Password",
|
"login.form.password": "Password",
|
||||||
|
|
||||||
|
"login.form.shibboleth": "Log in with Shibboleth",
|
||||||
|
|
||||||
"login.form.submit": "Log in",
|
"login.form.submit": "Log in",
|
||||||
|
|
||||||
"login.title": "Login",
|
"login.title": "Login",
|
||||||
|
|
||||||
|
"login.breadcrumbs": "Login",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"logout.form.header": "Log out from DSpace",
|
"logout.form.header": "Log out from DSpace",
|
||||||
@@ -1103,6 +1242,10 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"menu.section.admin_search": "Admin Search",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"menu.section.browse_community": "This Community",
|
"menu.section.browse_community": "This Community",
|
||||||
|
|
||||||
"menu.section.browse_community_by_author": "By Author",
|
"menu.section.browse_community_by_author": "By Author",
|
||||||
@@ -1153,18 +1296,10 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
"menu.section.find": "Find",
|
|
||||||
|
|
||||||
"menu.section.find_items": "Items",
|
|
||||||
|
|
||||||
"menu.section.find_private_items": "Private Items",
|
|
||||||
|
|
||||||
"menu.section.find_withdrawn_items": "Withdrawn Items",
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"menu.section.icon.access_control": "Access Control menu section",
|
"menu.section.icon.access_control": "Access Control menu section",
|
||||||
|
|
||||||
|
"menu.section.icon.admin_search": "Admin search menu section",
|
||||||
|
|
||||||
"menu.section.icon.control_panel": "Control Panel menu section",
|
"menu.section.icon.control_panel": "Control Panel menu section",
|
||||||
|
|
||||||
"menu.section.icon.curation_task": "Curation Task menu section",
|
"menu.section.icon.curation_task": "Curation Task menu section",
|
||||||
@@ -1339,6 +1474,8 @@
|
|||||||
|
|
||||||
"nav.mydspace": "MyDSpace",
|
"nav.mydspace": "MyDSpace",
|
||||||
|
|
||||||
|
"nav.profile": "Profile",
|
||||||
|
|
||||||
"nav.search": "Search",
|
"nav.search": "Search",
|
||||||
|
|
||||||
"nav.statistics.header": "Statistics",
|
"nav.statistics.header": "Statistics",
|
||||||
@@ -1397,6 +1534,64 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"profile.breadcrumbs": "Update Profile",
|
||||||
|
|
||||||
|
"profile.card.identify": "Identify",
|
||||||
|
|
||||||
|
"profile.card.security": "Security",
|
||||||
|
|
||||||
|
"profile.form.submit": "Update Profile",
|
||||||
|
|
||||||
|
"profile.groups.head": "Authorization groups you belong to",
|
||||||
|
|
||||||
|
"profile.head": "Update Profile",
|
||||||
|
|
||||||
|
"profile.metadata.form.error.firstname.required": "First Name is required",
|
||||||
|
|
||||||
|
"profile.metadata.form.error.lastname.required": "Last Name is required",
|
||||||
|
|
||||||
|
"profile.metadata.form.label.email": "Email Address",
|
||||||
|
|
||||||
|
"profile.metadata.form.label.firstname": "First Name",
|
||||||
|
|
||||||
|
"profile.metadata.form.label.language": "Language",
|
||||||
|
|
||||||
|
"profile.metadata.form.label.lastname": "Last Name",
|
||||||
|
|
||||||
|
"profile.metadata.form.label.phone": "Contact Telephone",
|
||||||
|
|
||||||
|
"profile.metadata.form.notifications.success.content": "Your changes to the profile were saved.",
|
||||||
|
|
||||||
|
"profile.metadata.form.notifications.success.title": "Profile saved",
|
||||||
|
|
||||||
|
"profile.notifications.warning.no-changes.content": "No changes were made to the Profile.",
|
||||||
|
|
||||||
|
"profile.notifications.warning.no-changes.title": "No changes",
|
||||||
|
|
||||||
|
"profile.security.form.error.matching-passwords": "The passwords do not match.",
|
||||||
|
|
||||||
|
"profile.security.form.error.password-length": "The password should be at least 6 characters long.",
|
||||||
|
|
||||||
|
"profile.security.form.info": "Optionally, you can enter a new password in the box below, and confirm it by typing it again into the second box. It should be at least six characters long.",
|
||||||
|
|
||||||
|
"profile.security.form.label.password": "Password",
|
||||||
|
|
||||||
|
"profile.security.form.label.passwordrepeat": "Retype to confirm",
|
||||||
|
|
||||||
|
"profile.security.form.notifications.success.content": "Your changes to the password were saved.",
|
||||||
|
|
||||||
|
"profile.security.form.notifications.success.title": "Password saved",
|
||||||
|
|
||||||
|
"profile.security.form.notifications.error.title": "Error changing passwords",
|
||||||
|
|
||||||
|
"profile.security.form.notifications.error.not-long-enough": "The password has to be at least 6 characters long.",
|
||||||
|
|
||||||
|
"profile.security.form.notifications.error.not-same": "The provided passwords are not the same.",
|
||||||
|
|
||||||
|
"profile.title": "Update Profile",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"project.listelement.badge": "Research Project",
|
"project.listelement.badge": "Research Project",
|
||||||
|
|
||||||
"project.page.contributor": "Contributors",
|
"project.page.contributor": "Contributors",
|
||||||
@@ -1462,7 +1657,7 @@
|
|||||||
"relationships.isSingleVolumeOf": "Journal Volume",
|
"relationships.isSingleVolumeOf": "Journal Volume",
|
||||||
|
|
||||||
"relationships.isVolumeOf": "Journal Volumes",
|
"relationships.isVolumeOf": "Journal Volumes",
|
||||||
|
|
||||||
"relationships.isContributorOf": "Contributors",
|
"relationships.isContributorOf": "Contributors",
|
||||||
|
|
||||||
|
|
||||||
@@ -1473,6 +1668,7 @@
|
|||||||
|
|
||||||
"search.title": "DSpace Angular :: Search",
|
"search.title": "DSpace Angular :: Search",
|
||||||
|
|
||||||
|
"search.breadcrumbs": "Search",
|
||||||
|
|
||||||
|
|
||||||
"search.filters.applied.f.author": "Author",
|
"search.filters.applied.f.author": "Author",
|
||||||
@@ -1483,6 +1679,8 @@
|
|||||||
|
|
||||||
"search.filters.applied.f.dateSubmitted": "Date submitted",
|
"search.filters.applied.f.dateSubmitted": "Date submitted",
|
||||||
|
|
||||||
|
"search.filters.applied.f.discoverable": "Private",
|
||||||
|
|
||||||
"search.filters.applied.f.entityType": "Item Type",
|
"search.filters.applied.f.entityType": "Item Type",
|
||||||
|
|
||||||
"search.filters.applied.f.has_content_in_original_bundle": "Has files",
|
"search.filters.applied.f.has_content_in_original_bundle": "Has files",
|
||||||
@@ -1494,10 +1692,15 @@
|
|||||||
"search.filters.applied.f.subject": "Subject",
|
"search.filters.applied.f.subject": "Subject",
|
||||||
|
|
||||||
"search.filters.applied.f.submitter": "Submitter",
|
"search.filters.applied.f.submitter": "Submitter",
|
||||||
|
|
||||||
"search.filters.applied.f.jobTitle": "Job Title",
|
"search.filters.applied.f.jobTitle": "Job Title",
|
||||||
|
|
||||||
"search.filters.applied.f.birthDate.max": "End birth date",
|
"search.filters.applied.f.birthDate.max": "End birth date",
|
||||||
|
|
||||||
"search.filters.applied.f.birthDate.min": "Start birth date",
|
"search.filters.applied.f.birthDate.min": "Start birth date",
|
||||||
|
|
||||||
|
"search.filters.applied.f.withdrawn": "Withdrawn",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"search.filters.filter.author.head": "Author",
|
"search.filters.filter.author.head": "Author",
|
||||||
@@ -1534,6 +1737,10 @@
|
|||||||
|
|
||||||
"search.filters.filter.dateSubmitted.placeholder": "Date submitted",
|
"search.filters.filter.dateSubmitted.placeholder": "Date submitted",
|
||||||
|
|
||||||
|
"search.filters.filter.discoverable.head": "Private",
|
||||||
|
|
||||||
|
"search.filters.filter.withdrawn.head": "Withdrawn",
|
||||||
|
|
||||||
"search.filters.filter.entityType.head": "Item Type",
|
"search.filters.filter.entityType.head": "Item Type",
|
||||||
|
|
||||||
"search.filters.filter.entityType.placeholder": "Item Type",
|
"search.filters.filter.entityType.placeholder": "Item Type",
|
||||||
@@ -1590,6 +1797,25 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"search.filters.entityType.JournalIssue": "Journal Issue",
|
||||||
|
|
||||||
|
"search.filters.entityType.JournalVolume": "Journal Volume",
|
||||||
|
|
||||||
|
"search.filters.entityType.OrgUnit": "Organizational Unit",
|
||||||
|
|
||||||
|
"search.filters.has_content_in_original_bundle.true": "Yes",
|
||||||
|
|
||||||
|
"search.filters.has_content_in_original_bundle.false": "No",
|
||||||
|
|
||||||
|
"search.filters.discoverable.true": "No",
|
||||||
|
|
||||||
|
"search.filters.discoverable.false": "Yes",
|
||||||
|
|
||||||
|
"search.filters.withdrawn.true": "Yes",
|
||||||
|
|
||||||
|
"search.filters.withdrawn.false": "No",
|
||||||
|
|
||||||
|
|
||||||
"search.filters.head": "Filters",
|
"search.filters.head": "Filters",
|
||||||
|
|
||||||
"search.filters.reset": "Reset filters",
|
"search.filters.reset": "Reset filters",
|
||||||
@@ -1973,6 +2199,8 @@
|
|||||||
"title": "DSpace",
|
"title": "DSpace",
|
||||||
|
|
||||||
|
|
||||||
|
"administrativeView.search.results.head": "Administrative Search",
|
||||||
|
|
||||||
|
|
||||||
"uploader.browse": "browse",
|
"uploader.browse": "browse",
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -918,7 +918,7 @@
|
|||||||
"item.edit.move.processing": "Movendo...",
|
"item.edit.move.processing": "Movendo...",
|
||||||
|
|
||||||
// "item.edit.move.search.placeholder": "Enter a search query to look for collections",
|
// "item.edit.move.search.placeholder": "Enter a search query to look for collections",
|
||||||
"item.edit.move.search.placeholder": "nsira uma consulta para procurar coleções",
|
"item.edit.move.search.placeholder": "Insira uma consulta para procurar coleções",
|
||||||
|
|
||||||
// "item.edit.move.success": "The item has been moved successfully",
|
// "item.edit.move.success": "The item has been moved successfully",
|
||||||
"item.edit.move.success": "O item foi movido com sucesso",
|
"item.edit.move.success": "O item foi movido com sucesso",
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{ path: 'epeople', component: EPeopleRegistryComponent, data: { title: 'admin.access-control.epeople.title' } },
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Routing module for the AccessControl section of the admin sidebar
|
||||||
|
*/
|
||||||
|
export class AdminAccessControlRoutingModule {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { AdminAccessControlRoutingModule } from './admin-access-control-routing.module';
|
||||||
|
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
||||||
|
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
RouterModule,
|
||||||
|
TranslateModule,
|
||||||
|
AdminAccessControlRoutingModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
EPeopleRegistryComponent,
|
||||||
|
EPersonFormComponent
|
||||||
|
],
|
||||||
|
entryComponents: []
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* This module handles all components related to the access control pages
|
||||||
|
*/
|
||||||
|
export class AdminAccessControlModule {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,49 @@
|
|||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||||
|
import { type } from '../../../shared/ngrx/type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each action type in an action group, make a simple
|
||||||
|
* enum object for all of this group's action types.
|
||||||
|
*
|
||||||
|
* The 'type' utility function coerces strings into string
|
||||||
|
* literal types and runs a simple check to guarantee all
|
||||||
|
* action types in the application are unique.
|
||||||
|
*/
|
||||||
|
export const EPeopleRegistryActionTypes = {
|
||||||
|
|
||||||
|
EDIT_EPERSON: type('dspace/epeople-registry/EDIT_EPERSON'),
|
||||||
|
CANCEL_EDIT_EPERSON: type('dspace/epeople-registry/CANCEL_EDIT_EPERSON'),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
/**
|
||||||
|
* Used to edit an EPerson in the EPeople registry
|
||||||
|
*/
|
||||||
|
export class EPeopleRegistryEditEPersonAction implements Action {
|
||||||
|
type = EPeopleRegistryActionTypes.EDIT_EPERSON;
|
||||||
|
|
||||||
|
eperson: EPerson;
|
||||||
|
|
||||||
|
constructor(eperson: EPerson) {
|
||||||
|
this.eperson = eperson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to cancel the editing of an EPerson in the EPeople registry
|
||||||
|
*/
|
||||||
|
export class EPeopleRegistryCancelEPersonAction implements Action {
|
||||||
|
type = EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a type alias of all actions in this action group
|
||||||
|
* so that reducers can easily compose action types
|
||||||
|
* These are all the actions to perform on the EPeople registry state
|
||||||
|
*/
|
||||||
|
export type EPeopleRegistryAction
|
||||||
|
= EPeopleRegistryEditEPersonAction
|
||||||
|
| EPeopleRegistryCancelEPersonAction
|
@@ -0,0 +1,90 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="epeople-registry row">
|
||||||
|
<div class="col-12">
|
||||||
|
|
||||||
|
<h2 id="header" class="border-bottom pb-2">{{labelPrefix + 'head' | translate}}</h2>
|
||||||
|
|
||||||
|
<ds-eperson-form *ngIf="isEPersonFormShown" (submitForm)="forceUpdateEPeople()"
|
||||||
|
(cancelForm)="isEPersonFormShown = false"></ds-eperson-form>
|
||||||
|
|
||||||
|
<div *ngIf="!isEPersonFormShown" class="button-row top d-flex pb-2">
|
||||||
|
<button class="mr-auto btn btn-success addEPerson-button"
|
||||||
|
(click)="isEPersonFormShown = true">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
<span class="d-none d-sm-inline">{{labelPrefix + 'button.add' | translate}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}</h3>
|
||||||
|
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row">
|
||||||
|
<div class="col-12 col-sm-3">
|
||||||
|
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
||||||
|
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
|
||||||
|
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-9 col-12">
|
||||||
|
<div class="form-group input-group">
|
||||||
|
<input type="text" name="query" id="query" formControlName="query"
|
||||||
|
class="form-control" aria-label="Search input">
|
||||||
|
<span class="input-group-append">
|
||||||
|
<button type="submit"
|
||||||
|
class="search-button btn btn-secondary">{{ labelPrefix + 'search.button' | translate }}</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<ds-pagination
|
||||||
|
*ngIf="(ePeople | async)?.payload?.totalElements > 0"
|
||||||
|
[paginationOptions]="config"
|
||||||
|
[pageInfoState]="(ePeople | async)?.payload"
|
||||||
|
[collectionSize]="(ePeople | async)?.payload?.totalElements"
|
||||||
|
[hideGear]="true"
|
||||||
|
[hidePagerWhenSinglePage]="true"
|
||||||
|
(pageChange)="onPageChange($event)">
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="epeople" class="table table-striped table-hover table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
|
||||||
|
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
|
||||||
|
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
|
||||||
|
<th>{{labelPrefix + 'table.edit' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let eperson of (ePeople | async)?.payload?.page"
|
||||||
|
[ngClass]="{'table-primary' : isActive(eperson) | async}">
|
||||||
|
<td>{{eperson.id}}</td>
|
||||||
|
<td>{{eperson.name}}</td>
|
||||||
|
<td>{{eperson.email}}</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group edit-field">
|
||||||
|
<button (click)="toggleEditEPerson(eperson)"
|
||||||
|
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
||||||
|
title="{{labelPrefix + 'table.edit.buttons.edit' | translate}}">
|
||||||
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
<button (click)="deleteEPerson(eperson)"
|
||||||
|
class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
||||||
|
title="{{labelPrefix + 'table.edit.buttons.remove' | translate}}">
|
||||||
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ds-pagination>
|
||||||
|
|
||||||
|
<div *ngIf="(ePeople | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||||
|
{{labelPrefix + 'no-items' | translate}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,205 @@
|
|||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { FindListOptions } from '../../../core/data/request.models';
|
||||||
|
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||||
|
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||||
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
|
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||||
|
import { getMockFormBuilderService } from '../../../shared/mocks/mock-form-builder-service';
|
||||||
|
import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader';
|
||||||
|
import { getMockTranslateService } from '../../../shared/mocks/mock-translate.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock';
|
||||||
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
|
||||||
|
import { EPeopleRegistryComponent } from './epeople-registry.component';
|
||||||
|
|
||||||
|
describe('EPeopleRegistryComponent', () => {
|
||||||
|
let component: EPeopleRegistryComponent;
|
||||||
|
let fixture: ComponentFixture<EPeopleRegistryComponent>;
|
||||||
|
let translateService: TranslateService;
|
||||||
|
let builderService: FormBuilderService;
|
||||||
|
|
||||||
|
const mockEPeople = [EPersonMock, EPersonMock2];
|
||||||
|
let ePersonDataServiceStub: any;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
ePersonDataServiceStub = {
|
||||||
|
activeEPerson: null,
|
||||||
|
allEpeople: mockEPeople,
|
||||||
|
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||||
|
},
|
||||||
|
getActiveEPerson(): Observable<EPerson> {
|
||||||
|
return observableOf(this.activeEPerson);
|
||||||
|
},
|
||||||
|
searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
|
if (scope === 'email') {
|
||||||
|
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||||
|
return ePerson.email === query
|
||||||
|
});
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||||
|
}
|
||||||
|
if (scope === 'metadata') {
|
||||||
|
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||||
|
return (ePerson.name.includes(query) || ePerson.email.includes(query))
|
||||||
|
});
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||||
|
}
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||||
|
},
|
||||||
|
deleteEPerson(ePerson: EPerson): Observable<boolean> {
|
||||||
|
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
|
||||||
|
return (ePerson2.uuid !== ePerson.uuid);
|
||||||
|
});
|
||||||
|
return observableOf(true);
|
||||||
|
},
|
||||||
|
editEPerson(ePerson: EPerson) {
|
||||||
|
this.activeEPerson = ePerson;
|
||||||
|
},
|
||||||
|
cancelEditEPerson() {
|
||||||
|
this.activeEPerson = null;
|
||||||
|
},
|
||||||
|
clearEPersonRequests(): void {
|
||||||
|
// empty
|
||||||
|
}
|
||||||
|
};
|
||||||
|
builderService = getMockFormBuilderService();
|
||||||
|
translateService = getMockTranslateService();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
declarations: [EPeopleRegistryComponent],
|
||||||
|
providers: [EPeopleRegistryComponent,
|
||||||
|
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||||
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
|
{ provide: FormBuilderService, useValue: builderService },
|
||||||
|
EPeopleRegistryComponent
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EPeopleRegistryComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create EPeopleRegistryComponent', inject([EPeopleRegistryComponent], (comp: EPeopleRegistryComponent) => {
|
||||||
|
expect(comp).toBeDefined();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display list of ePeople', () => {
|
||||||
|
const ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||||
|
expect(ePeopleIdsFound.length).toEqual(2);
|
||||||
|
mockEPeople.map((ePerson: EPerson) => {
|
||||||
|
expect(ePeopleIdsFound.find((foundEl) => {
|
||||||
|
return (foundEl.nativeElement.textContent.trim() === ePerson.uuid);
|
||||||
|
})).toBeTruthy();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('search', () => {
|
||||||
|
describe('when searching with scope/query (scope metadata)', () => {
|
||||||
|
let ePeopleIdsFound;
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
component.search({ scope: 'metadata', query: EPersonMock2.name });
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display search result', () => {
|
||||||
|
expect(ePeopleIdsFound.length).toEqual(1);
|
||||||
|
expect(ePeopleIdsFound.find((foundEl) => {
|
||||||
|
return (foundEl.nativeElement.textContent.trim() === EPersonMock2.uuid);
|
||||||
|
})).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when searching with scope/query (scope email)', () => {
|
||||||
|
let ePeopleIdsFound;
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
component.search({ scope: 'email', query: EPersonMock.email });
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display search result', () => {
|
||||||
|
expect(ePeopleIdsFound.length).toEqual(1);
|
||||||
|
expect(ePeopleIdsFound.find((foundEl) => {
|
||||||
|
return (foundEl.nativeElement.textContent.trim() === EPersonMock.uuid);
|
||||||
|
})).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggleEditEPerson', () => {
|
||||||
|
describe('when you click on first edit eperson button', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
const editButtons = fixture.debugElement.queryAll(By.css('.access-control-editEPersonButton'));
|
||||||
|
editButtons[0].triggerEventHandler('click', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('editEPerson form is toggled', () => {
|
||||||
|
const ePeopleIds = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||||
|
ePersonDataServiceStub.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
|
||||||
|
if (activeEPerson === ePeopleIds[0].nativeElement.textContent) {
|
||||||
|
expect(component.isEPersonFormShown).toEqual(false);
|
||||||
|
} else {
|
||||||
|
expect(component.isEPersonFormShown).toEqual(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteEPerson', () => {
|
||||||
|
describe('when you click on first delete eperson button', () => {
|
||||||
|
let ePeopleIdsFoundBeforeDelete;
|
||||||
|
let ePeopleIdsFoundAfterDelete;
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
ePeopleIdsFoundBeforeDelete = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||||
|
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
|
||||||
|
deleteButtons[0].triggerEventHandler('click', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
ePeopleIdsFoundAfterDelete = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('first ePerson is deleted', () => {
|
||||||
|
expect(ePeopleIdsFoundBeforeDelete.length === ePeopleIdsFoundAfterDelete + 1);
|
||||||
|
ePeopleIdsFoundAfterDelete.forEach((epersonElement) => {
|
||||||
|
expect(epersonElement !== ePeopleIdsFoundBeforeDelete[0].nativeElement.textContent).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,163 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { FormBuilder } from '@angular/forms';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map, take } from 'rxjs/operators';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||||
|
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-epeople-registry',
|
||||||
|
templateUrl: './epeople-registry.component.html',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* A component used for managing all existing epeople within the repository.
|
||||||
|
* The admin can create, edit or delete epeople here.
|
||||||
|
*/
|
||||||
|
export class EPeopleRegistryComponent {
|
||||||
|
|
||||||
|
labelPrefix = 'admin.access-control.epeople.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of all the current EPeople within the repository or the result of the search
|
||||||
|
*/
|
||||||
|
ePeople: Observable<RemoteData<PaginatedList<EPerson>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pagination config used to display the list of epeople
|
||||||
|
*/
|
||||||
|
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'epeople-list-pagination',
|
||||||
|
pageSize: 5,
|
||||||
|
currentPage: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to show the EPerson form
|
||||||
|
*/
|
||||||
|
isEPersonFormShown: boolean;
|
||||||
|
|
||||||
|
// The search form
|
||||||
|
searchForm;
|
||||||
|
|
||||||
|
constructor(private epersonService: EPersonDataService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private formBuilder: FormBuilder) {
|
||||||
|
this.updateEPeople({
|
||||||
|
currentPage: 1,
|
||||||
|
elementsPerPage: this.config.pageSize
|
||||||
|
});
|
||||||
|
this.isEPersonFormShown = false;
|
||||||
|
this.searchForm = this.formBuilder.group(({
|
||||||
|
scope: 'metadata',
|
||||||
|
query: '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event triggered when the user changes page
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
onPageChange(event) {
|
||||||
|
this.updateEPeople({
|
||||||
|
currentPage: event,
|
||||||
|
elementsPerPage: this.config.pageSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the list of EPeople by fetching it from the rest api or cache
|
||||||
|
*/
|
||||||
|
private updateEPeople(options) {
|
||||||
|
this.ePeople = this.epersonService.getEPeople(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force-update the list of EPeople by first clearing the cache related to EPeople, then performing
|
||||||
|
* a new REST call
|
||||||
|
*/
|
||||||
|
public forceUpdateEPeople() {
|
||||||
|
this.epersonService.clearEPersonRequests();
|
||||||
|
this.isEPersonFormShown = false;
|
||||||
|
this.search({ query: '', scope: 'metadata' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search in the EPeople by metadata (default) or email
|
||||||
|
* @param data Contains scope and query param
|
||||||
|
*/
|
||||||
|
search(data: any) {
|
||||||
|
this.ePeople = this.epersonService.searchByScope(data.scope, data.query, {
|
||||||
|
currentPage: 1,
|
||||||
|
elementsPerPage: this.config.pageSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given EPerson is active (being edited)
|
||||||
|
* @param eperson
|
||||||
|
*/
|
||||||
|
isActive(eperson: EPerson): Observable<boolean> {
|
||||||
|
return this.getActiveEPerson().pipe(
|
||||||
|
map((activeEPerson) => eperson === activeEPerson)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the active eperson (being edited)
|
||||||
|
*/
|
||||||
|
getActiveEPerson(): Observable<EPerson> {
|
||||||
|
return this.epersonService.getActiveEPerson();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start editing the selected EPerson
|
||||||
|
* @param ePerson
|
||||||
|
*/
|
||||||
|
toggleEditEPerson(ePerson: EPerson) {
|
||||||
|
this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => {
|
||||||
|
if (ePerson === activeEPerson) {
|
||||||
|
this.epersonService.cancelEditEPerson();
|
||||||
|
this.isEPersonFormShown = false;
|
||||||
|
} else {
|
||||||
|
this.epersonService.editEPerson(ePerson);
|
||||||
|
this.isEPersonFormShown = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.scrollToTop()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes EPerson, show notification on success/failure & updates EPeople list
|
||||||
|
*/
|
||||||
|
deleteEPerson(ePerson: EPerson) {
|
||||||
|
if (hasValue(ePerson.id)) {
|
||||||
|
this.epersonService.deleteEPerson(ePerson).pipe(take(1)).subscribe((success: boolean) => {
|
||||||
|
if (success) {
|
||||||
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: ePerson.name }));
|
||||||
|
this.forceUpdateEPeople();
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.failure', { name: ePerson.name }));
|
||||||
|
}
|
||||||
|
this.epersonService.cancelEditEPerson();
|
||||||
|
this.isEPersonFormShown = false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToTop() {
|
||||||
|
(function smoothscroll() {
|
||||||
|
const currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
|
||||||
|
if (currentScroll > 0) {
|
||||||
|
window.requestAnimationFrame(smoothscroll);
|
||||||
|
window.scrollTo(0, currentScroll - (currentScroll / 8));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,54 @@
|
|||||||
|
import { EPersonMock } from '../../../shared/testing/eperson-mock';
|
||||||
|
import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from './epeople-registry.actions';
|
||||||
|
import { ePeopleRegistryReducer, EPeopleRegistryState } from './epeople-registry.reducers';
|
||||||
|
|
||||||
|
const initialState: EPeopleRegistryState = {
|
||||||
|
editEPerson: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const editState: EPeopleRegistryState = {
|
||||||
|
editEPerson: EPersonMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
class NullAction extends EPeopleRegistryEditEPersonAction {
|
||||||
|
type = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('epeopleRegistryReducer', () => {
|
||||||
|
|
||||||
|
it('should return the current state when no valid actions have been made', () => {
|
||||||
|
const state = initialState;
|
||||||
|
const action = new NullAction();
|
||||||
|
const newState = ePeopleRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with an initial state', () => {
|
||||||
|
const state = initialState;
|
||||||
|
const action = new NullAction();
|
||||||
|
const initState = ePeopleRegistryReducer(undefined, action);
|
||||||
|
|
||||||
|
expect(initState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the current state to change the editEPerson to a new eperson when EPeopleRegistryEditEPersonAction is dispatched', () => {
|
||||||
|
const state = editState;
|
||||||
|
const action = new EPeopleRegistryEditEPersonAction(EPersonMock);
|
||||||
|
const newState = ePeopleRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState.editEPerson).toEqual(EPersonMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the current state to remove the editEPerson from the state when EPeopleRegistryCancelEPersonAction is dispatched', () => {
|
||||||
|
const state = editState;
|
||||||
|
const action = new EPeopleRegistryCancelEPersonAction();
|
||||||
|
const newState = ePeopleRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState.editEPerson).toEqual(null);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,47 @@
|
|||||||
|
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||||
|
import {
|
||||||
|
EPeopleRegistryAction,
|
||||||
|
EPeopleRegistryActionTypes,
|
||||||
|
EPeopleRegistryEditEPersonAction
|
||||||
|
} from './epeople-registry.actions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The EPeople registry state.
|
||||||
|
* @interface EPeopleRegistryState
|
||||||
|
*/
|
||||||
|
export interface EPeopleRegistryState {
|
||||||
|
editEPerson: EPerson;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initial state.
|
||||||
|
*/
|
||||||
|
const initialState: EPeopleRegistryState = {
|
||||||
|
editEPerson: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reducer that handles EPeopleRegistryActions to modify EPeople
|
||||||
|
* @param state The current EPeopleRegistryState
|
||||||
|
* @param action The EPeopleRegistryAction to perform on the state
|
||||||
|
*/
|
||||||
|
export function ePeopleRegistryReducer(state = initialState, action: EPeopleRegistryAction): EPeopleRegistryState {
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case EPeopleRegistryActionTypes.EDIT_EPERSON: {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
editEPerson: (action as EPeopleRegistryEditEPersonAction).eperson
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON: {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
editEPerson: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
<div *ngIf="epersonService.getActiveEPerson() | async; then editheader; else createHeader"></div>
|
||||||
|
|
||||||
|
<ng-template #createHeader>
|
||||||
|
<h4>{{messagePrefix + '.create' | translate}}</h4>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #editheader>
|
||||||
|
<h4>{{messagePrefix + '.edit' | translate}}</h4>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ds-form [formId]="formId"
|
||||||
|
[formModel]="formModel"
|
||||||
|
[formGroup]="formGroup"
|
||||||
|
[formLayout]="formLayout"
|
||||||
|
(cancel)="onCancel()"
|
||||||
|
(submitForm)="onSubmit()">
|
||||||
|
</ds-form>
|
@@ -0,0 +1,206 @@
|
|||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RestResponse } from '../../../../core/cache/response.models';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { FindListOptions } from '../../../../core/data/request.models';
|
||||||
|
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||||
|
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
|
import { getMockFormBuilderService } from '../../../../shared/mocks/mock-form-builder-service';
|
||||||
|
import { getMockTranslateService } from '../../../../shared/mocks/mock-translate.service';
|
||||||
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
|
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson-mock';
|
||||||
|
import { MockTranslateLoader } from '../../../../shared/testing/mock-translate-loader';
|
||||||
|
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||||
|
import { EPeopleRegistryComponent } from '../epeople-registry.component';
|
||||||
|
import { EPersonFormComponent } from './eperson-form.component';
|
||||||
|
|
||||||
|
describe('EPersonFormComponent', () => {
|
||||||
|
let component: EPersonFormComponent;
|
||||||
|
let fixture: ComponentFixture<EPersonFormComponent>;
|
||||||
|
let translateService: TranslateService;
|
||||||
|
let builderService: FormBuilderService;
|
||||||
|
|
||||||
|
const mockEPeople = [EPersonMock, EPersonMock2];
|
||||||
|
let ePersonDataServiceStub: any;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
ePersonDataServiceStub = {
|
||||||
|
activeEPerson: null,
|
||||||
|
allEpeople: mockEPeople,
|
||||||
|
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||||
|
},
|
||||||
|
getActiveEPerson(): Observable<EPerson> {
|
||||||
|
return observableOf(this.activeEPerson);
|
||||||
|
},
|
||||||
|
searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
|
if (scope === 'email') {
|
||||||
|
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||||
|
return ePerson.email === query
|
||||||
|
});
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||||
|
}
|
||||||
|
if (scope === 'metadata') {
|
||||||
|
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||||
|
return (ePerson.name.includes(query) || ePerson.email.includes(query))
|
||||||
|
});
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||||
|
}
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||||
|
},
|
||||||
|
deleteEPerson(ePerson: EPerson): Observable<boolean> {
|
||||||
|
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
|
||||||
|
return (ePerson2.uuid !== ePerson.uuid);
|
||||||
|
});
|
||||||
|
return observableOf(true);
|
||||||
|
},
|
||||||
|
create(ePerson: EPerson) {
|
||||||
|
this.allEpeople = [...this.allEpeople, ePerson]
|
||||||
|
},
|
||||||
|
editEPerson(ePerson: EPerson) {
|
||||||
|
this.activeEPerson = ePerson;
|
||||||
|
},
|
||||||
|
cancelEditEPerson() {
|
||||||
|
this.activeEPerson = null;
|
||||||
|
},
|
||||||
|
clearEPersonRequests(): void {
|
||||||
|
// empty
|
||||||
|
},
|
||||||
|
tryToCreate(ePerson: EPerson): Observable<RestResponse> {
|
||||||
|
this.allEpeople = [...this.allEpeople, ePerson]
|
||||||
|
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||||
|
},
|
||||||
|
updateEPerson(ePerson: EPerson): Observable<RestResponse> {
|
||||||
|
this.allEpeople.forEach((ePersonInList: EPerson, i: number) => {
|
||||||
|
if (ePersonInList.id === ePerson.id) {
|
||||||
|
this.allEpeople[i] = ePerson;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
builderService = getMockFormBuilderService();
|
||||||
|
translateService = getMockTranslateService();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
declarations: [EPeopleRegistryComponent, EPersonFormComponent],
|
||||||
|
providers: [EPersonFormComponent,
|
||||||
|
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||||
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
|
{ provide: FormBuilderService, useValue: builderService },
|
||||||
|
EPeopleRegistryComponent
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EPersonFormComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create EPersonFormComponent', inject([EPersonFormComponent], (comp: EPersonFormComponent) => {
|
||||||
|
expect(comp).toBeDefined();
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('when submitting the form', () => {
|
||||||
|
const firstName = 'testName';
|
||||||
|
const lastName = 'testLastName';
|
||||||
|
const email = 'testEmail@test.com';
|
||||||
|
const canLogIn = false;
|
||||||
|
const requireCertificate = false;
|
||||||
|
|
||||||
|
const expected = Object.assign(new EPerson(), {
|
||||||
|
metadata: {
|
||||||
|
'eperson.firstname': [
|
||||||
|
{
|
||||||
|
value: firstName
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'eperson.lastname': [
|
||||||
|
{
|
||||||
|
value: lastName
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
email: email,
|
||||||
|
canLogIn: canLogIn,
|
||||||
|
requireCertificate: requireCertificate,
|
||||||
|
});
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(component.submitForm, 'emit');
|
||||||
|
component.firstName.value = firstName;
|
||||||
|
component.lastName.value = lastName;
|
||||||
|
component.email.value = email;
|
||||||
|
component.canLogIn.value = canLogIn;
|
||||||
|
component.requireCertificate.value = requireCertificate;
|
||||||
|
});
|
||||||
|
describe('without active EPerson', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(undefined));
|
||||||
|
component.onSubmit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit a new eperson using the correct values', async(() => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with an active eperson', () => {
|
||||||
|
const expectedWithId = Object.assign(new EPerson(), {
|
||||||
|
metadata: {
|
||||||
|
'eperson.firstname': [
|
||||||
|
{
|
||||||
|
value: firstName
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'eperson.lastname': [
|
||||||
|
{
|
||||||
|
value: lastName
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
email: email,
|
||||||
|
canLogIn: canLogIn,
|
||||||
|
requireCertificate: requireCertificate,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
|
||||||
|
component.onSubmit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit the existing eperson using the correct values', async(() => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,344 @@
|
|||||||
|
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
|
import { FormGroup } from '@angular/forms';
|
||||||
|
import {
|
||||||
|
DynamicCheckboxModel,
|
||||||
|
DynamicFormControlModel,
|
||||||
|
DynamicFormLayout,
|
||||||
|
DynamicInputModel
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||||
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { RestResponse } from '../../../../core/cache/response.models';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||||
|
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
|
import { hasValue } from '../../../../shared/empty.util';
|
||||||
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-eperson-form',
|
||||||
|
templateUrl: './eperson-form.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* A form used for creating and editing EPeople
|
||||||
|
*/
|
||||||
|
export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
labelPrefix = 'admin.access-control.epeople.form.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A unique id used for ds-form
|
||||||
|
*/
|
||||||
|
formId = 'eperson-form';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The labelPrefix for all messages related to this form
|
||||||
|
*/
|
||||||
|
messagePrefix = 'admin.access-control.epeople.form';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic input models for the inputs of form
|
||||||
|
*/
|
||||||
|
firstName: DynamicInputModel;
|
||||||
|
lastName: DynamicInputModel;
|
||||||
|
email: DynamicInputModel;
|
||||||
|
// booleans
|
||||||
|
canLogIn: DynamicCheckboxModel;
|
||||||
|
requireCertificate: DynamicCheckboxModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of all dynamic input models
|
||||||
|
*/
|
||||||
|
formModel: DynamicFormControlModel[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout used for structuring the form inputs
|
||||||
|
*/
|
||||||
|
formLayout: DynamicFormLayout = {
|
||||||
|
firstName: {
|
||||||
|
grid: {
|
||||||
|
host: 'row'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lastName: {
|
||||||
|
grid: {
|
||||||
|
host: 'row'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
grid: {
|
||||||
|
host: 'row'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
canLogIn: {
|
||||||
|
grid: {
|
||||||
|
host: 'col col-sm-6 d-inline-block'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requireCertificate: {
|
||||||
|
grid: {
|
||||||
|
host: 'col col-sm-6 d-inline-block'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A FormGroup that combines all inputs
|
||||||
|
*/
|
||||||
|
formGroup: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An EventEmitter that's fired whenever the form is being submitted
|
||||||
|
*/
|
||||||
|
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An EventEmitter that's fired whenever the form is cancelled
|
||||||
|
*/
|
||||||
|
@Output() cancelForm: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of subscriptions
|
||||||
|
*/
|
||||||
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to retrieve initial active eperson, to fill in checkboxes at component creation
|
||||||
|
*/
|
||||||
|
epersonInitial: EPerson;
|
||||||
|
|
||||||
|
constructor(public epersonService: EPersonDataService,
|
||||||
|
private formBuilderService: FormBuilderService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private notificationsService: NotificationsService,) {
|
||||||
|
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||||
|
this.epersonInitial = eperson;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
combineLatest(
|
||||||
|
this.translateService.get(`${this.messagePrefix}.firstName`),
|
||||||
|
this.translateService.get(`${this.messagePrefix}.lastName`),
|
||||||
|
this.translateService.get(`${this.messagePrefix}.email`),
|
||||||
|
this.translateService.get(`${this.messagePrefix}.canLogIn`),
|
||||||
|
this.translateService.get(`${this.messagePrefix}.requireCertificate`),
|
||||||
|
this.translateService.get(`${this.messagePrefix}.emailHint`),
|
||||||
|
).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => {
|
||||||
|
this.firstName = new DynamicInputModel({
|
||||||
|
id: 'firstName',
|
||||||
|
label: firstName,
|
||||||
|
name: 'firstName',
|
||||||
|
validators: {
|
||||||
|
required: null,
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
this.lastName = new DynamicInputModel({
|
||||||
|
id: 'lastName',
|
||||||
|
label: lastName,
|
||||||
|
name: 'lastName',
|
||||||
|
validators: {
|
||||||
|
required: null,
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
this.email = new DynamicInputModel({
|
||||||
|
id: 'email',
|
||||||
|
label: email,
|
||||||
|
name: 'email',
|
||||||
|
validators: {
|
||||||
|
required: null,
|
||||||
|
pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$'
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
hint: emailHint
|
||||||
|
});
|
||||||
|
this.canLogIn = new DynamicCheckboxModel(
|
||||||
|
{
|
||||||
|
id: 'canLogIn',
|
||||||
|
label: canLogIn,
|
||||||
|
name: 'canLogIn',
|
||||||
|
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true)
|
||||||
|
});
|
||||||
|
this.requireCertificate = new DynamicCheckboxModel(
|
||||||
|
{
|
||||||
|
id: 'requireCertificate',
|
||||||
|
label: requireCertificate,
|
||||||
|
name: 'requireCertificate',
|
||||||
|
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false)
|
||||||
|
});
|
||||||
|
this.formModel = [
|
||||||
|
this.firstName,
|
||||||
|
this.lastName,
|
||||||
|
this.email,
|
||||||
|
this.canLogIn,
|
||||||
|
this.requireCertificate,
|
||||||
|
];
|
||||||
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
|
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '',
|
||||||
|
lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '',
|
||||||
|
email: eperson != null ? eperson.email : '',
|
||||||
|
canLogIn: eperson != null ? eperson.canLogIn : true,
|
||||||
|
requireCertificate: eperson != null ? eperson.requireCertificate : false
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop editing the currently selected eperson
|
||||||
|
*/
|
||||||
|
onCancel() {
|
||||||
|
this.epersonService.cancelEditEPerson();
|
||||||
|
this.cancelForm.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the form
|
||||||
|
* When the eperson has an id attached -> Edit the eperson
|
||||||
|
* When the eperson has no id attached -> Create new eperson
|
||||||
|
* Emit the updated/created eperson using the EventEmitter submitForm
|
||||||
|
*/
|
||||||
|
onSubmit() {
|
||||||
|
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe(
|
||||||
|
(ePerson: EPerson) => {
|
||||||
|
console.log('onsubmit ep', ePerson)
|
||||||
|
const values = {
|
||||||
|
metadata: {
|
||||||
|
'eperson.firstname': [
|
||||||
|
{
|
||||||
|
value: this.firstName.value
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'eperson.lastname': [
|
||||||
|
{
|
||||||
|
value: this.lastName.value
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
email: this.email.value,
|
||||||
|
canLogIn: this.canLogIn.value,
|
||||||
|
requireCertificate: this.requireCertificate.value,
|
||||||
|
};
|
||||||
|
if (ePerson == null) {
|
||||||
|
this.createNewEPerson(values);
|
||||||
|
} else {
|
||||||
|
this.editEPerson(ePerson, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates new EPerson based on given values from form
|
||||||
|
* @param values
|
||||||
|
*/
|
||||||
|
createNewEPerson(values) {
|
||||||
|
console.log('createNewEPerson(values)', values)
|
||||||
|
const ePersonToCreate = Object.assign(new EPerson(), values);
|
||||||
|
|
||||||
|
const response = this.epersonService.tryToCreate(ePersonToCreate);
|
||||||
|
response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
|
||||||
|
if (restResponse.isSuccessful) {
|
||||||
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: ePersonToCreate.name }));
|
||||||
|
this.submitForm.emit(ePersonToCreate);
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: ePersonToCreate.name }));
|
||||||
|
this.cancelForm.emit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.showNotificationIfEmailInUse(ePersonToCreate, 'created');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits existing EPerson based on given values from form and old EPerson
|
||||||
|
* @param ePerson ePerson to edit
|
||||||
|
* @param values new ePerson values (of form)
|
||||||
|
*/
|
||||||
|
editEPerson(ePerson: EPerson, values) {
|
||||||
|
const editedEperson = Object.assign(new EPerson(), {
|
||||||
|
id: ePerson.id,
|
||||||
|
metadata: {
|
||||||
|
'eperson.firstname': [
|
||||||
|
{
|
||||||
|
value: (this.firstName.value ? this.firstName.value : ePerson.firstMetadataValue('eperson.firstname'))
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'eperson.lastname': [
|
||||||
|
{
|
||||||
|
value: (this.lastName.value ? this.lastName.value : ePerson.firstMetadataValue('eperson.lastname'))
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
email: (hasValue(values.email) ? values.email : ePerson.email),
|
||||||
|
canLogIn: (hasValue(values.canLogIn) ? values.canLogIn : ePerson.canLogIn),
|
||||||
|
requireCertificate: (hasValue(values.requireCertificate) ? values.requireCertificate : ePerson.requireCertificate),
|
||||||
|
_links: ePerson._links,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = this.epersonService.updateEPerson(editedEperson);
|
||||||
|
response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
|
||||||
|
if (restResponse.isSuccessful) {
|
||||||
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name }));
|
||||||
|
this.submitForm.emit(editedEperson);
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: editedEperson.name }));
|
||||||
|
this.cancelForm.emit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (values.email != null && values.email !== ePerson.email) {
|
||||||
|
this.showNotificationIfEmailInUse(editedEperson, 'edited');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for the given ePerson if there is already an ePerson in the system with that email
|
||||||
|
* and shows notification if this is the case
|
||||||
|
* @param ePerson ePerson values to check
|
||||||
|
* @param notificationSection whether in create or edit
|
||||||
|
*/
|
||||||
|
private showNotificationIfEmailInUse(ePerson: EPerson, notificationSection: string) {
|
||||||
|
// Relevant message for email in use
|
||||||
|
this.subs.push(this.epersonService.searchByScope('email', ePerson.email, {
|
||||||
|
currentPage: 1,
|
||||||
|
elementsPerPage: 0
|
||||||
|
}).pipe(getSucceededRemoteData(), getRemoteDataPayload())
|
||||||
|
.subscribe((list: PaginatedList<EPerson>) => {
|
||||||
|
if (list.totalElements > 0) {
|
||||||
|
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.' + notificationSection + '.failure.emailInUse', {
|
||||||
|
name: ePerson.name,
|
||||||
|
email: ePerson.email
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all input-fields to be empty
|
||||||
|
*/
|
||||||
|
clearFields() {
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
canLogin: true,
|
||||||
|
requireCertificate: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.onCancel();
|
||||||
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
}
|
@@ -230,10 +230,10 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
comp.deleteFormats();
|
comp.deleteFormats();
|
||||||
|
|
||||||
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1.id);
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2.id);
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3.id);
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4.id);
|
||||||
|
|
||||||
expect(notificationsServiceStub.success).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.success.head',
|
expect(notificationsServiceStub.success).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.success.head',
|
||||||
'admin.registries.bitstream-formats.delete.success.amount');
|
'admin.registries.bitstream-formats.delete.success.amount');
|
||||||
@@ -276,10 +276,10 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
comp.deleteFormats();
|
comp.deleteFormats();
|
||||||
|
|
||||||
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1.id);
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2.id);
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3.id);
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4.id);
|
||||||
|
|
||||||
expect(notificationsServiceStub.error).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.failure.head',
|
expect(notificationsServiceStub.error).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.failure.head',
|
||||||
'admin.registries.bitstream-formats.delete.failure.amount');
|
'admin.registries.bitstream-formats.delete.failure.amount');
|
||||||
|
@@ -64,7 +64,7 @@ export class BitstreamFormatsComponent implements OnInit {
|
|||||||
const tasks$ = [];
|
const tasks$ = [];
|
||||||
for (const format of formats) {
|
for (const format of formats) {
|
||||||
if (hasValue(format.id)) {
|
if (hasValue(format.id)) {
|
||||||
tasks$.push(this.bitstreamFormatService.delete(format));
|
tasks$.push(this.bitstreamFormatService.delete(format.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
zip(...tasks$).subscribe((results: boolean[]) => {
|
zip(...tasks$).subscribe((results: boolean[]) => {
|
||||||
|
@@ -2,8 +2,11 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
import { getAdminModulePath } from '../app-routing.module';
|
import { getAdminModulePath } from '../app-routing.module';
|
||||||
|
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
const REGISTRIES_MODULE_PATH = 'registries';
|
const REGISTRIES_MODULE_PATH = 'registries';
|
||||||
|
const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
||||||
|
|
||||||
export function getRegistriesModulePath() {
|
export function getRegistriesModulePath() {
|
||||||
return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString();
|
return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString();
|
||||||
@@ -15,7 +18,17 @@ export function getRegistriesModulePath() {
|
|||||||
{
|
{
|
||||||
path: REGISTRIES_MODULE_PATH,
|
path: REGISTRIES_MODULE_PATH,
|
||||||
loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule'
|
loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule'
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
path: ACCESS_CONTROL_MODULE_PATH,
|
||||||
|
loadChildren: './admin-access-control/admin-access-control.module#AdminAccessControlModule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'search',
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
component: AdminSearchPageComponent,
|
||||||
|
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }
|
||||||
|
},
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
<ds-configuration-search-page configuration="administrativeView" [context]="context"></ds-configuration-search-page>
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AdminSearchPageComponent } from './admin-search-page.component';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
|
||||||
|
describe('AdminSearchPageComponent', () => {
|
||||||
|
let component: AdminSearchPageComponent;
|
||||||
|
let fixture: ComponentFixture<AdminSearchPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ AdminSearchPageComponent ],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AdminSearchPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,18 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Context } from '../../core/shared/context.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-admin-search-page',
|
||||||
|
templateUrl: './admin-search-page.component.html',
|
||||||
|
styleUrls: ['./admin-search-page.component.scss']
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents a search page for administrators
|
||||||
|
*/
|
||||||
|
export class AdminSearchPageComponent {
|
||||||
|
/**
|
||||||
|
* The context of this page
|
||||||
|
*/
|
||||||
|
context: Context = Context.AdminSearch;
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
<ds-collection-search-result-grid-element [object]="object"
|
||||||
|
[index]="index"
|
||||||
|
[linkType]="linkType"
|
||||||
|
[listID]="listID">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item text-center">
|
||||||
|
<a class="btn btn-light btn-sm btn-auto my-1 edit-link" [routerLink]="[editPath]">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ds-collection-search-result-grid-element>
|
||||||
|
|
@@ -0,0 +1,66 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||||
|
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
|
||||||
|
import { SharedModule } from '../../../../../shared/shared.module';
|
||||||
|
import { CollectionAdminSearchResultGridElementComponent } from './collection-admin-search-result-grid-element.component';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
|
||||||
|
import { Collection } from '../../../../../core/shared/collection.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
|
||||||
|
|
||||||
|
describe('CollectionAdminSearchResultGridElementComponent', () => {
|
||||||
|
let component: CollectionAdminSearchResultGridElementComponent;
|
||||||
|
let fixture: ComponentFixture<CollectionAdminSearchResultGridElementComponent>;
|
||||||
|
let id;
|
||||||
|
let searchResult;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
searchResult = new CollectionSearchResult();
|
||||||
|
searchResult.indexableObject = new Collection();
|
||||||
|
searchResult.indexableObject.uuid = id;
|
||||||
|
}
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
SharedModule
|
||||||
|
],
|
||||||
|
declarations: [CollectionAdminSearchResultGridElementComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
|
{ provide: BitstreamDataService, useValue: {} },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CollectionAdminSearchResultGridElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = searchResult;
|
||||||
|
component.linkTypes = CollectionElementLinkType;
|
||||||
|
component.index = 0;
|
||||||
|
component.viewModes = ViewMode;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render an edit button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.edit-link'));
|
||||||
|
const link = a.nativeElement.href;
|
||||||
|
expect(link).toContain(getCollectionEditPath(id));
|
||||||
|
})
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
|
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
|
||||||
|
import { Collection } from '../../../../../core/shared/collection.model';
|
||||||
|
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
|
||||||
|
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
|
||||||
|
|
||||||
|
@listableObjectComponent(CollectionSearchResult, ViewMode.GridElement, Context.AdminSearch)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-collection-admin-search-result-list-element',
|
||||||
|
styleUrls: ['./collection-admin-search-result-grid-element.component.scss'],
|
||||||
|
templateUrl: './collection-admin-search-result-grid-element.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a list element for a collection search result on the admin search page
|
||||||
|
*/
|
||||||
|
export class CollectionAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<CollectionSearchResult, Collection> {
|
||||||
|
editPath: string;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.editPath = getCollectionEditPath(this.dso.uuid);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
<ds-community-search-result-grid-element [object]="object"
|
||||||
|
[index]="index"
|
||||||
|
[linkType]="linkType"
|
||||||
|
[listID]="listID">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item text-center">
|
||||||
|
<a class="btn btn-light btn-sm btn-auto my-1 edit-link" [routerLink]="[editPath]">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ds-community-search-result-grid-element>
|
||||||
|
|
@@ -0,0 +1,70 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||||
|
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
|
||||||
|
import { SharedModule } from '../../../../../shared/shared.module';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { CommunityAdminSearchResultGridElementComponent } from './community-admin-search-result-grid-element.component';
|
||||||
|
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||||
|
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
|
||||||
|
import { Community } from '../../../../../core/shared/community.model';
|
||||||
|
import { CommunityAdminSearchResultListElementComponent } from '../../admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component';
|
||||||
|
|
||||||
|
describe('CommunityAdminSearchResultGridElementComponent', () => {
|
||||||
|
let component: CommunityAdminSearchResultGridElementComponent;
|
||||||
|
let fixture: ComponentFixture<CommunityAdminSearchResultGridElementComponent>;
|
||||||
|
let id;
|
||||||
|
let searchResult;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
searchResult = new CommunitySearchResult();
|
||||||
|
searchResult.indexableObject = new Community();
|
||||||
|
searchResult.indexableObject.uuid = id;
|
||||||
|
}
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
SharedModule
|
||||||
|
],
|
||||||
|
declarations: [CommunityAdminSearchResultGridElementComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
|
{ provide: BitstreamDataService, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CommunityAdminSearchResultGridElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = searchResult;
|
||||||
|
component.linkTypes = CollectionElementLinkType;
|
||||||
|
component.index = 0;
|
||||||
|
component.viewModes = ViewMode;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render an edit button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.edit-link'));
|
||||||
|
const link = a.nativeElement.href;
|
||||||
|
expect(link).toContain(getCommunityEditPath(id));
|
||||||
|
})
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
|
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||||
|
import { Community } from '../../../../../core/shared/community.model';
|
||||||
|
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
|
||||||
|
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
|
||||||
|
|
||||||
|
@listableObjectComponent(CommunitySearchResult, ViewMode.GridElement, Context.AdminSearch)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-community-admin-search-result-grid-element',
|
||||||
|
styleUrls: ['./community-admin-search-result-grid-element.component.scss'],
|
||||||
|
templateUrl: './community-admin-search-result-grid-element.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a list element for a community search result on the admin search page
|
||||||
|
*/
|
||||||
|
export class CommunityAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<CommunitySearchResult, Community> {
|
||||||
|
editPath: string;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.editPath = getCommunityEditPath(this.dso.uuid);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
<ng-template dsListableObject>
|
||||||
|
</ng-template>
|
||||||
|
<div #badges class="position-absolute ml-1">
|
||||||
|
<div *ngIf="dso && !dso.isDiscoverable" class="private-badge">
|
||||||
|
<span class="badge badge-danger">{{ "admin.search.item.private" | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="dso && dso.isWithdrawn" class="withdrawn-badge">
|
||||||
|
<span class="badge badge-warning">{{ "admin.search.item.withdrawn" | translate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul #buttons class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<ds-item-admin-search-result-actions-element class="d-flex justify-content-between" [item]="dso" [small]="true"></ds-item-admin-search-result-actions-element>
|
||||||
|
</li>
|
||||||
|
</ul>
|
@@ -0,0 +1,121 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||||
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
|
import { Bitstream } from '../../../../../core/shared/bitstream.model';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
|
||||||
|
import { SharedModule } from '../../../../../shared/shared.module';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
|
import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-result-grid-element.component';
|
||||||
|
|
||||||
|
describe('ItemAdminSearchResultGridElementComponent', () => {
|
||||||
|
let component: ItemAdminSearchResultGridElementComponent;
|
||||||
|
let fixture: ComponentFixture<ItemAdminSearchResultGridElementComponent>;
|
||||||
|
let id;
|
||||||
|
let searchResult;
|
||||||
|
|
||||||
|
const mockBitstreamDataService = {
|
||||||
|
getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(new Bitstream());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
searchResult = new ItemSearchResult();
|
||||||
|
searchResult.indexableObject = new Item();
|
||||||
|
searchResult.indexableObject.uuid = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule(
|
||||||
|
{
|
||||||
|
declarations: [ItemAdminSearchResultGridElementComponent],
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
SharedModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
|
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemAdminSearchResultGridElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = searchResult;
|
||||||
|
component.linkTypes = CollectionElementLinkType;
|
||||||
|
component.index = 0;
|
||||||
|
component.viewModes = ViewMode;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the item is not withdrawn', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.dso.isWithdrawn = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show the withdrawn badge', () => {
|
||||||
|
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||||
|
expect(badge).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the item is withdrawn', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.dso.isWithdrawn = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the withdrawn badge', () => {
|
||||||
|
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||||
|
expect(badge).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the item is not private', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.dso.isDiscoverable = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should not show the private badge', () => {
|
||||||
|
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||||
|
expect(badge).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the item is private', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.dso.isDiscoverable = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the private badge', () => {
|
||||||
|
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||||
|
expect(badge).not.toBeNull();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
@@ -0,0 +1,75 @@
|
|||||||
|
import { Component, ComponentFactoryResolver, ElementRef, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { getListableObjectComponent, listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
|
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
|
import { getItemEditPath } from '../../../../../+item-page/item-page-routing.module';
|
||||||
|
import { URLCombiner } from '../../../../../core/url-combiner/url-combiner';
|
||||||
|
import {
|
||||||
|
ITEM_EDIT_DELETE_PATH,
|
||||||
|
ITEM_EDIT_MOVE_PATH,
|
||||||
|
ITEM_EDIT_PRIVATE_PATH,
|
||||||
|
ITEM_EDIT_PUBLIC_PATH,
|
||||||
|
ITEM_EDIT_REINSTATE_PATH,
|
||||||
|
ITEM_EDIT_WITHDRAW_PATH
|
||||||
|
} from '../../../../../+item-page/edit-item-page/edit-item-page.routing.module';
|
||||||
|
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||||
|
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
|
||||||
|
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||||
|
|
||||||
|
@listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-admin-search-result-grid-element',
|
||||||
|
styleUrls: ['./item-admin-search-result-grid-element.component.scss'],
|
||||||
|
templateUrl: './item-admin-search-result-grid-element.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a list element for an item search result on the admin search page
|
||||||
|
*/
|
||||||
|
export class ItemAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<ItemSearchResult, Item> implements OnInit {
|
||||||
|
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
|
||||||
|
@ViewChild('badges', { static: true }) badges: ElementRef;
|
||||||
|
@ViewChild('buttons', { static: true }) buttons: ElementRef;
|
||||||
|
|
||||||
|
constructor(protected truncatableService: TruncatableService,
|
||||||
|
protected bitstreamDataService: BitstreamDataService,
|
||||||
|
private componentFactoryResolver: ComponentFactoryResolver
|
||||||
|
) {
|
||||||
|
super(truncatableService, bitstreamDataService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the dynamic child component
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent());
|
||||||
|
|
||||||
|
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
||||||
|
viewContainerRef.clear();
|
||||||
|
|
||||||
|
const componentRef = viewContainerRef.createComponent(
|
||||||
|
componentFactory,
|
||||||
|
0,
|
||||||
|
undefined,
|
||||||
|
[
|
||||||
|
[this.badges.nativeElement],
|
||||||
|
[this.buttons.nativeElement]
|
||||||
|
]);
|
||||||
|
(componentRef.instance as any).object = this.object;
|
||||||
|
(componentRef.instance as any).index = this.index;
|
||||||
|
(componentRef.instance as any).linkType = this.linkType;
|
||||||
|
(componentRef.instance as any).listID = this.listID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the component depending on the item's relationship type, view mode and context
|
||||||
|
* @returns {GenericConstructor<Component>}
|
||||||
|
*/
|
||||||
|
private getComponent(): GenericConstructor<Component> {
|
||||||
|
return getListableObjectComponent(this.object.getRenderTypes(), ViewMode.GridElement, undefined)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
<ds-collection-search-result-list-element [object]="object"
|
||||||
|
[index]="index"
|
||||||
|
[linkType]="linkType"
|
||||||
|
[listID]="listID"></ds-collection-search-result-list-element>
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-light mt-1" [routerLink]="[editPath]">
|
||||||
|
<i class="fa fa-edit"></i> {{"admin.search.collection.edit" | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
@@ -0,0 +1,60 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { CollectionAdminSearchResultListElementComponent } from './collection-admin-search-result-list-element.component';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
|
||||||
|
import { Collection } from '../../../../../core/shared/collection.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
|
||||||
|
|
||||||
|
describe('CollectionAdminSearchResultListElementComponent', () => {
|
||||||
|
let component: CollectionAdminSearchResultListElementComponent;
|
||||||
|
let fixture: ComponentFixture<CollectionAdminSearchResultListElementComponent>;
|
||||||
|
let id;
|
||||||
|
let searchResult;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
searchResult = new CollectionSearchResult();
|
||||||
|
searchResult.indexableObject = new Collection();
|
||||||
|
searchResult.indexableObject.uuid = id;
|
||||||
|
}
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [CollectionAdminSearchResultListElementComponent],
|
||||||
|
providers: [{ provide: TruncatableService, useValue: {} }],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CollectionAdminSearchResultListElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = searchResult;
|
||||||
|
component.linkTypes = CollectionElementLinkType;
|
||||||
|
component.index = 0;
|
||||||
|
component.viewModes = ViewMode;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render an edit button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a'));
|
||||||
|
const link = a.nativeElement.href;
|
||||||
|
expect(link).toContain(getCollectionEditPath(id));
|
||||||
|
})
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
|
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||||
|
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
|
||||||
|
import { Collection } from '../../../../../core/shared/collection.model';
|
||||||
|
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
|
||||||
|
|
||||||
|
@listableObjectComponent(CollectionSearchResult, ViewMode.ListElement, Context.AdminSearch)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-collection-admin-search-result-list-element',
|
||||||
|
styleUrls: ['./collection-admin-search-result-list-element.component.scss'],
|
||||||
|
templateUrl: './collection-admin-search-result-list-element.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a list element for a collection search result on the admin search page
|
||||||
|
*/
|
||||||
|
export class CollectionAdminSearchResultListElementComponent extends SearchResultListElementComponent<CollectionSearchResult, Collection> {
|
||||||
|
editPath: string;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.editPath = getCollectionEditPath(this.dso.uuid);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
<ds-community-search-result-list-element [object]="object"
|
||||||
|
[index]="index"
|
||||||
|
[linkType]="linkType"
|
||||||
|
[listID]="listID"></ds-community-search-result-list-element>
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-light mt-1" [routerLink]="[editPath]">
|
||||||
|
<i class="fa fa-edit"></i> {{"admin.search.community.edit" | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
@@ -0,0 +1,60 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { CommunityAdminSearchResultListElementComponent } from './community-admin-search-result-list-element.component';
|
||||||
|
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||||
|
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
|
||||||
|
import { Community } from '../../../../../core/shared/community.model';
|
||||||
|
|
||||||
|
describe('CommunityAdminSearchResultListElementComponent', () => {
|
||||||
|
let component: CommunityAdminSearchResultListElementComponent;
|
||||||
|
let fixture: ComponentFixture<CommunityAdminSearchResultListElementComponent>;
|
||||||
|
let id;
|
||||||
|
let searchResult;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
searchResult = new CommunitySearchResult();
|
||||||
|
searchResult.indexableObject = new Community();
|
||||||
|
searchResult.indexableObject.uuid = id;
|
||||||
|
}
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [CommunityAdminSearchResultListElementComponent],
|
||||||
|
providers: [{ provide: TruncatableService, useValue: {} }],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CommunityAdminSearchResultListElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = searchResult;
|
||||||
|
component.linkTypes = CollectionElementLinkType;
|
||||||
|
component.index = 0;
|
||||||
|
component.viewModes = ViewMode;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render an edit button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a'));
|
||||||
|
const link = a.nativeElement.href;
|
||||||
|
expect(link).toContain(getCommunityEditPath(id));
|
||||||
|
})
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
|
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||||
|
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||||
|
import { Community } from '../../../../../core/shared/community.model';
|
||||||
|
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
|
||||||
|
|
||||||
|
@listableObjectComponent(CommunitySearchResult, ViewMode.ListElement, Context.AdminSearch)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-community-admin-search-result-list-element',
|
||||||
|
styleUrls: ['./community-admin-search-result-list-element.component.scss'],
|
||||||
|
templateUrl: './community-admin-search-result-list-element.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a list element for a community search result on the admin search page
|
||||||
|
*/
|
||||||
|
export class CommunityAdminSearchResultListElementComponent extends SearchResultListElementComponent<CommunitySearchResult, Community> {
|
||||||
|
editPath: string;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.editPath = getCommunityEditPath(this.dso.uuid);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
<div *ngIf="dso && !dso.isDiscoverable" class="private-badge">
|
||||||
|
<span class="badge badge-danger">{{ "admin.search.item.private" | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="dso && dso.isWithdrawn" class="withdrawn-badge">
|
||||||
|
<span class="badge badge-warning">{{ "admin.search.item.withdrawn" | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<ds-listable-object-component-loader [object]="object"
|
||||||
|
[viewMode]="viewModes.ListElement"
|
||||||
|
[index]="index"
|
||||||
|
[linkType]="linkType"
|
||||||
|
[listID]="listID"></ds-listable-object-component-loader>
|
||||||
|
<ds-item-admin-search-result-actions-element [item]="dso" [small]="false"></ds-item-admin-search-result-actions-element>
|
@@ -0,0 +1,101 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
|
import { ItemAdminSearchResultListElementComponent } from './item-admin-search-result-list-element.component';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
|
||||||
|
describe('ItemAdminSearchResultListElementComponent', () => {
|
||||||
|
let component: ItemAdminSearchResultListElementComponent;
|
||||||
|
let fixture: ComponentFixture<ItemAdminSearchResultListElementComponent>;
|
||||||
|
let id;
|
||||||
|
let searchResult;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
searchResult = new ItemSearchResult();
|
||||||
|
searchResult.indexableObject = new Item();
|
||||||
|
searchResult.indexableObject.uuid = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [ItemAdminSearchResultListElementComponent],
|
||||||
|
providers: [{ provide: TruncatableService, useValue: {} }],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemAdminSearchResultListElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = searchResult;
|
||||||
|
component.linkTypes = CollectionElementLinkType;
|
||||||
|
component.index = 0;
|
||||||
|
component.viewModes = ViewMode;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the item is not withdrawn', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.dso.isWithdrawn = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show the withdrawn badge', () => {
|
||||||
|
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||||
|
expect(badge).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the item is withdrawn', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.dso.isWithdrawn = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the withdrawn badge', () => {
|
||||||
|
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||||
|
expect(badge).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the item is not private', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.dso.isDiscoverable = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should not show the private badge', () => {
|
||||||
|
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||||
|
expect(badge).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the item is private', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.dso.isDiscoverable = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the private badge', () => {
|
||||||
|
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||||
|
expect(badge).not.toBeNull();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
@@ -0,0 +1,20 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
|
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
|
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||||
|
|
||||||
|
@listableObjectComponent(ItemSearchResult, ViewMode.ListElement, Context.AdminSearch)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-admin-search-result-list-element',
|
||||||
|
styleUrls: ['./item-admin-search-result-list-element.component.scss'],
|
||||||
|
templateUrl: './item-admin-search-result-list-element.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a list element for an item search result on the admin search page
|
||||||
|
*/
|
||||||
|
export class ItemAdminSearchResultListElementComponent extends SearchResultListElementComponent<ItemSearchResult, Item> {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 edit-link" [routerLink]="[getEditPath()]" [title]="'admin.search.item.edit' | translate">
|
||||||
|
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-light my-1 withdraw-link" [routerLink]="[getWithdrawPath()]" [title]="'admin.search.item.withdraw' | translate">
|
||||||
|
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-light my-1 reinstate-link" [routerLink]="[getReinstatePath()]" [title]="'admin.search.item.reinstate' | translate">
|
||||||
|
<i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-light my-1 private-link" [routerLink]="[getPrivatePath()]" [title]="'admin.search.item.make-private' | translate">
|
||||||
|
<i class="fa fa-eye-slash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-light my-1 public-link" [routerLink]="[getPublicPath()]" [title]="'admin.search.item.make-public' | translate">
|
||||||
|
<i class="fa fa-eye"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeletePath()]" [title]="'admin.search.item.delete' | translate">
|
||||||
|
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 move-link" [routerLink]="[getMovePath()]" [title]="'admin.search.item.move' | translate">
|
||||||
|
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
|
||||||
|
</a>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,144 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { ItemAdminSearchResultActionsComponent } from './item-admin-search-result-actions.component';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import {
|
||||||
|
ITEM_EDIT_DELETE_PATH,
|
||||||
|
ITEM_EDIT_MOVE_PATH,
|
||||||
|
ITEM_EDIT_PRIVATE_PATH,
|
||||||
|
ITEM_EDIT_PUBLIC_PATH,
|
||||||
|
ITEM_EDIT_REINSTATE_PATH,
|
||||||
|
ITEM_EDIT_WITHDRAW_PATH
|
||||||
|
} from '../../../+item-page/edit-item-page/edit-item-page.routing.module';
|
||||||
|
import { getItemEditPath } from '../../../+item-page/item-page-routing.module';
|
||||||
|
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||||
|
|
||||||
|
describe('ItemAdminSearchResultActionsComponent', () => {
|
||||||
|
let component: ItemAdminSearchResultActionsComponent;
|
||||||
|
let fixture: ComponentFixture<ItemAdminSearchResultActionsComponent>;
|
||||||
|
let id;
|
||||||
|
let item;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
item = new Item();
|
||||||
|
item.uuid = id;
|
||||||
|
}
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [ItemAdminSearchResultActionsComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemAdminSearchResultActionsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.item = item;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render an edit button with the correct link', () => {
|
||||||
|
const button = fixture.debugElement.query(By.css('a.edit-link'));
|
||||||
|
const link = button.nativeElement.href;
|
||||||
|
expect(link).toContain(getItemEditPath(id));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a delete button with the correct link', () => {
|
||||||
|
const button = fixture.debugElement.query(By.css('a.delete-link'));
|
||||||
|
const link = button.nativeElement.href;
|
||||||
|
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_DELETE_PATH).toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a move button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.move-link'));
|
||||||
|
const link = a.nativeElement.href;
|
||||||
|
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_MOVE_PATH).toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the item is not withdrawn', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.item.isWithdrawn = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a withdraw button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.withdraw-link'));
|
||||||
|
const link = a.nativeElement.href;
|
||||||
|
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_WITHDRAW_PATH).toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render a reinstate button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.reinstate-link'));
|
||||||
|
expect(a).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the item is withdrawn', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.item.isWithdrawn = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render a withdraw button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.withdraw-link'));
|
||||||
|
expect(a).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a reinstate button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.reinstate-link'));
|
||||||
|
const link = a.nativeElement.href;
|
||||||
|
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_REINSTATE_PATH).toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the item is not private', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.item.isDiscoverable = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a make private button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.private-link'));
|
||||||
|
const link = a.nativeElement.href;
|
||||||
|
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_PRIVATE_PATH).toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render a make public button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.public-link'));
|
||||||
|
expect(a).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the item is private', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.item.isDiscoverable = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render a make private button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.private-link'));
|
||||||
|
expect(a).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a make private button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.public-link'));
|
||||||
|
const link = a.nativeElement.href;
|
||||||
|
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_PUBLIC_PATH).toString());
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
@@ -0,0 +1,81 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { getItemEditPath } from '../../../+item-page/item-page-routing.module';
|
||||||
|
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||||
|
import {
|
||||||
|
ITEM_EDIT_DELETE_PATH,
|
||||||
|
ITEM_EDIT_MOVE_PATH,
|
||||||
|
ITEM_EDIT_PRIVATE_PATH,
|
||||||
|
ITEM_EDIT_PUBLIC_PATH,
|
||||||
|
ITEM_EDIT_REINSTATE_PATH,
|
||||||
|
ITEM_EDIT_WITHDRAW_PATH
|
||||||
|
} from '../../../+item-page/edit-item-page/edit-item-page.routing.module';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-admin-search-result-actions-element',
|
||||||
|
styleUrls: ['./item-admin-search-result-actions.component.scss'],
|
||||||
|
templateUrl: './item-admin-search-result-actions.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying the actions for a list element for an item search result on the admin search page
|
||||||
|
*/
|
||||||
|
export class ItemAdminSearchResultActionsComponent {
|
||||||
|
/**
|
||||||
|
* The item to perform the actions on
|
||||||
|
*/
|
||||||
|
@Input() public item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to use small buttons
|
||||||
|
*/
|
||||||
|
@Input() public small: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the edit page of this item
|
||||||
|
*/
|
||||||
|
getEditPath(): string {
|
||||||
|
return getItemEditPath(this.item.uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the move page of this item
|
||||||
|
*/
|
||||||
|
getMovePath(): string {
|
||||||
|
return new URLCombiner(this.getEditPath(), ITEM_EDIT_MOVE_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the delete page of this item
|
||||||
|
*/
|
||||||
|
getDeletePath(): string {
|
||||||
|
return new URLCombiner(this.getEditPath(), ITEM_EDIT_DELETE_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the withdraw page of this item
|
||||||
|
*/
|
||||||
|
getWithdrawPath(): string {
|
||||||
|
return new URLCombiner(this.getEditPath(), ITEM_EDIT_WITHDRAW_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the reinstate page of this item
|
||||||
|
*/
|
||||||
|
getReinstatePath(): string {
|
||||||
|
return new URLCombiner(this.getEditPath(), ITEM_EDIT_REINSTATE_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the page where the user can make this item private
|
||||||
|
*/
|
||||||
|
getPrivatePath(): string {
|
||||||
|
return new URLCombiner(this.getEditPath(), ITEM_EDIT_PRIVATE_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the page where the user can make this item public
|
||||||
|
*/
|
||||||
|
getPublicPath(): string {
|
||||||
|
return new URLCombiner(this.getEditPath(), ITEM_EDIT_PUBLIC_PATH).toString();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,23 +1,23 @@
|
|||||||
import { Component, Injector, OnInit } from '@angular/core';
|
import { Component, Injector, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
|
||||||
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
|
||||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
|
||||||
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
|
|
||||||
import { MenuComponent } from '../../shared/menu/menu.component';
|
|
||||||
import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
|
|
||||||
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
|
||||||
import { first, map } from 'rxjs/operators';
|
|
||||||
import { combineLatest as combineLatestObservable } from 'rxjs';
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
|
import { combineLatest as combineLatestObservable } from 'rxjs';
|
||||||
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { first, map } from 'rxjs/operators';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
||||||
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-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 { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||||
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||||
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||||
import {CreateItemParentSelectorComponent} from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||||
|
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||||
|
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
|
||||||
|
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
|
||||||
|
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
|
||||||
|
import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
|
||||||
|
import { MenuComponent } from '../../shared/menu/menu.component';
|
||||||
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
|
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component representing the admin sidebar
|
* Component representing the admin sidebar
|
||||||
@@ -325,7 +325,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.access_control_people',
|
text: 'menu.section.access_control_people',
|
||||||
link: ''
|
link: '/admin/access-control/epeople'
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -350,53 +350,19 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
link: ''
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
|
/* Admin Search */
|
||||||
/* Search */
|
|
||||||
{
|
{
|
||||||
id: 'find',
|
id: 'admin_search',
|
||||||
active: false,
|
active: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.TEXT,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.find'
|
text: 'menu.section.admin_search',
|
||||||
} as TextMenuItemModel,
|
link: '/admin/search'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
icon: 'search',
|
icon: 'search',
|
||||||
index: 5
|
index: 5
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'find_items',
|
|
||||||
parentID: 'find',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.find_items',
|
|
||||||
link: '/search'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'find_withdrawn_items',
|
|
||||||
parentID: 'find',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.find_withdrawn_items',
|
|
||||||
link: ''
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'find_private_items',
|
|
||||||
parentID: 'find',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.find_private_items',
|
|
||||||
link: ''
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Registries */
|
/* Registries */
|
||||||
{
|
{
|
||||||
id: 'registries',
|
id: 'registries',
|
||||||
|
@@ -1,14 +1,45 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { AdminAccessControlModule } from './admin-access-control/admin-access-control.module';
|
||||||
import { AdminRegistriesModule } from './admin-registries/admin-registries.module';
|
import { AdminRegistriesModule } from './admin-registries/admin-registries.module';
|
||||||
import { AdminRoutingModule } from './admin-routing.module';
|
import { AdminRoutingModule } from './admin-routing.module';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
|
||||||
|
import { SearchPageModule } from '../+search-page/search-page.module';
|
||||||
|
import { ItemAdminSearchResultListElementComponent } from './admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component';
|
||||||
|
import { CommunityAdminSearchResultListElementComponent } from './admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component';
|
||||||
|
import { CollectionAdminSearchResultListElementComponent } from './admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component';
|
||||||
|
import { ItemAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component';
|
||||||
|
import { CommunityAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component';
|
||||||
|
import { CollectionAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component';
|
||||||
|
import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin-search-results/item-admin-search-result-actions.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
AdminRegistriesModule,
|
|
||||||
AdminRoutingModule,
|
AdminRoutingModule,
|
||||||
|
AdminRegistriesModule,
|
||||||
|
AdminAccessControlModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
SearchPageModule
|
||||||
],
|
],
|
||||||
|
declarations: [
|
||||||
|
AdminSearchPageComponent,
|
||||||
|
ItemAdminSearchResultListElementComponent,
|
||||||
|
CommunityAdminSearchResultListElementComponent,
|
||||||
|
CollectionAdminSearchResultListElementComponent,
|
||||||
|
ItemAdminSearchResultGridElementComponent,
|
||||||
|
CommunityAdminSearchResultGridElementComponent,
|
||||||
|
CollectionAdminSearchResultGridElementComponent,
|
||||||
|
ItemAdminSearchResultActionsComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
ItemAdminSearchResultListElementComponent,
|
||||||
|
CommunityAdminSearchResultListElementComponent,
|
||||||
|
CollectionAdminSearchResultListElementComponent,
|
||||||
|
ItemAdminSearchResultGridElementComponent,
|
||||||
|
CommunityAdminSearchResultGridElementComponent,
|
||||||
|
CollectionAdminSearchResultGridElementComponent,
|
||||||
|
ItemAdminSearchResultActionsComponent
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class AdminModule {
|
export class AdminModule {
|
||||||
|
|
||||||
|
@@ -20,7 +20,6 @@ import { Item } from '../../core/shared/item.model';
|
|||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { MockRouter } from '../../shared/mocks/mock-router';
|
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||||
import { ResourceType } from '../../core/shared/resource-type';
|
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
41
src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts
Normal file
41
src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Community } from '../core/shared/community.model';
|
||||||
|
import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service';
|
||||||
|
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||||
|
import { Collection } from '../core/shared/collection.model';
|
||||||
|
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../core/shared/operators';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
import { getDSOPath } from '../app-routing.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class that resolves the BreadcrumbConfig object for a DSpaceObject on a browse by page
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class BrowseByDSOBreadcrumbResolver {
|
||||||
|
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DSpaceObjectDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving a breadcrumb config object
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns BreadcrumbConfig object
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<Community | Collection>> {
|
||||||
|
const uuid = route.queryParams.scope;
|
||||||
|
if (hasValue(uuid)) {
|
||||||
|
return this.dataService.findById(uuid).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((object: Community | Collection) => {
|
||||||
|
return { provider: this.breadcrumbService, key: object, url: getDSOPath(object) };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
@@ -37,24 +37,24 @@ export class BrowseByGuard implements CanActivate {
|
|||||||
return dsoAndMetadata$.pipe(
|
return dsoAndMetadata$.pipe(
|
||||||
map((dsoRD) => {
|
map((dsoRD) => {
|
||||||
const name = dsoRD.payload.name;
|
const name = dsoRD.payload.name;
|
||||||
route.data = this.createData(title, id, metadataField, name, metadataTranslated, value);
|
route.data = this.createData(title, id, metadataField, name, metadataTranslated, value, route);
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
route.data = this.createData(title, id, metadataField, '', metadataTranslated, value);
|
route.data = this.createData(title, id, metadataField, '', metadataTranslated, value, route);
|
||||||
return observableOf(true);
|
return observableOf(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createData(title, id, metadataField, collection, field, value) {
|
private createData(title, id, metadataField, collection, field, value, route) {
|
||||||
return {
|
return Object.assign({}, route.data, {
|
||||||
title: title,
|
title: title,
|
||||||
id: id,
|
id: id,
|
||||||
metadataField: metadataField,
|
metadataField: metadataField,
|
||||||
collection: collection,
|
collection: collection,
|
||||||
field: field,
|
field: field,
|
||||||
value: hasValue(value) ? `"${value}"` : ''
|
value: hasValue(value) ? `"${value}"` : ''
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
src/app/+browse-by/browse-by-i18n-breadcrumb.resolver.ts
Normal file
28
src/app/+browse-by/browse-by-i18n-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
|
import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
||||||
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class resolves a BreadcrumbConfig object with an i18n key string for a route
|
||||||
|
* It adds the metadata field of the current browse-by page
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class BrowseByI18nBreadcrumbResolver extends I18nBreadcrumbResolver {
|
||||||
|
constructor(protected breadcrumbService: I18nBreadcrumbsService) {
|
||||||
|
super(breadcrumbService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving a browse-by i18n breadcrumb configuration object
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns BreadcrumbConfig object for a browse-by page
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig<string> {
|
||||||
|
const extendedBreadcrumbKey = route.data.breadcrumbKey + '.' + route.params.id;
|
||||||
|
route.data = Object.assign({}, route.data, { breadcrumbKey: extendedBreadcrumbKey });
|
||||||
|
return super.resolve(route, state);
|
||||||
|
}
|
||||||
|
}
|
@@ -2,12 +2,29 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { BrowseByGuard } from './browse-by-guard';
|
import { BrowseByGuard } from './browse-by-guard';
|
||||||
import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component';
|
import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component';
|
||||||
|
import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
|
||||||
|
import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{ path: ':id', component: BrowseBySwitcherComponent, canActivate: [BrowseByGuard], data: { title: 'browse.title' } }
|
{
|
||||||
])
|
path: '',
|
||||||
|
resolve: { breadcrumb: BrowseByDSOBreadcrumbResolver },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
component: BrowseBySwitcherComponent,
|
||||||
|
canActivate: [BrowseByGuard],
|
||||||
|
resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver },
|
||||||
|
data: { title: 'browse.title', breadcrumbKey: 'browse.metadata' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}])
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
BrowseByI18nBreadcrumbResolver,
|
||||||
|
BrowseByDSOBreadcrumbResolver
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class BrowseByRoutingModule {
|
export class BrowseByRoutingModule {
|
||||||
|
@@ -83,7 +83,7 @@ describe('CollectionItemMapperComponent', () => {
|
|||||||
const itemDataServiceStub = {
|
const itemDataServiceStub = {
|
||||||
mapToCollection: () => of(new RestResponse(true, 200, 'OK'))
|
mapToCollection: () => of(new RestResponse(true, 200, 'OK'))
|
||||||
};
|
};
|
||||||
const activatedRouteStub = new ActivatedRouteStub({}, { collection: mockCollectionRD });
|
const activatedRouteStub = new ActivatedRouteStub({}, { dso: mockCollectionRD });
|
||||||
const translateServiceStub = {
|
const translateServiceStub = {
|
||||||
get: () => of('test-message of collection ' + mockCollection.name),
|
get: () => of('test-message of collection ' + mockCollection.name),
|
||||||
onLangChange: new EventEmitter(),
|
onLangChange: new EventEmitter(),
|
||||||
|
@@ -102,7 +102,7 @@ export class CollectionItemMapperComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.collectionRD$ = this.route.data.pipe(map((data) => data.collection)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Collection>>;
|
this.collectionRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Collection>>;
|
||||||
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
||||||
this.loadItemLists();
|
this.loadItemLists();
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,9 @@ import { DeleteCollectionPageComponent } from './delete-collection-page/delete-c
|
|||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
import { getCollectionModulePath } from '../app-routing.module';
|
import { getCollectionModulePath } from '../app-routing.module';
|
||||||
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||||
|
import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver';
|
||||||
|
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||||
|
import { LinkService } from '../core/cache/builders/link.service';
|
||||||
|
|
||||||
export const COLLECTION_PARENT_PARAMETER = 'parent';
|
export const COLLECTION_PARENT_PARAMETER = 'parent';
|
||||||
|
|
||||||
@@ -18,7 +21,7 @@ export function getCollectionPageRoute(collectionId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getCollectionEditPath(id: string) {
|
export function getCollectionEditPath(id: string) {
|
||||||
return new URLCombiner(getCollectionModulePath(), COLLECTION_EDIT_PATH.replace(/:id/, id)).toString()
|
return new URLCombiner(getCollectionModulePath(), id, COLLECTION_EDIT_PATH).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCollectionCreatePath() {
|
export function getCollectionCreatePath() {
|
||||||
@@ -26,51 +29,55 @@ export function getCollectionCreatePath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const COLLECTION_CREATE_PATH = 'create';
|
const COLLECTION_CREATE_PATH = 'create';
|
||||||
const COLLECTION_EDIT_PATH = ':id/edit';
|
const COLLECTION_EDIT_PATH = 'edit';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
resolve: {
|
||||||
|
dso: CollectionPageResolver,
|
||||||
|
breadcrumb: CollectionBreadcrumbResolver
|
||||||
|
},
|
||||||
|
runGuardsAndResolvers: 'always',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: COLLECTION_EDIT_PATH,
|
||||||
|
loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule',
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'delete',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: DeleteCollectionPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CollectionPageComponent,
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/edit/mapper',
|
||||||
|
component: CollectionItemMapperComponent,
|
||||||
|
pathMatch: 'full',
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: COLLECTION_CREATE_PATH,
|
path: COLLECTION_CREATE_PATH,
|
||||||
component: CreateCollectionPageComponent,
|
component: CreateCollectionPageComponent,
|
||||||
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
|
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: COLLECTION_EDIT_PATH,
|
|
||||||
loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule',
|
|
||||||
canActivate: [AuthenticatedGuard]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':id/delete',
|
|
||||||
pathMatch: 'full',
|
|
||||||
component: DeleteCollectionPageComponent,
|
|
||||||
canActivate: [AuthenticatedGuard],
|
|
||||||
resolve: {
|
|
||||||
dso: CollectionPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':id',
|
|
||||||
component: CollectionPageComponent,
|
|
||||||
pathMatch: 'full',
|
|
||||||
resolve: {
|
|
||||||
collection: CollectionPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':id/edit/mapper',
|
|
||||||
component: CollectionItemMapperComponent,
|
|
||||||
pathMatch: 'full',
|
|
||||||
resolve: {
|
|
||||||
collection: CollectionPageResolver
|
|
||||||
},
|
|
||||||
canActivate: [AuthenticatedGuard]
|
|
||||||
}
|
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CollectionPageResolver,
|
CollectionPageResolver,
|
||||||
|
CollectionBreadcrumbResolver,
|
||||||
|
DSOBreadcrumbsService,
|
||||||
|
LinkService,
|
||||||
CreateCollectionPageGuard
|
CreateCollectionPageGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -62,7 +62,7 @@ export class CollectionPageComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.collectionRD$ = this.route.data.pipe(
|
this.collectionRD$ = this.route.data.pipe(
|
||||||
map((data) => data.collection as RemoteData<Collection>),
|
map((data) => data.dso as RemoteData<Collection>),
|
||||||
redirectToPageNotFoundOn404(this.router),
|
redirectToPageNotFoundOn404(this.router),
|
||||||
take(1)
|
take(1)
|
||||||
);
|
);
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { EditCollectionPageComponent } from './edit-collection-page.component';
|
import { EditCollectionPageComponent } from './edit-collection-page.component';
|
||||||
import { CollectionPageResolver } from '../collection-page.resolver';
|
|
||||||
import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component';
|
import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component';
|
||||||
import { CollectionRolesComponent } from './collection-roles/collection-roles.component';
|
import { CollectionRolesComponent } from './collection-roles/collection-roles.component';
|
||||||
import { CollectionSourceComponent } from './collection-source/collection-source.component';
|
import { CollectionSourceComponent } from './collection-source/collection-source.component';
|
||||||
import { CollectionCurateComponent } from './collection-curate/collection-curate.component';
|
import { CollectionCurateComponent } from './collection-curate/collection-curate.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing module that handles the routing for the Edit Collection page administrator functionality
|
* Routing module that handles the routing for the Edit Collection page administrator functionality
|
||||||
@@ -15,10 +15,11 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate
|
|||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: EditCollectionPageComponent,
|
|
||||||
resolve: {
|
resolve: {
|
||||||
dso: CollectionPageResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
|
data: { breadcrumbKey: 'collection.edit' },
|
||||||
|
component: EditCollectionPageComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
@@ -30,30 +31,28 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate
|
|||||||
component: CollectionMetadataComponent,
|
component: CollectionMetadataComponent,
|
||||||
data: {
|
data: {
|
||||||
title: 'collection.edit.tabs.metadata.title',
|
title: 'collection.edit.tabs.metadata.title',
|
||||||
hideReturnButton: true
|
hideReturnButton: true,
|
||||||
|
showBreadcrumbs: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'roles',
|
path: 'roles',
|
||||||
component: CollectionRolesComponent,
|
component: CollectionRolesComponent,
|
||||||
data: { title: 'collection.edit.tabs.roles.title' }
|
data: { title: 'collection.edit.tabs.roles.title', showBreadcrumbs: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'source',
|
path: 'source',
|
||||||
component: CollectionSourceComponent,
|
component: CollectionSourceComponent,
|
||||||
data: { title: 'collection.edit.tabs.source.title' }
|
data: { title: 'collection.edit.tabs.source.title', showBreadcrumbs: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'curate',
|
path: 'curate',
|
||||||
component: CollectionCurateComponent,
|
component: CollectionCurateComponent,
|
||||||
data: { title: 'collection.edit.tabs.curate.title' }
|
data: { title: 'collection.edit.tabs.curate.title', showBreadcrumbs: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
CollectionPageResolver,
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class EditCollectionPageRoutingModule {
|
export class EditCollectionPageRoutingModule {
|
||||||
|
@@ -9,6 +9,9 @@ import { CreateCommunityPageGuard } from './create-community-page/create-communi
|
|||||||
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
import { getCommunityModulePath } from '../app-routing.module';
|
import { getCommunityModulePath } from '../app-routing.module';
|
||||||
|
import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver';
|
||||||
|
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||||
|
import { LinkService } from '../core/cache/builders/link.service';
|
||||||
|
|
||||||
export const COMMUNITY_PARENT_PARAMETER = 'parent';
|
export const COMMUNITY_PARENT_PARAMETER = 'parent';
|
||||||
|
|
||||||
@@ -17,7 +20,7 @@ export function getCommunityPageRoute(communityId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getCommunityEditPath(id: string) {
|
export function getCommunityEditPath(id: string) {
|
||||||
return new URLCombiner(getCommunityModulePath(), COMMUNITY_EDIT_PATH.replace(/:id/, id)).toString()
|
return new URLCombiner(getCommunityModulePath(), id, COMMUNITY_EDIT_PATH).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCommunityCreatePath() {
|
export function getCommunityCreatePath() {
|
||||||
@@ -25,42 +28,49 @@ export function getCommunityCreatePath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const COMMUNITY_CREATE_PATH = 'create';
|
const COMMUNITY_CREATE_PATH = 'create';
|
||||||
const COMMUNITY_EDIT_PATH = ':id/edit';
|
const COMMUNITY_EDIT_PATH = 'edit';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
resolve: {
|
||||||
|
dso: CommunityPageResolver,
|
||||||
|
breadcrumb: CommunityBreadcrumbResolver
|
||||||
|
},
|
||||||
|
runGuardsAndResolvers: 'always',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: COMMUNITY_EDIT_PATH,
|
||||||
|
loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule',
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'delete',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: DeleteCommunityPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CommunityPageComponent,
|
||||||
|
pathMatch: 'full',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: COMMUNITY_CREATE_PATH,
|
path: COMMUNITY_CREATE_PATH,
|
||||||
component: CreateCommunityPageComponent,
|
component: CreateCommunityPageComponent,
|
||||||
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
|
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: COMMUNITY_EDIT_PATH,
|
|
||||||
loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule',
|
|
||||||
canActivate: [AuthenticatedGuard]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':id/delete',
|
|
||||||
pathMatch: 'full',
|
|
||||||
component: DeleteCommunityPageComponent,
|
|
||||||
canActivate: [AuthenticatedGuard],
|
|
||||||
resolve: {
|
|
||||||
dso: CommunityPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':id',
|
|
||||||
component: CommunityPageComponent,
|
|
||||||
pathMatch: 'full',
|
|
||||||
resolve: {
|
|
||||||
community: CommunityPageResolver
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CommunityPageResolver,
|
CommunityPageResolver,
|
||||||
|
CommunityBreadcrumbResolver,
|
||||||
|
DSOBreadcrumbsService,
|
||||||
|
LinkService,
|
||||||
CreateCommunityPageGuard
|
CreateCommunityPageGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -46,7 +46,7 @@ export class CommunityPageComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.communityRD$ = this.route.data.pipe(
|
this.communityRD$ = this.route.data.pipe(
|
||||||
map((data) => data.community as RemoteData<Community>),
|
map((data) => data.dso as RemoteData<Community>),
|
||||||
redirectToPageNotFoundOn404(this.router)
|
redirectToPageNotFoundOn404(this.router)
|
||||||
);
|
);
|
||||||
this.logoRD$ = this.communityRD$.pipe(
|
this.logoRD$ = this.communityRD$.pipe(
|
||||||
|
@@ -5,6 +5,7 @@ import { NgModule } from '@angular/core';
|
|||||||
import { CommunityMetadataComponent } from './community-metadata/community-metadata.component';
|
import { CommunityMetadataComponent } from './community-metadata/community-metadata.component';
|
||||||
import { CommunityRolesComponent } from './community-roles/community-roles.component';
|
import { CommunityRolesComponent } from './community-roles/community-roles.component';
|
||||||
import { CommunityCurateComponent } from './community-curate/community-curate.component';
|
import { CommunityCurateComponent } from './community-curate/community-curate.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing module that handles the routing for the Edit Community page administrator functionality
|
* Routing module that handles the routing for the Edit Community page administrator functionality
|
||||||
@@ -14,10 +15,11 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co
|
|||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: EditCommunityPageComponent,
|
|
||||||
resolve: {
|
resolve: {
|
||||||
dso: CommunityPageResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
|
data: { breadcrumbKey: 'community.edit' },
|
||||||
|
component: EditCommunityPageComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
@@ -29,26 +31,24 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co
|
|||||||
component: CommunityMetadataComponent,
|
component: CommunityMetadataComponent,
|
||||||
data: {
|
data: {
|
||||||
title: 'community.edit.tabs.metadata.title',
|
title: 'community.edit.tabs.metadata.title',
|
||||||
hideReturnButton: true
|
hideReturnButton: true,
|
||||||
|
showBreadcrumbs: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'roles',
|
path: 'roles',
|
||||||
component: CommunityRolesComponent,
|
component: CommunityRolesComponent,
|
||||||
data: { title: 'community.edit.tabs.roles.title' }
|
data: { title: 'community.edit.tabs.roles.title', showBreadcrumbs: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'curate',
|
path: 'curate',
|
||||||
component: CommunityCurateComponent,
|
component: CommunityCurateComponent,
|
||||||
data: { title: 'community.edit.tabs.curate.title' }
|
data: { title: 'community.edit.tabs.curate.title', showBreadcrumbs: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
|
||||||
CommunityPageResolver,
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class EditCommunityPageRoutingModule {
|
export class EditCommunityPageRoutingModule {
|
||||||
|
|
||||||
|
@@ -22,6 +22,7 @@ import { EditRelationshipComponent } from './item-relationships/edit-relationshi
|
|||||||
import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
|
import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
|
||||||
import { ItemMoveComponent } from './item-move/item-move.component';
|
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||||
import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component';
|
import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component';
|
||||||
|
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module that contains all components related to the Edit Item page administrator functionality
|
* Module that contains all components related to the Edit Item page administrator functionality
|
||||||
@@ -47,6 +48,7 @@ import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.co
|
|||||||
ItemMetadataComponent,
|
ItemMetadataComponent,
|
||||||
ItemRelationshipsComponent,
|
ItemRelationshipsComponent,
|
||||||
ItemBitstreamsComponent,
|
ItemBitstreamsComponent,
|
||||||
|
ItemVersionHistoryComponent,
|
||||||
EditInPlaceFieldComponent,
|
EditInPlaceFieldComponent,
|
||||||
EditRelationshipComponent,
|
EditRelationshipComponent,
|
||||||
EditRelationshipListComponent,
|
EditRelationshipListComponent,
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import { ItemPageResolver } from '../item-page.resolver';
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { EditItemPageComponent } from './edit-item-page.component';
|
import { EditItemPageComponent } from './edit-item-page.component';
|
||||||
@@ -13,13 +12,15 @@ import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.compo
|
|||||||
import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component';
|
import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component';
|
||||||
import { ItemMoveComponent } from './item-move/item-move.component';
|
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||||
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
||||||
|
|
||||||
const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
||||||
const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
export const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
||||||
const ITEM_EDIT_PRIVATE_PATH = 'private';
|
export const ITEM_EDIT_PRIVATE_PATH = 'private';
|
||||||
const ITEM_EDIT_PUBLIC_PATH = 'public';
|
export const ITEM_EDIT_PUBLIC_PATH = 'public';
|
||||||
const ITEM_EDIT_DELETE_PATH = 'delete';
|
export const ITEM_EDIT_DELETE_PATH = 'delete';
|
||||||
const ITEM_EDIT_MOVE_PATH = 'move';
|
export const ITEM_EDIT_MOVE_PATH = 'move';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing module that handles the routing for the Edit Item page administrator functionality
|
* Routing module that handles the routing for the Edit Item page administrator functionality
|
||||||
@@ -29,104 +30,93 @@ const ITEM_EDIT_MOVE_PATH = 'move';
|
|||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: EditItemPageComponent,
|
|
||||||
resolve: {
|
resolve: {
|
||||||
item: ItemPageResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
|
data: { breadcrumbKey: 'item.edit' },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
redirectTo: 'status',
|
component: EditItemPageComponent,
|
||||||
pathMatch: 'full'
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'status',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'status',
|
||||||
|
component: ItemStatusComponent,
|
||||||
|
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'bitstreams',
|
||||||
|
component: ItemBitstreamsComponent,
|
||||||
|
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'metadata',
|
||||||
|
component: ItemMetadataComponent,
|
||||||
|
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'relationships',
|
||||||
|
component: ItemRelationshipsComponent,
|
||||||
|
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'view',
|
||||||
|
/* TODO - change when view page exists */
|
||||||
|
component: ItemBitstreamsComponent,
|
||||||
|
data: { title: 'item.edit.tabs.view.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'curate',
|
||||||
|
/* TODO - change when curate page exists */
|
||||||
|
component: ItemBitstreamsComponent,
|
||||||
|
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'versionhistory',
|
||||||
|
component: ItemVersionHistoryComponent,
|
||||||
|
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'status',
|
path: 'mapper',
|
||||||
component: ItemStatusComponent,
|
component: ItemCollectionMapperComponent,
|
||||||
data: { title: 'item.edit.tabs.status.title' }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'bitstreams',
|
path: ITEM_EDIT_WITHDRAW_PATH,
|
||||||
component: ItemBitstreamsComponent,
|
component: ItemWithdrawComponent,
|
||||||
data: { title: 'item.edit.tabs.bitstreams.title' }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'metadata',
|
path: ITEM_EDIT_REINSTATE_PATH,
|
||||||
component: ItemMetadataComponent,
|
component: ItemReinstateComponent,
|
||||||
data: { title: 'item.edit.tabs.metadata.title' }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'relationships',
|
path: ITEM_EDIT_PRIVATE_PATH,
|
||||||
component: ItemRelationshipsComponent,
|
component: ItemPrivateComponent,
|
||||||
data: { title: 'item.edit.tabs.relationships.title' }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'view',
|
path: ITEM_EDIT_PUBLIC_PATH,
|
||||||
/* TODO - change when view page exists */
|
component: ItemPublicComponent,
|
||||||
component: ItemBitstreamsComponent,
|
|
||||||
data: { title: 'item.edit.tabs.view.title' }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'curate',
|
path: ITEM_EDIT_DELETE_PATH,
|
||||||
/* TODO - change when curate page exists */
|
component: ItemDeleteComponent,
|
||||||
component: ItemBitstreamsComponent,
|
|
||||||
data: { title: 'item.edit.tabs.curate.title' }
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ITEM_EDIT_MOVE_PATH,
|
||||||
|
component: ItemMoveComponent,
|
||||||
|
data: { title: 'item.edit.move.title' },
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
{
|
])
|
||||||
path: 'mapper',
|
|
||||||
component: ItemCollectionMapperComponent,
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ITEM_EDIT_WITHDRAW_PATH,
|
|
||||||
component: ItemWithdrawComponent,
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ITEM_EDIT_REINSTATE_PATH,
|
|
||||||
component: ItemReinstateComponent,
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ITEM_EDIT_PRIVATE_PATH,
|
|
||||||
component: ItemPrivateComponent,
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ITEM_EDIT_PUBLIC_PATH,
|
|
||||||
component: ItemPublicComponent,
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ITEM_EDIT_DELETE_PATH,
|
|
||||||
component: ItemDeleteComponent,
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ITEM_EDIT_MOVE_PATH,
|
|
||||||
component: ItemMoveComponent,
|
|
||||||
data: { title: 'item.edit.move.title' },
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
}])
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: []
|
||||||
ItemPageResolver,
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class EditItemPageRoutingModule {
|
export class EditItemPageRoutingModule {
|
||||||
|
|
||||||
|
@@ -220,7 +220,7 @@ describe('ItemDeleteComponent', () => {
|
|||||||
spyOn(comp, 'notify');
|
spyOn(comp, 'notify');
|
||||||
comp.performAction();
|
comp.performAction();
|
||||||
expect(mockItemDataService.delete)
|
expect(mockItemDataService.delete)
|
||||||
.toHaveBeenCalledWith(mockItem, types.filter((type) => typesSelection[type]).map((type) => type.id));
|
.toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id));
|
||||||
expect(comp.notify).toHaveBeenCalled();
|
expect(comp.notify).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -312,7 +312,7 @@ export class ItemDeleteComponent
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
).subscribe((types) => {
|
).subscribe((types) => {
|
||||||
this.itemDataService.delete(this.item, types).pipe(first()).subscribe(
|
this.itemDataService.delete(this.item.id, types).pipe(first()).subscribe(
|
||||||
(succeeded: boolean) => {
|
(succeeded: boolean) => {
|
||||||
this.notify(succeeded);
|
this.notify(succeeded);
|
||||||
}
|
}
|
||||||
@@ -322,7 +322,7 @@ export class ItemDeleteComponent
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* When the item is successfully delete, navigate to the homepage, otherwise navigate back to the item edit page
|
* When the item is successfully delete, navigate to the homepage, otherwise navigate back to the item edit page
|
||||||
* @param response
|
* @param succeeded
|
||||||
*/
|
*/
|
||||||
notify(succeeded: boolean) {
|
notify(succeeded: boolean) {
|
||||||
if (succeeded) {
|
if (succeeded) {
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="mt-4">
|
||||||
|
<ds-alert [content]="'item.edit.tabs.versionhistory.under-construction'" [type]="AlertTypeEnum.Warning"></ds-alert>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2" *ngVar="(itemRD$ | async)?.payload as item">
|
||||||
|
<ds-item-versions *ngIf="item" [item]="item" [displayWhenEmpty]="true" [displayTitle]="false"></ds-item-versions>
|
||||||
|
</div>
|
@@ -0,0 +1,44 @@
|
|||||||
|
import { ItemVersionHistoryComponent } from './item-version-history.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../../../shared/testing/utils';
|
||||||
|
|
||||||
|
describe('ItemVersionHistoryComponent', () => {
|
||||||
|
let component: ItemVersionHistoryComponent;
|
||||||
|
let fixture: ComponentFixture<ItemVersionHistoryComponent>;
|
||||||
|
|
||||||
|
const item = Object.assign(new Item(), {
|
||||||
|
uuid: 'item-identifier-1',
|
||||||
|
handle: '123456789/1',
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ItemVersionHistoryComponent, VarDirective],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ item: createSuccessfulRemoteDataObject(item) }) } } }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemVersionHistoryComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize the itemRD$ from the route\'s data', (done) => {
|
||||||
|
component.itemRD$.subscribe((itemRD) => {
|
||||||
|
expect(itemRD.payload).toBe(item);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,35 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-version-history',
|
||||||
|
templateUrl: './item-version-history.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for listing and managing an item's version history
|
||||||
|
*/
|
||||||
|
export class ItemVersionHistoryComponent {
|
||||||
|
/**
|
||||||
|
* The item to display the version history for
|
||||||
|
*/
|
||||||
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AlertType enumeration
|
||||||
|
* @type {AlertType}
|
||||||
|
*/
|
||||||
|
AlertTypeEnum = AlertType;
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.item)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="itemRD?.payload as item">
|
<div *ngIf="itemRD?.payload as item">
|
||||||
|
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
||||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||||
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
||||||
<div class="simple-view-link my-3">
|
<div class="simple-view-link my-3">
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
</table>
|
</table>
|
||||||
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
|
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
|
||||||
<ds-item-page-collections [item]="item"></ds-item-page-collections>
|
<ds-item-page-collections [item]="item"></ds-item-page-collections>
|
||||||
|
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||||
|
@@ -7,44 +7,56 @@ import { ItemPageResolver } from './item-page.resolver';
|
|||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
import { getItemModulePath } from '../app-routing.module';
|
import { getItemModulePath } from '../app-routing.module';
|
||||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
|
import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
|
||||||
|
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||||
|
import { LinkService } from '../core/cache/builders/link.service';
|
||||||
|
|
||||||
export function getItemPageRoute(itemId: string) {
|
export function getItemPageRoute(itemId: string) {
|
||||||
return new URLCombiner(getItemModulePath(), itemId).toString();
|
return new URLCombiner(getItemModulePath(), itemId).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getItemEditPath(id: string) {
|
export function getItemEditPath(id: string) {
|
||||||
return new URLCombiner(getItemModulePath(),ITEM_EDIT_PATH.replace(/:id/, id)).toString()
|
return new URLCombiner(getItemModulePath(), id, ITEM_EDIT_PATH).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const ITEM_EDIT_PATH = ':id/edit';
|
const ITEM_EDIT_PATH = 'edit';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: ItemPageComponent,
|
|
||||||
pathMatch: 'full',
|
|
||||||
resolve: {
|
resolve: {
|
||||||
item: ItemPageResolver
|
item: ItemPageResolver,
|
||||||
}
|
breadcrumb: ItemBreadcrumbResolver
|
||||||
},
|
},
|
||||||
{
|
runGuardsAndResolvers: 'always',
|
||||||
path: ':id/full',
|
children: [
|
||||||
component: FullItemPageComponent,
|
{
|
||||||
resolve: {
|
path: '',
|
||||||
item: ItemPageResolver
|
component: ItemPageComponent,
|
||||||
}
|
pathMatch: 'full',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ITEM_EDIT_PATH,
|
path: 'full',
|
||||||
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
component: FullItemPageComponent,
|
||||||
canActivate: [AuthenticatedGuard]
|
},
|
||||||
},
|
{
|
||||||
|
path: ITEM_EDIT_PATH,
|
||||||
|
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ItemPageResolver,
|
ItemPageResolver,
|
||||||
|
ItemBreadcrumbResolver,
|
||||||
|
DSOBreadcrumbsService,
|
||||||
|
LinkService
|
||||||
]
|
]
|
||||||
|
|
||||||
})
|
})
|
||||||
export class ItemPageRoutingModule {
|
export class ItemPageRoutingModule {
|
||||||
|
|
||||||
|
@@ -59,7 +59,7 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental-
|
|||||||
MetadataRepresentationListComponent,
|
MetadataRepresentationListComponent,
|
||||||
RelatedEntitiesSearchComponent,
|
RelatedEntitiesSearchComponent,
|
||||||
TabbedRelatedEntitiesSearchComponent,
|
TabbedRelatedEntitiesSearchComponent,
|
||||||
AbstractIncrementalListComponent
|
AbstractIncrementalListComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
ItemComponent,
|
ItemComponent,
|
||||||
|
@@ -27,7 +27,8 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
|||||||
return this.itemService.findById(route.params.id,
|
return this.itemService.findById(route.params.id,
|
||||||
followLink('owningCollection'),
|
followLink('owningCollection'),
|
||||||
followLink('bundles'),
|
followLink('bundles'),
|
||||||
followLink('relationships')
|
followLink('relationships'),
|
||||||
|
followLink('version', undefined, true, followLink('versionhistory')),
|
||||||
).pipe(
|
).pipe(
|
||||||
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||||
);
|
);
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="itemRD?.payload as item">
|
<div *ngIf="itemRD?.payload as item">
|
||||||
|
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
||||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
||||||
|
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||||
|
@@ -2,12 +2,14 @@ import { NgModule } from '@angular/core';
|
|||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { LoginPageComponent } from './login-page.component';
|
import { LoginPageComponent } from './login-page.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{ path: '', pathMatch: 'full', component: LoginPageComponent, data: { title: 'login.title' } }
|
{ path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } }
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class LoginPageRoutingModule { }
|
export class LoginPageRoutingModule {
|
||||||
|
}
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
<ds-search-labels [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
<ds-search-labels [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div id="search-body"
|
<div id="search-body"
|
||||||
class="row-offcanvas row-offcanvas-left"
|
class="row-offcanvas row-offcanvas-left w-100"
|
||||||
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
|
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
|
||||||
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
|
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
|
||||||
id="search-sidebar-sm"
|
id="search-sidebar-sm"
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { switchMap } from 'rxjs/operators';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||||
import { SearchComponent } from './search.component';
|
import { SearchComponent } from './search.component';
|
||||||
|
@@ -4,13 +4,24 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
||||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||||
import { SearchPageComponent } from './search-page.component';
|
import { SearchPageComponent } from './search-page.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([{
|
||||||
{ path: '', component: SearchPageComponent, data: { title: 'search.title' } },
|
path: '',
|
||||||
{ path: ':configuration', component: ConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard]}
|
resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'search' },
|
||||||
])
|
children: [
|
||||||
|
{ path: '', component: SearchPageComponent },
|
||||||
|
{ path: ':configuration', component: ConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard] }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
I18nBreadcrumbResolver,
|
||||||
|
I18nBreadcrumbsService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SearchPageRoutingModule {
|
export class SearchPageRoutingModule {
|
||||||
|
@@ -14,6 +14,7 @@ import { SearchPageComponent } from './search-page.component';
|
|||||||
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
|
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
|
||||||
import { SearchFilterService } from '../core/shared/search/search-filter.service';
|
import { SearchFilterService } from '../core/shared/search/search-filter.service';
|
||||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
const components = [
|
const components = [
|
||||||
SearchPageComponent,
|
SearchPageComponent,
|
||||||
@@ -28,7 +29,7 @@ const components = [
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
CoreModule.forRoot(),
|
CoreModule.forRoot(),
|
||||||
StatisticsModule.forRoot(),
|
StatisticsModule.forRoot()
|
||||||
],
|
],
|
||||||
declarations: components,
|
declarations: components,
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -1,54 +1,55 @@
|
|||||||
<div class="container" *ngIf="(isXsOrSm$ | async)">
|
<div class="container" *ngIf="(isXsOrSm$ | async)">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<ds-page-with-sidebar [id]="'search-page'" [sidebarContent]="sidebarContent">
|
<ds-page-with-sidebar [id]="'search-page'" [sidebarContent]="sidebarContent">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12" *ngIf="!(isXsOrSm$ | async)">
|
<div class="col-12" *ngIf="!(isXsOrSm$ | async)">
|
||||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
<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"
|
||||||
|
[context]="context"></ds-search-results>
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
</ds-page-with-sidebar>
|
||||||
|
|
||||||
<ng-template #sidebarContent>
|
<ng-template #sidebarContent>
|
||||||
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
||||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||||
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
||||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||||
(toggleSidebar)="closeSidebar()"
|
(toggleSidebar)="closeSidebar()"
|
||||||
>
|
>
|
||||||
</ds-search-sidebar>
|
</ds-search-sidebar>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #searchForm>
|
<ng-template #searchForm>
|
||||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||||
[query]="(searchOptions$ | async)?.query"
|
[query]="(searchOptions$ | async)?.query"
|
||||||
[scope]="(searchOptions$ | async)?.scope"
|
[scope]="(searchOptions$ | async)?.scope"
|
||||||
[currentUrl]="searchLink"
|
[currentUrl]="searchLink"
|
||||||
[scopes]="(scopeListRD$ | async)"
|
[scopes]="(scopeListRD$ | async)"
|
||||||
[inPlaceSearch]="inPlaceSearch">
|
[inPlaceSearch]="inPlaceSearch">
|
||||||
</ds-search-form>
|
</ds-search-form>
|
||||||
<div class="row mb-3 mb-md-1">
|
<div class="row mb-3 mb-md-1">
|
||||||
<div class="labels col-sm-9 offset-sm-3">
|
<div class="labels col-sm-9 offset-sm-3">
|
||||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@@ -17,6 +17,7 @@ import { SearchConfigurationService } from '../core/shared/search/search-configu
|
|||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
import { currentPath } from '../shared/utils/route.utils';
|
import { currentPath } from '../shared/utils/route.utils';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { Context } from '../core/shared/context.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search',
|
selector: 'ds-search',
|
||||||
@@ -84,6 +85,12 @@ export class SearchComponent implements OnInit {
|
|||||||
@Input()
|
@Input()
|
||||||
configuration$: Observable<string>;
|
configuration$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current context
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
context: Context;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Link to the search page
|
* Link to the search page
|
||||||
*/
|
*/
|
||||||
|
@@ -3,49 +3,96 @@ import { RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||||
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
||||||
|
import { DSpaceObject } from './core/shared/dspace-object.model';
|
||||||
|
import { Community } from './core/shared/community.model';
|
||||||
|
import { getCommunityPageRoute } from './+community-page/community-page-routing.module';
|
||||||
|
import { Collection } from './core/shared/collection.model';
|
||||||
|
import { Item } from './core/shared/item.model';
|
||||||
|
import { getItemPageRoute } from './+item-page/item-page-routing.module';
|
||||||
|
import { getCollectionPageRoute } from './+collection-page/collection-page-routing.module';
|
||||||
|
|
||||||
const ITEM_MODULE_PATH = 'items';
|
const ITEM_MODULE_PATH = 'items';
|
||||||
|
|
||||||
export function getItemModulePath() {
|
export function getItemModulePath() {
|
||||||
return `/${ITEM_MODULE_PATH}`;
|
return `/${ITEM_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLLECTION_MODULE_PATH = 'collections';
|
const COLLECTION_MODULE_PATH = 'collections';
|
||||||
|
|
||||||
export function getCollectionModulePath() {
|
export function getCollectionModulePath() {
|
||||||
return `/${COLLECTION_MODULE_PATH}`;
|
return `/${COLLECTION_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const COMMUNITY_MODULE_PATH = 'communities';
|
const COMMUNITY_MODULE_PATH = 'communities';
|
||||||
|
|
||||||
export function getCommunityModulePath() {
|
export function getCommunityModulePath() {
|
||||||
return `/${COMMUNITY_MODULE_PATH}`;
|
return `/${COMMUNITY_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADMIN_MODULE_PATH = 'admin';
|
const ADMIN_MODULE_PATH = 'admin';
|
||||||
|
|
||||||
export function getAdminModulePath() {
|
export function getAdminModulePath() {
|
||||||
return `/${ADMIN_MODULE_PATH}`;
|
return `/${ADMIN_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PROFILE_MODULE_PATH = 'profile';
|
||||||
|
|
||||||
|
export function getProfileModulePath() {
|
||||||
|
return `/${PROFILE_MODULE_PATH}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDSOPath(dso: DSpaceObject): string {
|
||||||
|
switch ((dso as any).type) {
|
||||||
|
case Community.type.value:
|
||||||
|
return getCommunityPageRoute(dso.uuid);
|
||||||
|
case Collection.type.value:
|
||||||
|
return getCollectionPageRoute(dso.uuid);
|
||||||
|
case Item.type.value:
|
||||||
|
return getItemPageRoute(dso.uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot([
|
RouterModule.forRoot([
|
||||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||||
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
|
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false } },
|
||||||
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
|
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
|
||||||
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||||
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||||
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
||||||
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
||||||
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
||||||
{ path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] },
|
{
|
||||||
|
path: 'mydspace',
|
||||||
|
loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule',
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
|
},
|
||||||
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
||||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
|
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
|
||||||
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
||||||
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||||
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
||||||
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
||||||
{ path: 'workspaceitems', loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' },
|
{
|
||||||
{ path: 'workflowitems', loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule' },
|
path: 'workspaceitems',
|
||||||
|
loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'workflowitems',
|
||||||
|
loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: PROFILE_MODULE_PATH,
|
||||||
|
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
|
||||||
|
},
|
||||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||||
])
|
],
|
||||||
|
{
|
||||||
|
onSameUrlNavigation: 'reload',
|
||||||
|
})
|
||||||
],
|
],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class AppRoutingModule {
|
export class AppRoutingModule {
|
||||||
|
|
||||||
|
@@ -10,6 +10,10 @@
|
|||||||
[options]="config.notifications">
|
[options]="config.notifications">
|
||||||
</ds-notifications-board>
|
</ds-notifications-board>
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
|
<div class="container">
|
||||||
|
<ds-breadcrumbs></ds-breadcrumbs>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container" *ngIf="isLoading$ | async">
|
<div class="container" *ngIf="isLoading$ | async">
|
||||||
<ds-loading message="{{'loading.default' | translate}}"></ds-loading>
|
<ds-loading message="{{'loading.default' | translate}}"></ds-loading>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,12 +3,11 @@ import { HttpClientModule } from '@angular/common/http';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||||
import { META_REDUCERS, MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
||||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||||
|
import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ import { AppRoutingModule } from './app-routing.module';
|
|||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
import { appEffects } from './app.effects';
|
import { appEffects } from './app.effects';
|
||||||
import { appMetaReducers, debugMetaReducers, universalMetaReducer } from './app.metareducers';
|
import { appMetaReducers, debugMetaReducers } from './app.metareducers';
|
||||||
import { appReducers, AppState } from './app.reducer';
|
import { appReducers, AppState } from './app.reducer';
|
||||||
|
|
||||||
import { CoreModule } from './core/core.module';
|
import { CoreModule } from './core/core.module';
|
||||||
@@ -39,6 +38,7 @@ import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-s
|
|||||||
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
||||||
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
||||||
import { SharedModule } from './shared/shared.module';
|
import { SharedModule } from './shared/shared.module';
|
||||||
|
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
return ENV_CONFIG;
|
return ENV_CONFIG;
|
||||||
@@ -97,7 +97,8 @@ const PROVIDERS = [
|
|||||||
provide: RouterStateSerializer,
|
provide: RouterStateSerializer,
|
||||||
useClass: DSpaceRouterStateSerializer
|
useClass: DSpaceRouterStateSerializer
|
||||||
},
|
},
|
||||||
ClientCookieService
|
ClientCookieService,
|
||||||
|
...DYNAMIC_MATCHER_PROVIDERS,
|
||||||
];
|
];
|
||||||
|
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
@@ -128,6 +129,7 @@ const EXPORTS = [
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
...DECLARATIONS,
|
...DECLARATIONS,
|
||||||
|
BreadcrumbsComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
...EXPORTS
|
...EXPORTS
|
||||||
|
@@ -1,30 +1,34 @@
|
|||||||
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
|
|
||||||
import * as fromRouter from '@ngrx/router-store';
|
import * as fromRouter from '@ngrx/router-store';
|
||||||
|
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
|
||||||
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
|
import {
|
||||||
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer';
|
ePeopleRegistryReducer,
|
||||||
import { formReducer, FormState } from './shared/form/form.reducer';
|
EPeopleRegistryState
|
||||||
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
|
} from './+admin/admin-access-control/epeople-registry/epeople-registry.reducers';
|
||||||
import { sidebarFilterReducer, SidebarFiltersState } from './shared/sidebar/filter/sidebar-filter.reducer';
|
|
||||||
import { filterReducer, SearchFiltersState } from './shared/search/search-filters/search-filter/search-filter.reducer';
|
|
||||||
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
|
|
||||||
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
|
||||||
import {
|
import {
|
||||||
metadataRegistryReducer,
|
metadataRegistryReducer,
|
||||||
MetadataRegistryState
|
MetadataRegistryState
|
||||||
} from './+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
} from './+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
||||||
|
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer';
|
||||||
import { hasValue } from './shared/empty.util';
|
import { hasValue } from './shared/empty.util';
|
||||||
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
import {
|
||||||
|
NameVariantListsState,
|
||||||
|
nameVariantReducer
|
||||||
|
} from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
|
||||||
|
import { formReducer, FormState } from './shared/form/form.reducer';
|
||||||
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
|
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
|
||||||
|
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
|
||||||
import {
|
import {
|
||||||
selectableListReducer,
|
selectableListReducer,
|
||||||
SelectableListsState
|
SelectableListsState
|
||||||
} from './shared/object-list/selectable-list/selectable-list.reducer';
|
} from './shared/object-list/selectable-list/selectable-list.reducer';
|
||||||
import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.reducer';
|
import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.reducer';
|
||||||
import {
|
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
||||||
NameVariantListsState,
|
|
||||||
nameVariantReducer
|
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
|
||||||
} from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
|
import { filterReducer, SearchFiltersState } from './shared/search/search-filters/search-filter/search-filter.reducer';
|
||||||
|
import { sidebarFilterReducer, SidebarFiltersState } from './shared/sidebar/filter/sidebar-filter.reducer';
|
||||||
|
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
|
||||||
|
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
router: fromRouter.RouterReducerState;
|
router: fromRouter.RouterReducerState;
|
||||||
@@ -42,6 +46,7 @@ export interface AppState {
|
|||||||
selectableLists: SelectableListsState;
|
selectableLists: SelectableListsState;
|
||||||
relationshipLists: NameVariantListsState;
|
relationshipLists: NameVariantListsState;
|
||||||
communityList: CommunityListState;
|
communityList: CommunityListState;
|
||||||
|
epeopleRegistry: EPeopleRegistryState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appReducers: ActionReducerMap<AppState> = {
|
export const appReducers: ActionReducerMap<AppState> = {
|
||||||
@@ -60,6 +65,7 @@ export const appReducers: ActionReducerMap<AppState> = {
|
|||||||
selectableLists: selectableListReducer,
|
selectableLists: selectableListReducer,
|
||||||
relationshipLists: nameVariantReducer,
|
relationshipLists: nameVariantReducer,
|
||||||
communityList: CommunityListReducer,
|
communityList: CommunityListReducer,
|
||||||
|
epeopleRegistry: ePeopleRegistryReducer,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const routerStateSelector = (state: AppState) => state.router;
|
export const routerStateSelector = (state: AppState) => state.router;
|
||||||
|
21
src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts
Normal file
21
src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { BreadcrumbsService } from '../../core/breadcrumbs/breadcrumbs.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for breadcrumb configuration objects
|
||||||
|
*/
|
||||||
|
export interface BreadcrumbConfig<T> {
|
||||||
|
/**
|
||||||
|
* The service used to calculate the breadcrumb object
|
||||||
|
*/
|
||||||
|
provider: BreadcrumbsService<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key that is used to calculate the breadcrumb display value
|
||||||
|
*/
|
||||||
|
key: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The url of the breadcrumb
|
||||||
|
*/
|
||||||
|
url?: string;
|
||||||
|
}
|
15
src/app/breadcrumbs/breadcrumb/breadcrumb.model.ts
Normal file
15
src/app/breadcrumbs/breadcrumb/breadcrumb.model.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Class representing a single breadcrumb
|
||||||
|
*/
|
||||||
|
export class Breadcrumb {
|
||||||
|
constructor(
|
||||||
|
/**
|
||||||
|
* The display value of the breadcrumb
|
||||||
|
*/
|
||||||
|
public text: string,
|
||||||
|
/**
|
||||||
|
* The optional url of the breadcrumb
|
||||||
|
*/
|
||||||
|
public url?: string) {
|
||||||
|
}
|
||||||
|
}
|
17
src/app/breadcrumbs/breadcrumbs.component.html
Normal file
17
src/app/breadcrumbs/breadcrumbs.component.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<nav *ngIf="showBreadcrumbs" aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<ng-container *ngTemplateOutlet="breadcrumbs.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'Home', url: '/'}"></ng-container>
|
||||||
|
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">
|
||||||
|
<ng-container *ngTemplateOutlet="!last ? breadcrumb : activeBreadcrumb; context: bc"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<ng-template #breadcrumb let-text="text" let-url="url">
|
||||||
|
<li class="breadcrumb-item"><a [routerLink]="url">{{text | translate}}</a></li>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #activeBreadcrumb let-text="text" >
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">{{text | translate}}</li>
|
||||||
|
</ng-template>
|
||||||
|
|
0
src/app/breadcrumbs/breadcrumbs.component.scss
Normal file
0
src/app/breadcrumbs/breadcrumbs.component.scss
Normal file
111
src/app/breadcrumbs/breadcrumbs.component.spec.ts
Normal file
111
src/app/breadcrumbs/breadcrumbs.component.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { BreadcrumbsComponent } from './breadcrumbs.component';
|
||||||
|
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { MockTranslateLoader } from '../shared/testing/mock-translate-loader';
|
||||||
|
import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model';
|
||||||
|
import { BreadcrumbsService } from '../core/breadcrumbs/breadcrumbs.service';
|
||||||
|
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
|
||||||
|
class TestBreadcrumbsService implements BreadcrumbsService<string> {
|
||||||
|
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> {
|
||||||
|
return observableOf([new Breadcrumb(key, url)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('BreadcrumbsComponent', () => {
|
||||||
|
let component: BreadcrumbsComponent;
|
||||||
|
let fixture: ComponentFixture<BreadcrumbsComponent>;
|
||||||
|
let router: any;
|
||||||
|
let route: any;
|
||||||
|
let breadcrumbProvider;
|
||||||
|
let breadcrumbConfigA: BreadcrumbConfig<string>;
|
||||||
|
let breadcrumbConfigB: BreadcrumbConfig<string>;
|
||||||
|
let expectedBreadcrumbs;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
breadcrumbProvider = new TestBreadcrumbsService();
|
||||||
|
|
||||||
|
breadcrumbConfigA = { provider: breadcrumbProvider, key: 'example.path', url: 'example.com' };
|
||||||
|
breadcrumbConfigB = { provider: breadcrumbProvider, key: 'another.path', url: 'another.com' };
|
||||||
|
|
||||||
|
route = {
|
||||||
|
root: {
|
||||||
|
snapshot: {
|
||||||
|
data: { breadcrumb: breadcrumbConfigA },
|
||||||
|
routeConfig: { resolve: { breadcrumb: {} } }
|
||||||
|
},
|
||||||
|
firstChild: {
|
||||||
|
snapshot: {
|
||||||
|
// Example without resolver should be ignored
|
||||||
|
data: { breadcrumb: breadcrumbConfigA },
|
||||||
|
},
|
||||||
|
firstChild: {
|
||||||
|
snapshot: {
|
||||||
|
data: { breadcrumb: breadcrumbConfigB },
|
||||||
|
routeConfig: { resolve: { breadcrumb: {} } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expectedBreadcrumbs = [
|
||||||
|
new Breadcrumb(breadcrumbConfigA.key, breadcrumbConfigA.url),
|
||||||
|
new Breadcrumb(breadcrumbConfigB.key, breadcrumbConfigB.url)
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [BreadcrumbsComponent],
|
||||||
|
imports: [RouterTestingModule.withRoutes([]), TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: route }
|
||||||
|
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BreadcrumbsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
router = TestBed.get(Router);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ngOnInit', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([]))
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call resolveBreadcrumb on init', () => {
|
||||||
|
router.events = observableOf(new NavigationEnd(0, '', ''));
|
||||||
|
component.ngOnInit();
|
||||||
|
expect(component.resolveBreadcrumbs).toHaveBeenCalledWith(route.root);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolveBreadcrumbs', () => {
|
||||||
|
it('should return the correct breadcrumbs', () => {
|
||||||
|
const breadcrumbs = component.resolveBreadcrumbs(route.root);
|
||||||
|
getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: expectedBreadcrumbs })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
100
src/app/breadcrumbs/breadcrumbs.component.ts
Normal file
100
src/app/breadcrumbs/breadcrumbs.component.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||||
|
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||||
|
import { hasNoValue, hasValue, isNotUndefined, isUndefined } from '../shared/empty.util';
|
||||||
|
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { combineLatest, Observable, Subscription, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component representing the breadcrumbs of a page
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-breadcrumbs',
|
||||||
|
templateUrl: './breadcrumbs.component.html',
|
||||||
|
styleUrls: ['./breadcrumbs.component.scss']
|
||||||
|
})
|
||||||
|
export class BreadcrumbsComponent implements OnInit, OnDestroy {
|
||||||
|
/**
|
||||||
|
* List of breadcrumbs for this page
|
||||||
|
*/
|
||||||
|
breadcrumbs: Breadcrumb[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to show breadcrumbs on this page
|
||||||
|
*/
|
||||||
|
showBreadcrumbs: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription to unsubscribe from on destroy
|
||||||
|
*/
|
||||||
|
subscription: Subscription;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the breadcrumbs on init for this page
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subscription = this.router.events.pipe(
|
||||||
|
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
||||||
|
tap(() => this.reset()),
|
||||||
|
switchMap(() => this.resolveBreadcrumbs(this.route.root))
|
||||||
|
).subscribe((breadcrumbs) => {
|
||||||
|
this.breadcrumbs = breadcrumbs;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that recursively resolves breadcrumbs
|
||||||
|
* @param route The route to get the breadcrumb from
|
||||||
|
*/
|
||||||
|
resolveBreadcrumbs(route: ActivatedRoute): Observable<Breadcrumb[]> {
|
||||||
|
const data = route.snapshot.data;
|
||||||
|
const routeConfig = route.snapshot.routeConfig;
|
||||||
|
|
||||||
|
const last: boolean = hasNoValue(route.firstChild);
|
||||||
|
if (last) {
|
||||||
|
if (hasValue(data.showBreadcrumbs)) {
|
||||||
|
this.showBreadcrumbs = data.showBreadcrumbs;
|
||||||
|
} else if (isUndefined(data.breadcrumb)) {
|
||||||
|
this.showBreadcrumbs = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasValue(data) && hasValue(data.breadcrumb) &&
|
||||||
|
hasValue(routeConfig) && hasValue(routeConfig.resolve) && hasValue(routeConfig.resolve.breadcrumb)
|
||||||
|
) {
|
||||||
|
const { provider, key, url } = data.breadcrumb;
|
||||||
|
if (!last) {
|
||||||
|
return combineLatest(provider.getBreadcrumbs(key, url), this.resolveBreadcrumbs(route.firstChild))
|
||||||
|
.pipe(map((crumbs) => [].concat.apply([], crumbs)));
|
||||||
|
} else {
|
||||||
|
return provider.getBreadcrumbs(key, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !last ? this.resolveBreadcrumbs(route.firstChild) : observableOf([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from subscription
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (hasValue(this.subscription)) {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the state of the breadcrumbs
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.breadcrumbs = [];
|
||||||
|
this.showBreadcrumbs = true;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,6 @@
|
|||||||
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
@@ -11,6 +10,7 @@ import { AuthGetRequest, AuthPostRequest, GetRequest, PostRequest, RestRequest }
|
|||||||
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { getResponseFromEntry } from '../shared/operators';
|
import { getResponseFromEntry } from '../shared/operators';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthRequestService {
|
export class AuthRequestService {
|
||||||
@@ -19,7 +19,8 @@ export class AuthRequestService {
|
|||||||
|
|
||||||
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected requestService: RequestService) {
|
protected requestService: RequestService,
|
||||||
|
private http: HttpClient) {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fetchRequest(request: RestRequest): Observable<any> {
|
protected fetchRequest(request: RestRequest): Observable<any> {
|
||||||
@@ -39,7 +40,7 @@ export class AuthRequestService {
|
|||||||
return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`;
|
return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> {
|
public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<any> {
|
||||||
return this.halService.getEndpoint(this.linkName).pipe(
|
return this.halService.getEndpoint(this.linkName).pipe(
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
|
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
|
||||||
@@ -68,4 +69,5 @@ export class AuthRequestService {
|
|||||||
mergeMap((request: GetRequest) => this.fetchRequest(request)),
|
mergeMap((request: GetRequest) => this.fetchRequest(request)),
|
||||||
distinctUntilChanged());
|
distinctUntilChanged());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user