mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge remote-tracking branch 'dspace/main' into accessibility-settings-main
# Conflicts: # src/app/profile-page/profile-page.component.html # src/app/profile-page/profile-page.component.ts # src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts # src/app/shared/notifications/notifications-board/notifications-board.component.ts # src/themes/custom/app/profile-page/profile-page.component.ts
This commit is contained in:
14
.github/workflows/build.yml
vendored
14
.github/workflows/build.yml
vendored
@@ -7,7 +7,8 @@ name: Build
|
|||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
packages: read # to fetch private images from GitHub Container Registry (GHCR)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
@@ -35,6 +36,9 @@ jobs:
|
|||||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||||
# Project name to use when running "docker compose" prior to e2e tests
|
# Project name to use when running "docker compose" prior to e2e tests
|
||||||
COMPOSE_PROJECT_NAME: 'ci'
|
COMPOSE_PROJECT_NAME: 'ci'
|
||||||
|
# Docker Registry to use for Docker compose scripts below.
|
||||||
|
# We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub.
|
||||||
|
DOCKER_REGISTRY: ghcr.io
|
||||||
strategy:
|
strategy:
|
||||||
# Create a matrix of Node versions to test against (in parallel)
|
# Create a matrix of Node versions to test against (in parallel)
|
||||||
matrix:
|
matrix:
|
||||||
@@ -114,6 +118,14 @@ jobs:
|
|||||||
path: 'coverage/dspace-angular/lcov.info'
|
path: 'coverage/dspace-angular/lcov.info'
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
|
# Login to our Docker registry, so that we can access private Docker images using "docker compose" below.
|
||||||
|
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.DOCKER_REGISTRY }}
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# Using "docker compose" start backend using CI configuration
|
# Using "docker compose" start backend using CI configuration
|
||||||
# and load assetstore from a cached copy
|
# and load assetstore from a cached copy
|
||||||
- name: Start DSpace REST Backend via Docker (for e2e tests)
|
- name: Start DSpace REST Backend via Docker (for e2e tests)
|
||||||
|
3
.github/workflows/docker.yml
vendored
3
.github/workflows/docker.yml
vendored
@@ -16,7 +16,8 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
packages: write # to write images to GitHub Container Registry (GHCR)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
#############################################################
|
#############################################################
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# This image will be published as dspace/dspace-angular
|
# This image will be published as dspace/dspace-angular
|
||||||
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
||||||
|
|
||||||
FROM node:18-alpine
|
FROM docker.io/node:18-alpine
|
||||||
|
|
||||||
# Ensure Python and other build tools are available
|
# Ensure Python and other build tools are available
|
||||||
# These are needed to install some node modules, especially on linux/arm64
|
# These are needed to install some node modules, especially on linux/arm64
|
||||||
@@ -22,5 +22,5 @@ ENV NODE_OPTIONS="--max_old_space_size=4096"
|
|||||||
# Listen / accept connections from all IP addresses.
|
# Listen / accept connections from all IP addresses.
|
||||||
# NOTE: At this time it is only possible to run Docker container in Production mode
|
# NOTE: At this time it is only possible to run Docker container in Production mode
|
||||||
# if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485
|
# if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485
|
||||||
ENV NODE_ENV development
|
ENV NODE_ENV=development
|
||||||
CMD npm run serve -- --host 0.0.0.0
|
CMD npm run serve -- --host 0.0.0.0
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
# Test build:
|
# Test build:
|
||||||
# docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist .
|
# docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist .
|
||||||
|
|
||||||
FROM node:18-alpine AS build
|
FROM docker.io/node:18-alpine AS build
|
||||||
|
|
||||||
# Ensure Python and other build tools are available
|
# Ensure Python and other build tools are available
|
||||||
# These are needed to install some node modules, especially on linux/arm64
|
# These are needed to install some node modules, especially on linux/arm64
|
||||||
@@ -26,6 +26,6 @@ COPY --chown=node:node docker/dspace-ui.json /app/dspace-ui.json
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
USER node
|
USER node
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV=production
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
CMD pm2-runtime start dspace-ui.json --json
|
CMD pm2-runtime start dspace-ui.json --json
|
||||||
|
@@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
|
|||||||
Quick start
|
Quick start
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
**Ensure you're running [Node](https://nodejs.org) `v16.x` or `v18.x`, [npm](https://www.npmjs.com/) >= `v5.x`**
|
**Ensure you're running [Node](https://nodejs.org) `v18.x` or `v20.x`, [npm](https://www.npmjs.com/) >= `v10.x`**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# clone the repo
|
# clone the repo
|
||||||
@@ -90,7 +90,7 @@ Requirements
|
|||||||
------------
|
------------
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org)
|
- [Node.js](https://nodejs.org)
|
||||||
- Ensure you're running node `v16.x` or `v18.x`
|
- Ensure you're running node `v18.x` or `v20.x`
|
||||||
|
|
||||||
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# NOTE: will log all redux actions and transfers in console
|
# NOTE: will log all redux actions and transfers in console
|
||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
# Angular Universal server settings
|
# Angular User Inteface settings
|
||||||
# NOTE: these settings define where Node.js will start your UI application. Therefore, these
|
# NOTE: these settings define where Node.js will start your UI application. Therefore, these
|
||||||
# "ui" settings usually specify a localhost port/URL which is later proxied to a public URL (using Apache or similar)
|
# "ui" settings usually specify a localhost port/URL which is later proxied to a public URL (using Apache or similar)
|
||||||
ui:
|
ui:
|
||||||
@@ -17,12 +17,14 @@ ui:
|
|||||||
# Trust X-FORWARDED-* headers from proxies (default = true)
|
# Trust X-FORWARDED-* headers from proxies (default = true)
|
||||||
useProxies: true
|
useProxies: true
|
||||||
|
|
||||||
universal:
|
# Angular Server Side Rendering (SSR) settings
|
||||||
# Whether to inline "critical" styles into the server-side rendered HTML.
|
ssr:
|
||||||
# Determining which styles are critical is a relatively expensive operation;
|
# Whether to tell Angular to inline "critical" styles into the server-side rendered HTML.
|
||||||
# this option can be disabled to boost server performance at the expense of
|
# Determining which styles are critical is a relatively expensive operation; this option is
|
||||||
# loading smoothness.
|
# disabled (false) by default to boost server performance at the expense of loading smoothness.
|
||||||
inlineCriticalCss: true
|
inlineCriticalCss: false
|
||||||
|
# Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects.
|
||||||
|
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ]
|
||||||
|
|
||||||
# The REST API server settings
|
# The REST API server settings
|
||||||
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
||||||
|
@@ -9,10 +9,12 @@ describe('Admin Add New Modals', () => {
|
|||||||
|
|
||||||
it('Add new Community modal should pass accessibility tests', () => {
|
it('Add new Community modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
cy.get('#admin-menu-section-new-title').click();
|
cy.get('[data-test="admin-menu-section-new-title"]').should('be.visible');
|
||||||
|
cy.get('[data-test="admin-menu-section-new-title"]').click();
|
||||||
|
|
||||||
cy.get('a[data-test="menu.section.new_community"]').click();
|
cy.get('a[data-test="menu.section.new_community"]').click();
|
||||||
|
|
||||||
@@ -22,10 +24,12 @@ describe('Admin Add New Modals', () => {
|
|||||||
|
|
||||||
it('Add new Collection modal should pass accessibility tests', () => {
|
it('Add new Collection modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
cy.get('#admin-menu-section-new-title').click();
|
cy.get('[data-test="admin-menu-section-new-title"]').should('be.visible');
|
||||||
|
cy.get('[data-test="admin-menu-section-new-title"]').click();
|
||||||
|
|
||||||
cy.get('a[data-test="menu.section.new_collection"]').click();
|
cy.get('a[data-test="menu.section.new_collection"]').click();
|
||||||
|
|
||||||
@@ -35,10 +39,12 @@ describe('Admin Add New Modals', () => {
|
|||||||
|
|
||||||
it('Add new Item modal should pass accessibility tests', () => {
|
it('Add new Item modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
cy.get('#admin-menu-section-new-title').click();
|
cy.get('[data-test="admin-menu-section-new-title"]').should('be.visible');
|
||||||
|
cy.get('[data-test="admin-menu-section-new-title"]').click();
|
||||||
|
|
||||||
cy.get('a[data-test="menu.section.new_item"]').click();
|
cy.get('a[data-test="menu.section.new_item"]').click();
|
||||||
|
|
||||||
|
@@ -9,10 +9,12 @@ describe('Admin Edit Modals', () => {
|
|||||||
|
|
||||||
it('Edit Community modal should pass accessibility tests', () => {
|
it('Edit Community modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
cy.get('#admin-menu-section-edit-title').click();
|
cy.get('[data-test="admin-menu-section-edit-title"]').should('be.visible');
|
||||||
|
cy.get('[data-test="admin-menu-section-edit-title"]').click();
|
||||||
|
|
||||||
cy.get('a[data-test="menu.section.edit_community"]').click();
|
cy.get('a[data-test="menu.section.edit_community"]').click();
|
||||||
|
|
||||||
@@ -22,10 +24,12 @@ describe('Admin Edit Modals', () => {
|
|||||||
|
|
||||||
it('Edit Collection modal should pass accessibility tests', () => {
|
it('Edit Collection modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
cy.get('#admin-menu-section-edit-title').click();
|
cy.get('[data-test="admin-menu-section-edit-title"]').should('be.visible');
|
||||||
|
cy.get('[data-test="admin-menu-section-edit-title"]').click();
|
||||||
|
|
||||||
cy.get('a[data-test="menu.section.edit_collection"]').click();
|
cy.get('a[data-test="menu.section.edit_collection"]').click();
|
||||||
|
|
||||||
@@ -35,10 +39,12 @@ describe('Admin Edit Modals', () => {
|
|||||||
|
|
||||||
it('Edit Item modal should pass accessibility tests', () => {
|
it('Edit Item modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
cy.get('#admin-menu-section-edit-title').click();
|
cy.get('[data-test="admin-menu-section-edit-title"]').should('be.visible');
|
||||||
|
cy.get('[data-test="admin-menu-section-edit-title"]').click();
|
||||||
|
|
||||||
cy.get('a[data-test="menu.section.edit_item"]').click();
|
cy.get('a[data-test="menu.section.edit_item"]').click();
|
||||||
|
|
||||||
|
@@ -9,10 +9,12 @@ describe('Admin Export Modals', () => {
|
|||||||
|
|
||||||
it('Export metadata modal should pass accessibility tests', () => {
|
it('Export metadata modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
cy.get('#admin-menu-section-export-title').click();
|
cy.get('[data-test="admin-menu-section-export-title"]').should('be.visible');
|
||||||
|
cy.get('[data-test="admin-menu-section-export-title"]').click();
|
||||||
|
|
||||||
cy.get('a[data-test="menu.section.export_metadata"]').click();
|
cy.get('a[data-test="menu.section.export_metadata"]').click();
|
||||||
|
|
||||||
@@ -22,10 +24,12 @@ describe('Admin Export Modals', () => {
|
|||||||
|
|
||||||
it('Export batch modal should pass accessibility tests', () => {
|
it('Export batch modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
cy.get('#admin-menu-section-export-title').click();
|
cy.get('[data-test="admin-menu-section-export-title"]').should('be.visible');
|
||||||
|
cy.get('[data-test="admin-menu-section-export-title"]').click();
|
||||||
|
|
||||||
cy.get('a[data-test="menu.section.export_batch"]').click();
|
cy.get('a[data-test="menu.section.export_batch"]').click();
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ describe('Admin Sidebar', () => {
|
|||||||
|
|
||||||
it('should be pinnable and pass accessibility tests', () => {
|
it('should be pinnable and pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
// Click on every expandable section to open all menus
|
// Click on every expandable section to open all menus
|
||||||
cy.get('ds-expandable-admin-sidebar-section').click({ multiple: true });
|
cy.get('ds-expandable-admin-sidebar-section').click({ multiple: true });
|
||||||
|
@@ -9,18 +9,15 @@ beforeEach(() => {
|
|||||||
|
|
||||||
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
||||||
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
|
||||||
// We need to wait for the correction types allowed for the item to be loaded to be sure that each tab is fully loaded.
|
|
||||||
// This because the edit item page causes often tests to fails due to timeout.
|
|
||||||
cy.intercept('GET', 'server/api/config/correctiontypes/search/findByItem*').as('correctionTypes');
|
|
||||||
cy.wait('@correctionTypes');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Edit Item > Edit Metadata tab', () => {
|
describe('Edit Item > Edit Metadata tab', () => {
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="metadata"]').should('be.visible');
|
||||||
cy.get('a[data-test="metadata"]').click();
|
cy.get('a[data-test="metadata"]').click();
|
||||||
|
|
||||||
// Our selected tab should be active
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="metadata"]').should('be.visible');
|
||||||
cy.get('a[data-test="metadata"]').should('have.class', 'active');
|
cy.get('a[data-test="metadata"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-edit-item-page> tag must be loaded
|
// <ds-edit-item-page> tag must be loaded
|
||||||
@@ -39,9 +36,11 @@ describe('Edit Item > Edit Metadata tab', () => {
|
|||||||
describe('Edit Item > Status tab', () => {
|
describe('Edit Item > Status tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="status"]').should('be.visible');
|
||||||
cy.get('a[data-test="status"]').click();
|
cy.get('a[data-test="status"]').click();
|
||||||
|
|
||||||
// Our selected tab should be active
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="status"]').should('be.visible');
|
||||||
cy.get('a[data-test="status"]').should('have.class', 'active');
|
cy.get('a[data-test="status"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-status> tag must be loaded
|
// <ds-item-status> tag must be loaded
|
||||||
@@ -55,9 +54,11 @@ describe('Edit Item > Status tab', () => {
|
|||||||
describe('Edit Item > Bitstreams tab', () => {
|
describe('Edit Item > Bitstreams tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="bitstreams"]').should('be.visible');
|
||||||
cy.get('a[data-test="bitstreams"]').click();
|
cy.get('a[data-test="bitstreams"]').click();
|
||||||
|
|
||||||
// Our selected tab should be active
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="bitstreams"]').should('be.visible');
|
||||||
cy.get('a[data-test="bitstreams"]').should('have.class', 'active');
|
cy.get('a[data-test="bitstreams"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-bitstreams> tag must be loaded
|
// <ds-item-bitstreams> tag must be loaded
|
||||||
@@ -82,9 +83,11 @@ describe('Edit Item > Bitstreams tab', () => {
|
|||||||
describe('Edit Item > Curate tab', () => {
|
describe('Edit Item > Curate tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="curate"]').should('be.visible');
|
||||||
cy.get('a[data-test="curate"]').click();
|
cy.get('a[data-test="curate"]').click();
|
||||||
|
|
||||||
// Our selected tab should be active
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="curate"]').should('be.visible');
|
||||||
cy.get('a[data-test="curate"]').should('have.class', 'active');
|
cy.get('a[data-test="curate"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-curate> tag must be loaded
|
// <ds-item-curate> tag must be loaded
|
||||||
@@ -98,9 +101,11 @@ describe('Edit Item > Curate tab', () => {
|
|||||||
describe('Edit Item > Relationships tab', () => {
|
describe('Edit Item > Relationships tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="relationships"]').should('be.visible');
|
||||||
cy.get('a[data-test="relationships"]').click();
|
cy.get('a[data-test="relationships"]').click();
|
||||||
|
|
||||||
// Our selected tab should be active
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="relationships"]').should('be.visible');
|
||||||
cy.get('a[data-test="relationships"]').should('have.class', 'active');
|
cy.get('a[data-test="relationships"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-relationships> tag must be loaded
|
// <ds-item-relationships> tag must be loaded
|
||||||
@@ -114,9 +119,11 @@ describe('Edit Item > Relationships tab', () => {
|
|||||||
describe('Edit Item > Version History tab', () => {
|
describe('Edit Item > Version History tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="versionhistory"]').should('be.visible');
|
||||||
cy.get('a[data-test="versionhistory"]').click();
|
cy.get('a[data-test="versionhistory"]').click();
|
||||||
|
|
||||||
// Our selected tab should be active
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="versionhistory"]').should('be.visible');
|
||||||
cy.get('a[data-test="versionhistory"]').should('have.class', 'active');
|
cy.get('a[data-test="versionhistory"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-version-history> tag must be loaded
|
// <ds-item-version-history> tag must be loaded
|
||||||
@@ -130,9 +137,11 @@ describe('Edit Item > Version History tab', () => {
|
|||||||
describe('Edit Item > Access Control tab', () => {
|
describe('Edit Item > Access Control tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="access-control"]').should('be.visible');
|
||||||
cy.get('a[data-test="access-control"]').click();
|
cy.get('a[data-test="access-control"]').click();
|
||||||
|
|
||||||
// Our selected tab should be active
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="access-control"]').should('be.visible');
|
||||||
cy.get('a[data-test="access-control"]').should('have.class', 'active');
|
cy.get('a[data-test="access-control"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-access-control> tag must be loaded
|
// <ds-item-access-control> tag must be loaded
|
||||||
@@ -146,9 +155,11 @@ describe('Edit Item > Access Control tab', () => {
|
|||||||
describe('Edit Item > Collection Mapper tab', () => {
|
describe('Edit Item > Collection Mapper tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="mapper"]').should('be.visible');
|
||||||
cy.get('a[data-test="mapper"]').click();
|
cy.get('a[data-test="mapper"]').click();
|
||||||
|
|
||||||
// Our selected tab should be active
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="mapper"]').should('be.visible');
|
||||||
cy.get('a[data-test="mapper"]').should('have.class', 'active');
|
cy.get('a[data-test="mapper"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-collection-mapper> tag must be loaded
|
// <ds-item-collection-mapper> tag must be loaded
|
||||||
|
@@ -54,9 +54,9 @@ before(() => {
|
|||||||
|
|
||||||
// Runs once before the first test in each "block"
|
// Runs once before the first test in each "block"
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
|
// Pre-agree to all Orejime cookies by setting the orejime-anonymous cookie
|
||||||
// This just ensures it doesn't get in the way of matching other objects in the page.
|
// This just ensures it doesn't get in the way of matching other objects in the page.
|
||||||
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true%2C%22google-recaptcha%22:true}');
|
cy.setCookie('orejime-anonymous', '{"authentication":true,"preferences":true,"acknowledgement":true,"google-analytics":true}');
|
||||||
|
|
||||||
// Remove any CSRF cookies saved from prior tests
|
// Remove any CSRF cookies saved from prior tests
|
||||||
cy.clearCookie(DSPACE_XSRF_COOKIE);
|
cy.clearCookie(DSPACE_XSRF_COOKIE);
|
||||||
|
@@ -21,7 +21,7 @@ networks:
|
|||||||
external: true
|
external: true
|
||||||
services:
|
services:
|
||||||
dspace-cli:
|
dspace-cli:
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}"
|
||||||
container_name: dspace-cli
|
container_name: dspace-cli
|
||||||
environment:
|
environment:
|
||||||
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
# # Therefore, it should be kept in sync with that file
|
# # Therefore, it should be kept in sync with that file
|
||||||
services:
|
services:
|
||||||
dspacedb:
|
dspacedb:
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql"
|
||||||
environment:
|
environment:
|
||||||
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
|
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
|
||||||
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||||
|
@@ -33,7 +33,7 @@ services:
|
|||||||
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
|
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
|
||||||
solr__D__statistics__P__autoCommit: 'false'
|
solr__D__statistics__P__autoCommit: 'false'
|
||||||
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
networks:
|
networks:
|
||||||
@@ -60,7 +60,7 @@ services:
|
|||||||
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
||||||
dspacedb:
|
dspacedb:
|
||||||
container_name: dspacedb
|
container_name: dspacedb
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql"
|
||||||
environment:
|
environment:
|
||||||
# This LOADSQL should be kept in sync with the LOADSQL in
|
# This LOADSQL should be kept in sync with the LOADSQL in
|
||||||
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
|
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
|
||||||
@@ -81,7 +81,7 @@ services:
|
|||||||
# DSpace Solr container
|
# DSpace Solr container
|
||||||
dspacesolr:
|
dspacesolr:
|
||||||
container_name: dspacesolr
|
container_name: dspacesolr
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
|
||||||
networks:
|
networks:
|
||||||
- dspacenet
|
- dspacenet
|
||||||
ports:
|
ports:
|
||||||
|
@@ -26,7 +26,7 @@ services:
|
|||||||
DSPACE_REST_HOST: sandbox.dspace.org
|
DSPACE_REST_HOST: sandbox.dspace.org
|
||||||
DSPACE_REST_PORT: 443
|
DSPACE_REST_PORT: 443
|
||||||
DSPACE_REST_NAMESPACE: /server
|
DSPACE_REST_NAMESPACE: /server
|
||||||
image: dspace/dspace-angular:${DSPACE_VER:-latest}-dist
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-latest}-dist"
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: Dockerfile.dist
|
dockerfile: Dockerfile.dist
|
||||||
|
@@ -40,7 +40,7 @@ services:
|
|||||||
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
||||||
proxies__P__trusted__P__ipranges: '172.23.0'
|
proxies__P__trusted__P__ipranges: '172.23.0'
|
||||||
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
networks:
|
networks:
|
||||||
@@ -68,7 +68,7 @@ services:
|
|||||||
dspacedb:
|
dspacedb:
|
||||||
container_name: dspacedb
|
container_name: dspacedb
|
||||||
# Uses a custom Postgres image with pgcrypto installed
|
# Uses a custom Postgres image with pgcrypto installed
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
|
||||||
environment:
|
environment:
|
||||||
PGDATA: /pgdata
|
PGDATA: /pgdata
|
||||||
POSTGRES_PASSWORD: dspace
|
POSTGRES_PASSWORD: dspace
|
||||||
@@ -85,7 +85,7 @@ services:
|
|||||||
# DSpace Solr container
|
# DSpace Solr container
|
||||||
dspacesolr:
|
dspacesolr:
|
||||||
container_name: dspacesolr
|
container_name: dspacesolr
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
|
||||||
networks:
|
networks:
|
||||||
- dspacenet
|
- dspacenet
|
||||||
ports:
|
ports:
|
||||||
|
@@ -23,7 +23,7 @@ services:
|
|||||||
DSPACE_REST_HOST: localhost
|
DSPACE_REST_HOST: localhost
|
||||||
DSPACE_REST_PORT: 8080
|
DSPACE_REST_PORT: 8080
|
||||||
DSPACE_REST_NAMESPACE: /server
|
DSPACE_REST_NAMESPACE: /server
|
||||||
image: dspace/dspace-angular:${DSPACE_VER:-latest}
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-latest}"
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
829
package-lock.json
generated
829
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -119,14 +119,14 @@
|
|||||||
"@ngx-translate/core": "^14.0.0",
|
"@ngx-translate/core": "^14.0.0",
|
||||||
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
|
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
|
||||||
"angulartics2": "^12.2.0",
|
"angulartics2": "^12.2.0",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.9",
|
||||||
"bootstrap": "^4.6.1",
|
"bootstrap": "^4.6.1",
|
||||||
"cerialize": "0.1.18",
|
"cerialize": "0.1.18",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.5",
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"core-js": "^3.38.1",
|
"core-js": "^3.39.0",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"date-fns-tz": "^1.3.7",
|
"date-fns-tz": "^1.3.7",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
@@ -137,17 +137,16 @@
|
|||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
"http-proxy-middleware": "^2.0.7",
|
"http-proxy-middleware": "^2.0.7",
|
||||||
"http-terminator": "^3.2.0",
|
"http-terminator": "^3.2.0",
|
||||||
"isbot": "^5.1.17",
|
"isbot": "^5.1.21",
|
||||||
"js-cookie": "2.2.1",
|
"js-cookie": "2.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"jsonschema": "1.4.1",
|
"jsonschema": "1.4.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"klaro": "^0.7.18",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lru-cache": "^7.14.1",
|
"lru-cache": "^7.14.1",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"mirador": "^3.3.0",
|
"mirador": "^3.4.2",
|
||||||
"mirador-dl-plugin": "^0.13.0",
|
"mirador-dl-plugin": "^0.13.0",
|
||||||
"mirador-share-plugin": "^0.16.0",
|
"mirador-share-plugin": "^0.16.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
@@ -157,6 +156,7 @@
|
|||||||
"ngx-pagination": "6.0.3",
|
"ngx-pagination": "6.0.3",
|
||||||
"ngx-ui-switch": "^14.1.0",
|
"ngx-ui-switch": "^14.1.0",
|
||||||
"nouislider": "^15.7.1",
|
"nouislider": "^15.7.1",
|
||||||
|
"orejime": "^2.3.1",
|
||||||
"pem": "1.14.8",
|
"pem": "1.14.8",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
"@angular/compiler-cli": "^17.3.11",
|
"@angular/compiler-cli": "^17.3.11",
|
||||||
"@angular/language-service": "^17.3.12",
|
"@angular/language-service": "^17.3.12",
|
||||||
"@cypress/schematic": "^1.5.0",
|
"@cypress/schematic": "^1.5.0",
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
"@ngrx/store-devtools": "^17.1.1",
|
"@ngrx/store-devtools": "^17.1.1",
|
||||||
"@ngtools/webpack": "^16.2.16",
|
"@ngtools/webpack": "^16.2.16",
|
||||||
"@types/deep-freeze": "0.1.5",
|
"@types/deep-freeze": "0.1.5",
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
"@types/grecaptcha": "^3.0.9",
|
"@types/grecaptcha": "^3.0.9",
|
||||||
"@types/jasmine": "~3.6.0",
|
"@types/jasmine": "~3.6.0",
|
||||||
"@types/js-cookie": "2.2.6",
|
"@types/js-cookie": "2.2.6",
|
||||||
"@types/lodash": "^4.17.13",
|
"@types/lodash": "^4.17.14",
|
||||||
"@types/node": "^14.14.9",
|
"@types/node": "^14.14.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||||
"@typescript-eslint/parser": "^7.18.0",
|
"@typescript-eslint/parser": "^7.18.0",
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
"compression-webpack-plugin": "^9.2.0",
|
"compression-webpack-plugin": "^9.2.0",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "^13.15.1",
|
"cypress": "^13.17.0",
|
||||||
"cypress-axe": "^1.5.0",
|
"cypress-axe": "^1.5.0",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"eslint": "^8.39.0",
|
"eslint": "^8.39.0",
|
||||||
@@ -206,12 +206,12 @@
|
|||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-import-newlines": "^1.3.1",
|
"eslint-plugin-import-newlines": "^1.3.1",
|
||||||
"eslint-plugin-jsdoc": "^45.0.0",
|
"eslint-plugin-jsdoc": "^45.0.0",
|
||||||
"eslint-plugin-jsonc": "^2.6.0",
|
"eslint-plugin-jsonc": "^2.18.2",
|
||||||
"eslint-plugin-lodash": "^7.4.0",
|
"eslint-plugin-lodash": "^7.4.0",
|
||||||
"eslint-plugin-rxjs": "^5.0.3",
|
"eslint-plugin-rxjs": "^5.0.3",
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||||
"eslint-plugin-unused-imports": "^3.2.0",
|
"eslint-plugin-unused-imports": "^3.2.0",
|
||||||
"express-static-gzip": "^2.1.8",
|
"express-static-gzip": "^2.2.0",
|
||||||
"jasmine": "^3.8.0",
|
"jasmine": "^3.8.0",
|
||||||
"jasmine-core": "^3.8.0",
|
"jasmine-core": "^3.8.0",
|
||||||
"jasmine-marbles": "0.9.2",
|
"jasmine-marbles": "0.9.2",
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
"karma-jasmine": "~4.0.0",
|
"karma-jasmine": "~4.0.0",
|
||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
"karma-mocha-reporter": "2.2.5",
|
"karma-mocha-reporter": "2.2.5",
|
||||||
"ng-mocks": "^14.13.1",
|
"ng-mocks": "^14.13.2",
|
||||||
"ngx-mask": "14.2.4",
|
"ngx-mask": "14.2.4",
|
||||||
"nodemon": "^2.0.22",
|
"nodemon": "^2.0.22",
|
||||||
"postcss": "^8.4",
|
"postcss": "^8.4",
|
||||||
@@ -229,12 +229,12 @@
|
|||||||
"postcss-loader": "^4.0.3",
|
"postcss-loader": "^4.0.3",
|
||||||
"postcss-preset-env": "^7.4.2",
|
"postcss-preset-env": "^7.4.2",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "~1.80.4",
|
"sass": "~1.83.1",
|
||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^12.6.0",
|
||||||
"sass-resources-loader": "^2.2.5",
|
"sass-resources-loader": "^2.2.5",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
"typescript": "~5.4.5",
|
"typescript": "~5.4.5",
|
||||||
"webpack": "5.95.0",
|
"webpack": "5.97.1",
|
||||||
"webpack-cli": "^5.1.4",
|
"webpack-cli": "^5.1.4",
|
||||||
"webpack-dev-server": "^4.15.1"
|
"webpack-dev-server": "^4.15.1"
|
||||||
}
|
}
|
||||||
|
@@ -218,7 +218,7 @@ export function app() {
|
|||||||
* The callback function to serve server side angular
|
* The callback function to serve server side angular
|
||||||
*/
|
*/
|
||||||
function ngApp(req, res, next) {
|
function ngApp(req, res, next) {
|
||||||
if (environment.ssr.enabled) {
|
if (environment.ssr.enabled && req.method === 'GET' && (req.path === '/' || environment.ssr.paths.some(pathPrefix => req.path.startsWith(pathPrefix)))) {
|
||||||
// Render the page to user via SSR (server side rendering)
|
// Render the page to user via SSR (server side rendering)
|
||||||
serverSideRender(req, res, next);
|
serverSideRender(req, res, next);
|
||||||
} else {
|
} else {
|
||||||
|
@@ -61,7 +61,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
|
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}">
|
[ngClass]="{'table-primary' : (activeEPerson$ | async) === epersonDto.eperson}">
|
||||||
<td>{{epersonDto.eperson.id}}</td>
|
<td>{{epersonDto.eperson.id}}</td>
|
||||||
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
||||||
<td>{{epersonDto.eperson.email}}</td>
|
<td>{{epersonDto.eperson.email}}</td>
|
||||||
|
@@ -100,6 +100,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
ePeopleDto$: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>({} as any);
|
ePeopleDto$: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>({} as any);
|
||||||
|
|
||||||
|
activeEPerson$: Observable<EPerson>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observable for the pageInfo, needed to pass to the pagination component
|
* An observable for the pageInfo, needed to pass to the pagination component
|
||||||
*/
|
*/
|
||||||
@@ -165,6 +167,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
initialisePage() {
|
initialisePage() {
|
||||||
this.searching$.next(true);
|
this.searching$.next(true);
|
||||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
||||||
|
this.activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||||
this.subs.push(this.ePeople$.pipe(
|
this.subs.push(this.ePeople$.pipe(
|
||||||
switchMap((epeople: PaginatedList<EPerson>) => {
|
switchMap((epeople: PaginatedList<EPerson>) => {
|
||||||
if (epeople.pageInfo.totalElements > 0) {
|
if (epeople.pageInfo.totalElements > 0) {
|
||||||
@@ -232,23 +235,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes EPerson, show notification on success/failure & updates EPeople list
|
* Deletes EPerson, show notification on success/failure & updates EPeople list
|
||||||
*/
|
*/
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<div class="group-form row">
|
<div class="group-form row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|
||||||
<div *ngIf="epersonService.getActiveEPerson() | async; then editHeader; else createHeader"></div>
|
<div *ngIf="activeEPerson$ | async; then editHeader; else createHeader"></div>
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<ng-template #createHeader>
|
||||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
|
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading>
|
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading>
|
||||||
|
|
||||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
<div *ngIf="activeEPerson$ | async">
|
||||||
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
|
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
|
||||||
|
|
||||||
<ds-loading [showMessage]="false" *ngIf="groups$ | async | dsHasNoValue"></ds-loading>
|
<ds-loading [showMessage]="false" *ngIf="groups$ | async | dsHasNoValue"></ds-loading>
|
||||||
@@ -75,7 +75,9 @@
|
|||||||
{{ dsoNameService.getName(group) }}
|
{{ dsoNameService.getName(group) }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">{{ dsoNameService.getName(undefined) }}</td>
|
<td class="align-middle">
|
||||||
|
{{ dsoNameService.getName((group.object | async)?.payload) }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
TestBed,
|
TestBed,
|
||||||
@@ -19,12 +19,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
Router,
|
Router,
|
||||||
|
RouterModule,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
TranslateLoader,
|
|
||||||
TranslateModule,
|
|
||||||
} from '@ngx-translate/core';
|
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
@@ -49,7 +47,6 @@ import { FormBuilderService } from '../../../shared/form/builder/form-builder.se
|
|||||||
import { FormComponent } from '../../../shared/form/form.component';
|
import { FormComponent } from '../../../shared/form/form.component';
|
||||||
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
||||||
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
||||||
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
@@ -92,9 +89,6 @@ describe('EPersonFormComponent', () => {
|
|||||||
ePersonDataServiceStub = {
|
ePersonDataServiceStub = {
|
||||||
activeEPerson: null,
|
activeEPerson: null,
|
||||||
allEpeople: mockEPeople,
|
allEpeople: mockEPeople,
|
||||||
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople));
|
|
||||||
},
|
|
||||||
getActiveEPerson(): Observable<EPerson> {
|
getActiveEPerson(): Observable<EPerson> {
|
||||||
return observableOf(this.activeEPerson);
|
return observableOf(this.activeEPerson);
|
||||||
},
|
},
|
||||||
@@ -228,12 +222,8 @@ describe('EPersonFormComponent', () => {
|
|||||||
router = new RouterStub();
|
router = new RouterStub();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
TranslateModule.forRoot({
|
RouterModule.forRoot([]),
|
||||||
loader: {
|
TranslateModule.forRoot(),
|
||||||
provide: TranslateLoader,
|
|
||||||
useClass: TranslateLoaderMock,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
EPersonFormComponent,
|
EPersonFormComponent,
|
||||||
HasNoValuePipe,
|
HasNoValuePipe,
|
||||||
],
|
],
|
||||||
@@ -251,7 +241,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
EPeopleRegistryComponent,
|
EPeopleRegistryComponent,
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
})
|
})
|
||||||
.overrideComponent(EPersonFormComponent, {
|
.overrideComponent(EPersonFormComponent, {
|
||||||
remove: { imports: [ ThemedLoadingComponent, PaginationComponent,FormComponent] },
|
remove: { imports: [ ThemedLoadingComponent, PaginationComponent,FormComponent] },
|
||||||
@@ -274,37 +264,13 @@ describe('EPersonFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('check form validation', () => {
|
describe('check form validation', () => {
|
||||||
let firstName;
|
let canLogIn: boolean;
|
||||||
let lastName;
|
let requireCertificate: boolean;
|
||||||
let email;
|
|
||||||
let canLogIn;
|
|
||||||
let requireCertificate;
|
|
||||||
|
|
||||||
let expected;
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
firstName = 'testName';
|
|
||||||
lastName = 'testLastName';
|
|
||||||
email = 'testEmail@test.com';
|
|
||||||
canLogIn = false;
|
canLogIn = false;
|
||||||
requireCertificate = false;
|
requireCertificate = false;
|
||||||
|
|
||||||
expected = Object.assign(new EPerson(), {
|
|
||||||
metadata: {
|
|
||||||
'eperson.firstname': [
|
|
||||||
{
|
|
||||||
value: firstName,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'eperson.lastname': [
|
|
||||||
{
|
|
||||||
value: lastName,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
email: email,
|
|
||||||
canLogIn: canLogIn,
|
|
||||||
requireCertificate: requireCertificate,
|
|
||||||
});
|
|
||||||
spyOn(component.submitForm, 'emit');
|
spyOn(component.submitForm, 'emit');
|
||||||
component.canLogIn.value = canLogIn;
|
component.canLogIn.value = canLogIn;
|
||||||
component.requireCertificate.value = requireCertificate;
|
component.requireCertificate.value = requireCertificate;
|
||||||
@@ -378,15 +344,13 @@ describe('EPersonFormComponent', () => {
|
|||||||
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when submitting the form', () => {
|
describe('when submitting the form', () => {
|
||||||
let firstName;
|
let firstName;
|
||||||
let lastName;
|
let lastName;
|
||||||
let email;
|
let email;
|
||||||
let canLogIn;
|
let canLogIn: boolean;
|
||||||
let requireCertificate;
|
let requireCertificate;
|
||||||
|
|
||||||
let expected;
|
let expected;
|
||||||
@@ -415,6 +379,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
requireCertificate: requireCertificate,
|
requireCertificate: requireCertificate,
|
||||||
});
|
});
|
||||||
spyOn(component.submitForm, 'emit');
|
spyOn(component.submitForm, 'emit');
|
||||||
|
component.ngOnInit();
|
||||||
component.firstName.value = firstName;
|
component.firstName.value = firstName;
|
||||||
component.lastName.value = lastName;
|
component.lastName.value = lastName;
|
||||||
component.email.value = email;
|
component.email.value = email;
|
||||||
@@ -454,9 +419,17 @@ describe('EPersonFormComponent', () => {
|
|||||||
email: email,
|
email: email,
|
||||||
canLogIn: canLogIn,
|
canLogIn: canLogIn,
|
||||||
requireCertificate: requireCertificate,
|
requireCertificate: requireCertificate,
|
||||||
_links: undefined,
|
_links: {
|
||||||
|
groups: {
|
||||||
|
href: '',
|
||||||
|
},
|
||||||
|
self: {
|
||||||
|
href: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
|
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
|
||||||
|
component.ngOnInit();
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -504,22 +477,19 @@ describe('EPersonFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
|
|
||||||
let ePersonId;
|
|
||||||
let eperson: EPerson;
|
let eperson: EPerson;
|
||||||
let modalService;
|
let modalService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(authService, 'impersonate').and.callThrough();
|
spyOn(authService, 'impersonate').and.callThrough();
|
||||||
ePersonId = 'testEPersonId';
|
|
||||||
eperson = EPersonMock;
|
eperson = EPersonMock;
|
||||||
component.epersonInitial = eperson;
|
component.epersonInitial = eperson;
|
||||||
component.canDelete$ = observableOf(true);
|
component.canDelete$ = observableOf(true);
|
||||||
spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson));
|
spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson));
|
||||||
modalService = (component as any).modalService;
|
modalService = (component as any).modalService;
|
||||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||||
|
component.ngOnInit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the delete button should be visible if the ePerson can be deleted', () => {
|
it('the delete button should be visible if the ePerson can be deleted', () => {
|
||||||
|
@@ -189,6 +189,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
canImpersonate$: Observable<boolean>;
|
canImpersonate$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current {@link EPerson}
|
||||||
|
*/
|
||||||
|
activeEPerson$: Observable<EPerson>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of subscriptions
|
* List of subscriptions
|
||||||
*/
|
*/
|
||||||
@@ -254,7 +259,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
) {
|
) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||||
|
this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => {
|
||||||
this.epersonInitial = eperson;
|
this.epersonInitial = eperson;
|
||||||
if (hasValue(eperson)) {
|
if (hasValue(eperson)) {
|
||||||
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
||||||
@@ -262,9 +271,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
this.submitLabel = 'form.submit';
|
this.submitLabel = 'form.submit';
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.initialisePage();
|
this.initialisePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,130 +278,121 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* This method will initialise the page
|
* This method will initialise the page
|
||||||
*/
|
*/
|
||||||
initialisePage() {
|
initialisePage() {
|
||||||
this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData<EPerson>) => {
|
if (this.route.snapshot.params.id) {
|
||||||
this.epersonService.editEPerson(ePersonRD.payload);
|
this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData<EPerson>) => {
|
||||||
}));
|
this.epersonService.editEPerson(ePersonRD.payload);
|
||||||
observableCombineLatest([
|
|
||||||
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,
|
|
||||||
errorMessages: {
|
|
||||||
emailTaken: 'error.validation.emailTaken',
|
|
||||||
pattern: 'error.validation.NotValidEmail',
|
|
||||||
},
|
|
||||||
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) => {
|
|
||||||
if (eperson != null) {
|
|
||||||
this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, {
|
|
||||||
currentPage: 1,
|
|
||||||
elementsPerPage: this.config.pageSize,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (eperson === null && !!this.formGroup.controls.email) {
|
|
||||||
this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService));
|
|
||||||
this.emailValueChangeSubscribe = this.email.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
|
||||||
this.changeDetectorRef.detectChanges();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
const activeEPerson$ = this.epersonService.getActiveEPerson();
|
this.firstName = new DynamicInputModel({
|
||||||
|
id: 'firstName',
|
||||||
this.groups$ = activeEPerson$.pipe(
|
label: this.translateService.instant(`${this.messagePrefix}.firstName`),
|
||||||
switchMap((eperson) => {
|
name: 'firstName',
|
||||||
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
|
validators: {
|
||||||
currentPage: 1,
|
required: null,
|
||||||
elementsPerPage: this.config.pageSize,
|
},
|
||||||
})]);
|
required: true,
|
||||||
}),
|
|
||||||
switchMap(([eperson, findListOptions]) => {
|
|
||||||
if (eperson != null) {
|
|
||||||
return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
|
|
||||||
}
|
|
||||||
return observableOf(undefined);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.groupsPageInfoState$ = this.groups$.pipe(
|
|
||||||
map(groupsRD => groupsRD.payload.pageInfo),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.canImpersonate$ = activeEPerson$.pipe(
|
|
||||||
switchMap((eperson) => {
|
|
||||||
if (hasValue(eperson)) {
|
|
||||||
return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self);
|
|
||||||
} else {
|
|
||||||
return observableOf(false);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.canDelete$ = activeEPerson$.pipe(
|
|
||||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)),
|
|
||||||
);
|
|
||||||
this.canReset$ = observableOf(true);
|
|
||||||
});
|
});
|
||||||
|
this.lastName = new DynamicInputModel({
|
||||||
|
id: 'lastName',
|
||||||
|
label: this.translateService.instant(`${this.messagePrefix}.lastName`),
|
||||||
|
name: 'lastName',
|
||||||
|
validators: {
|
||||||
|
required: null,
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
this.email = new DynamicInputModel({
|
||||||
|
id: 'email',
|
||||||
|
label: this.translateService.instant(`${this.messagePrefix}.email`),
|
||||||
|
name: 'email',
|
||||||
|
validators: {
|
||||||
|
required: null,
|
||||||
|
pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$',
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
errorMessages: {
|
||||||
|
emailTaken: 'error.validation.emailTaken',
|
||||||
|
pattern: 'error.validation.NotValidEmail',
|
||||||
|
},
|
||||||
|
hint: this.translateService.instant(`${this.messagePrefix}.emailHint`),
|
||||||
|
});
|
||||||
|
this.canLogIn = new DynamicCheckboxModel(
|
||||||
|
{
|
||||||
|
id: 'canLogIn',
|
||||||
|
label: this.translateService.instant(`${this.messagePrefix}.canLogIn`),
|
||||||
|
name: 'canLogIn',
|
||||||
|
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true),
|
||||||
|
});
|
||||||
|
this.requireCertificate = new DynamicCheckboxModel(
|
||||||
|
{
|
||||||
|
id: 'requireCertificate',
|
||||||
|
label: this.translateService.instant(`${this.messagePrefix}.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.activeEPerson$.subscribe((eperson: EPerson) => {
|
||||||
|
if (eperson != null) {
|
||||||
|
this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, {
|
||||||
|
currentPage: 1,
|
||||||
|
elementsPerPage: this.config.pageSize,
|
||||||
|
}, undefined, undefined, followLink('object'));
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eperson === null && !!this.formGroup.controls.email) {
|
||||||
|
this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService));
|
||||||
|
this.emailValueChangeSubscribe = this.email.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.groups$ = this.activeEPerson$.pipe(
|
||||||
|
switchMap((eperson) => {
|
||||||
|
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
|
||||||
|
currentPage: 1,
|
||||||
|
elementsPerPage: this.config.pageSize,
|
||||||
|
})]);
|
||||||
|
}),
|
||||||
|
switchMap(([eperson, findListOptions]) => {
|
||||||
|
if (eperson != null) {
|
||||||
|
return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
|
||||||
|
}
|
||||||
|
return observableOf(undefined);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.groupsPageInfoState$ = this.groups$.pipe(
|
||||||
|
map(groupsRD => groupsRD.payload.pageInfo),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.canImpersonate$ = this.activeEPerson$.pipe(
|
||||||
|
switchMap((eperson) => {
|
||||||
|
if (hasValue(eperson)) {
|
||||||
|
return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self);
|
||||||
|
} else {
|
||||||
|
return observableOf(false);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.canDelete$ = this.activeEPerson$.pipe(
|
||||||
|
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)),
|
||||||
|
);
|
||||||
|
this.canReset$ = observableOf(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -414,7 +411,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created eperson using the EventEmitter submitForm
|
* Emit the updated/created eperson using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe(
|
this.activeEPerson$.pipe(take(1)).subscribe(
|
||||||
(ePerson: EPerson) => {
|
(ePerson: EPerson) => {
|
||||||
const values = {
|
const values = {
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -533,7 +530,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||||
*/
|
*/
|
||||||
delete(): void {
|
delete(): void {
|
||||||
this.epersonService.getActiveEPerson().pipe(
|
this.activeEPerson$.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
switchMap((eperson: EPerson) => {
|
switchMap((eperson: EPerson) => {
|
||||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||||
@@ -637,7 +634,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* Update the list of groups by fetching it from the rest api or cache
|
* Update the list of groups by fetching it from the rest api or cache
|
||||||
*/
|
*/
|
||||||
private updateGroups(options) {
|
private updateGroups(options) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => {
|
||||||
this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, options);
|
this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, options);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<div class="group-form row">
|
<div class="group-form row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|
||||||
<div *ngIf="groupDataService.getActiveGroup() | async; then editHeader; else createHeader"></div>
|
<div *ngIf="activeGroup$ | async; then editHeader; else createHeader"></div>
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<ng-template #createHeader>
|
||||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
|
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
|
||||||
@@ -23,11 +23,15 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
|
<ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited">
|
||||||
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
|
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertType.Warning"
|
||||||
<ds-alert *ngIf="(canEdit$ | async) !== true && (groupDataService.getActiveGroup() | async)" [type]="AlertTypeEnum.Warning"
|
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
|
||||||
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName((getLinkedDSO(groupBeingEdited) | async)?.payload), comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })">
|
<ng-container *ngIf="(activeGroupLinkedDSO$ | async) as activeGroupLinkedDSO">
|
||||||
</ds-alert>
|
<ds-alert *ngIf="(canEdit$ | async) !== true" [type]="AlertType.Warning"
|
||||||
|
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName(activeGroupLinkedDSO), comcol: activeGroupLinkedDSO.type, comcolEditRolesRoute: (linkedEditRolesRoute$ | async) })">
|
||||||
|
</ds-alert>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ds-form [formId]="formId"
|
<ds-form [formId]="formId"
|
||||||
[formModel]="formModel"
|
[formModel]="formModel"
|
||||||
@@ -39,22 +43,21 @@
|
|||||||
<button (click)="onCancel()" type="button"
|
<button (click)="onCancel()" type="button"
|
||||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div after *ngIf="(canEdit$ | async) && !groupBeingEdited?.permanent" class="btn-group">
|
<div after *ngIf="(canEdit$ | async) && !(activeGroup$ | async)?.permanent" class="btn-group">
|
||||||
<button (click)="delete()" class="btn btn-danger delete-button" type="button">
|
<button (click)="delete()" class="btn btn-danger delete-button" type="button">
|
||||||
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
|
|
||||||
<div class="mb-5">
|
<ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited">
|
||||||
<ds-members-list *ngIf="groupBeingEdited !== undefined"
|
<div class="mb-5">
|
||||||
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
<ds-members-list *ngIf="groupBeingEdited !== undefined"
|
||||||
</div>
|
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
||||||
<ds-subgroups-list *ngIf="groupBeingEdited !== undefined"
|
</div>
|
||||||
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
<ds-subgroups-list *ngIf="groupBeingEdited !== undefined"
|
||||||
|
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
TestBed,
|
TestBed,
|
||||||
@@ -23,11 +23,7 @@ import {
|
|||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import {
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
TranslateLoader,
|
|
||||||
TranslateModule,
|
|
||||||
TranslateService,
|
|
||||||
} from '@ngx-translate/core';
|
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
@@ -61,15 +57,14 @@ import { FormComponent } from '../../../shared/form/form.component';
|
|||||||
import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
|
||||||
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
||||||
import { RouterMock } from '../../../shared/mocks/router.mock';
|
import { RouterMock } from '../../../shared/mocks/router.mock';
|
||||||
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
|
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||||
import {
|
import {
|
||||||
GroupMock,
|
GroupMock,
|
||||||
GroupMock2,
|
GroupMock2,
|
||||||
} from '../../../shared/testing/group-mock';
|
} from '../../../shared/testing/group-mock';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock';
|
|
||||||
import { GroupFormComponent } from './group-form.component';
|
import { GroupFormComponent } from './group-form.component';
|
||||||
import { MembersListComponent } from './members-list/members-list.component';
|
import { MembersListComponent } from './members-list/members-list.component';
|
||||||
import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component';
|
import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component';
|
||||||
@@ -78,19 +73,19 @@ import { ValidateGroupExists } from './validators/group-exists.validator';
|
|||||||
describe('GroupFormComponent', () => {
|
describe('GroupFormComponent', () => {
|
||||||
let component: GroupFormComponent;
|
let component: GroupFormComponent;
|
||||||
let fixture: ComponentFixture<GroupFormComponent>;
|
let fixture: ComponentFixture<GroupFormComponent>;
|
||||||
let translateService: TranslateService;
|
|
||||||
let builderService: FormBuilderService;
|
let builderService: FormBuilderService;
|
||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let groupsDataServiceStub: any;
|
let groupsDataServiceStub: any;
|
||||||
let dsoDataServiceStub: any;
|
let dsoDataServiceStub: any;
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let notificationService: NotificationsServiceStub;
|
let notificationService: NotificationsServiceStub;
|
||||||
let router;
|
let router: RouterMock;
|
||||||
|
let route: ActivatedRouteStub;
|
||||||
|
|
||||||
let groups;
|
let groups: Group[];
|
||||||
let groupName;
|
let groupName: string;
|
||||||
let groupDescription;
|
let groupDescription: string;
|
||||||
let expected;
|
let expected: Group;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
groups = [GroupMock, GroupMock2];
|
groups = [GroupMock, GroupMock2];
|
||||||
@@ -105,6 +100,15 @@ describe('GroupFormComponent', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
object: createSuccessfulRemoteDataObject$(undefined),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'group-selflink',
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
href: 'group-objectlink',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
ePersonDataServiceStub = {};
|
ePersonDataServiceStub = {};
|
||||||
groupsDataServiceStub = {
|
groupsDataServiceStub = {
|
||||||
@@ -141,7 +145,14 @@ describe('GroupFormComponent', () => {
|
|||||||
create(group: Group): Observable<RemoteData<Group>> {
|
create(group: Group): Observable<RemoteData<Group>> {
|
||||||
this.allGroups = [...this.allGroups, group];
|
this.allGroups = [...this.allGroups, group];
|
||||||
this.createdGroup = Object.assign({}, group, {
|
this.createdGroup = Object.assign({}, group, {
|
||||||
_links: { self: { href: 'group-selflink' } },
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'group-selflink',
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
href: 'group-objectlink',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return createSuccessfulRemoteDataObject$(this.createdGroup);
|
return createSuccessfulRemoteDataObject$(this.createdGroup);
|
||||||
},
|
},
|
||||||
@@ -223,17 +234,15 @@ describe('GroupFormComponent', () => {
|
|||||||
return typeof value === 'object' && value !== null;
|
return typeof value === 'object' && value !== null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
translateService = getMockTranslateService();
|
|
||||||
router = new RouterMock();
|
router = new RouterMock();
|
||||||
|
route = new ActivatedRouteStub();
|
||||||
notificationService = new NotificationsServiceStub();
|
notificationService = new NotificationsServiceStub();
|
||||||
|
|
||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot(),
|
||||||
loader: {
|
GroupFormComponent,
|
||||||
provide: TranslateLoader,
|
],
|
||||||
useClass: TranslateLoaderMock,
|
|
||||||
},
|
|
||||||
}), GroupFormComponent],
|
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||||
@@ -249,14 +258,11 @@ describe('GroupFormComponent', () => {
|
|||||||
{ provide: Store, useValue: {} },
|
{ provide: Store, useValue: {} },
|
||||||
{ provide: RemoteDataBuildService, useValue: {} },
|
{ provide: RemoteDataBuildService, useValue: {} },
|
||||||
{ provide: HALEndpointService, useValue: {} },
|
{ provide: HALEndpointService, useValue: {} },
|
||||||
{
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
provide: ActivatedRoute,
|
|
||||||
useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) },
|
|
||||||
},
|
|
||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
})
|
})
|
||||||
.overrideComponent(GroupFormComponent, {
|
.overrideComponent(GroupFormComponent, {
|
||||||
remove: { imports: [
|
remove: { imports: [
|
||||||
@@ -279,8 +285,8 @@ describe('GroupFormComponent', () => {
|
|||||||
describe('when submitting the form', () => {
|
describe('when submitting the form', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(component.submitForm, 'emit');
|
spyOn(component.submitForm, 'emit');
|
||||||
component.groupName.value = groupName;
|
component.groupName.setValue(groupName);
|
||||||
component.groupDescription.value = groupDescription;
|
component.groupDescription.setValue(groupDescription);
|
||||||
});
|
});
|
||||||
describe('without active Group', () => {
|
describe('without active Group', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -288,14 +294,22 @@ describe('GroupFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit a new group using the correct values', (async () => {
|
it('should emit a new group using the correct values', (() => {
|
||||||
await fixture.whenStable().then(() => {
|
expect(component.submitForm.emit).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
name: groupName,
|
||||||
});
|
metadata: {
|
||||||
|
'dc.description': [
|
||||||
|
{
|
||||||
|
value: groupDescription,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}));
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with active Group', () => {
|
describe('with active Group', () => {
|
||||||
let expected2;
|
let expected2: Group;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
expected2 = Object.assign(new Group(), {
|
expected2 = Object.assign(new Group(), {
|
||||||
name: 'newGroupName',
|
name: 'newGroupName',
|
||||||
@@ -306,15 +320,24 @@ describe('GroupFormComponent', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
object: createSuccessfulRemoteDataObject$(undefined),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'group-selflink',
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
href: 'group-objectlink',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected));
|
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected));
|
||||||
spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2));
|
spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2));
|
||||||
component.groupName.value = 'newGroupName';
|
component.ngOnInit();
|
||||||
component.onSubmit();
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should edit with name and description operations', () => {
|
it('should edit with name and description operations', () => {
|
||||||
|
component.groupName.setValue('newGroupName');
|
||||||
|
component.onSubmit();
|
||||||
const operations = [{
|
const operations = [{
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: '/metadata/dc.description',
|
path: '/metadata/dc.description',
|
||||||
@@ -328,9 +351,8 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should edit with description operations', () => {
|
it('should edit with description operations', () => {
|
||||||
component.groupName.value = null;
|
component.groupName.setValue(null);
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
|
||||||
const operations = [{
|
const operations = [{
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: '/metadata/dc.description',
|
path: '/metadata/dc.description',
|
||||||
@@ -340,9 +362,9 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should edit with name operations', () => {
|
it('should edit with name operations', () => {
|
||||||
component.groupDescription.value = null;
|
component.groupName.setValue('newGroupName');
|
||||||
|
component.groupDescription.setValue(null);
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
|
||||||
const operations = [{
|
const operations = [{
|
||||||
op: 'replace',
|
op: 'replace',
|
||||||
path: '/name',
|
path: '/name',
|
||||||
@@ -351,12 +373,13 @@ describe('GroupFormComponent', () => {
|
|||||||
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit the existing group using the correct new values', (async () => {
|
it('should emit the existing group using the correct new values', () => {
|
||||||
await fixture.whenStable().then(() => {
|
component.onSubmit();
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
it('should emit success notification', () => {
|
it('should emit success notification', () => {
|
||||||
|
component.onSubmit();
|
||||||
expect(notificationService.success).toHaveBeenCalled();
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -371,11 +394,8 @@ describe('GroupFormComponent', () => {
|
|||||||
|
|
||||||
|
|
||||||
describe('check form validation', () => {
|
describe('check form validation', () => {
|
||||||
let groupCommunity;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
groupName = 'testName';
|
groupName = 'testName';
|
||||||
groupCommunity = 'testgroupCommunity';
|
|
||||||
groupDescription = 'testgroupDescription';
|
groupDescription = 'testgroupDescription';
|
||||||
|
|
||||||
expected = Object.assign(new Group(), {
|
expected = Object.assign(new Group(), {
|
||||||
@@ -387,8 +407,17 @@ describe('GroupFormComponent', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'group-selflink',
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
href: 'group-objectlink',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
spyOn(component.submitForm, 'emit');
|
spyOn(component.submitForm, 'emit');
|
||||||
|
spyOn(dsoDataServiceStub, 'findByHref').and.returnValue(observableOf(expected));
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
component.initialisePage();
|
component.initialisePage();
|
||||||
@@ -438,21 +467,20 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
let deleteButton;
|
let deleteButton: HTMLButtonElement;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
component.initialisePage();
|
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
||||||
|
component.activeGroup$ = observableOf({
|
||||||
component.canEdit$ = observableOf(true);
|
id: 'active-group',
|
||||||
component.groupBeingEdited = {
|
|
||||||
permanent: false,
|
permanent: false,
|
||||||
} as Group;
|
} as Group);
|
||||||
|
component.canEdit$ = observableOf(true);
|
||||||
|
|
||||||
|
component.initialisePage();
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement;
|
deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement;
|
||||||
|
|
||||||
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
|
||||||
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf({ id: 'active-group' }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if confirmed via modal', () => {
|
describe('if confirmed via modal', () => {
|
||||||
|
@@ -11,7 +11,10 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { UntypedFormGroup } from '@angular/forms';
|
import {
|
||||||
|
AbstractControl,
|
||||||
|
UntypedFormGroup,
|
||||||
|
} from '@angular/forms';
|
||||||
import {
|
import {
|
||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
Router,
|
Router,
|
||||||
@@ -31,13 +34,10 @@ import { Operation } from 'fast-json-patch';
|
|||||||
import {
|
import {
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
|
||||||
Subscription,
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
catchError,
|
|
||||||
debounceTime,
|
debounceTime,
|
||||||
filter,
|
|
||||||
map,
|
map,
|
||||||
switchMap,
|
switchMap,
|
||||||
take,
|
take,
|
||||||
@@ -53,7 +53,6 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
|||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
|
||||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||||
import { Group } from '../../../core/eperson/models/group.model';
|
import { Group } from '../../../core/eperson/models/group.model';
|
||||||
import { Collection } from '../../../core/shared/collection.model';
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
@@ -61,9 +60,9 @@ import { Community } from '../../../core/shared/community.model';
|
|||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
import {
|
import {
|
||||||
|
getAllCompletedRemoteData,
|
||||||
getFirstCompletedRemoteData,
|
getFirstCompletedRemoteData,
|
||||||
getFirstSucceededRemoteData,
|
getFirstSucceededRemoteData,
|
||||||
getFirstSucceededRemoteDataPayload,
|
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { AlertComponent } from '../../../shared/alert/alert.component';
|
import { AlertComponent } from '../../../shared/alert/alert.component';
|
||||||
@@ -117,9 +116,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Dynamic models for the inputs of form
|
* Dynamic models for the inputs of form
|
||||||
*/
|
*/
|
||||||
groupName: DynamicInputModel;
|
groupName: AbstractControl;
|
||||||
groupCommunity: DynamicInputModel;
|
groupCommunity: AbstractControl;
|
||||||
groupDescription: DynamicTextAreaModel;
|
groupDescription: AbstractControl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all dynamic input models
|
* A list of all dynamic input models
|
||||||
@@ -162,21 +161,30 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
subs: Subscription[] = [];
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* Group currently being edited
|
|
||||||
*/
|
|
||||||
groupBeingEdited: Group;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable whether or not the logged in user is allowed to delete the Group & doesn't have a linked object (community / collection linked to workspace group
|
* Observable whether or not the logged in user is allowed to delete the Group & doesn't have a linked object (community / collection linked to workspace group
|
||||||
*/
|
*/
|
||||||
canEdit$: Observable<boolean>;
|
canEdit$: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The AlertType enumeration
|
* The current {@link Group}
|
||||||
* @type {AlertType}
|
|
||||||
*/
|
*/
|
||||||
public AlertTypeEnum = AlertType;
|
activeGroup$: Observable<Group>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current {@link Group}'s linked {@link Community}/{@link Collection}
|
||||||
|
*/
|
||||||
|
activeGroupLinkedDSO$: Observable<DSpaceObject>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link to the current {@link Group}'s {@link Community}/{@link Collection} edit role tab
|
||||||
|
*/
|
||||||
|
linkedEditRolesRoute$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AlertType enumeration
|
||||||
|
*/
|
||||||
|
public readonly AlertType = AlertType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription to email field value change
|
* Subscription to email field value change
|
||||||
@@ -186,126 +194,121 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public groupDataService: GroupDataService,
|
public groupDataService: GroupDataService,
|
||||||
private ePersonDataService: EPersonDataService,
|
protected dSpaceObjectDataService: DSpaceObjectDataService,
|
||||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
protected formBuilderService: FormBuilderService,
|
||||||
private formBuilderService: FormBuilderService,
|
protected translateService: TranslateService,
|
||||||
private translateService: TranslateService,
|
protected notificationsService: NotificationsService,
|
||||||
private notificationsService: NotificationsService,
|
protected route: ActivatedRoute,
|
||||||
private route: ActivatedRoute,
|
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
private authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
private modalService: NgbModal,
|
protected modalService: NgbModal,
|
||||||
public requestService: RequestService,
|
public requestService: RequestService,
|
||||||
protected changeDetectorRef: ChangeDetectorRef,
|
protected changeDetectorRef: ChangeDetectorRef,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
|
if (this.route.snapshot.params.groupId !== 'newGroup') {
|
||||||
|
this.setActiveGroup(this.route.snapshot.params.groupId);
|
||||||
|
}
|
||||||
|
this.activeGroup$ = this.groupDataService.getActiveGroup();
|
||||||
|
this.activeGroupLinkedDSO$ = this.getActiveGroupLinkedDSO();
|
||||||
|
this.linkedEditRolesRoute$ = this.getLinkedEditRolesRoute();
|
||||||
|
this.canEdit$ = this.activeGroupLinkedDSO$.pipe(
|
||||||
|
switchMap((dso: DSpaceObject) => {
|
||||||
|
if (hasValue(dso)) {
|
||||||
|
return [false];
|
||||||
|
} else {
|
||||||
|
return this.activeGroup$.pipe(
|
||||||
|
hasValueOperator(),
|
||||||
|
switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
this.initialisePage();
|
this.initialisePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
initialisePage() {
|
initialisePage() {
|
||||||
this.subs.push(this.route.params.subscribe((params) => {
|
const groupNameModel = new DynamicInputModel({
|
||||||
if (params.groupId !== 'newGroup') {
|
id: 'groupName',
|
||||||
this.setActiveGroup(params.groupId);
|
label: this.translateService.instant(`${this.messagePrefix}.groupName`),
|
||||||
}
|
name: 'groupName',
|
||||||
}));
|
validators: {
|
||||||
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
required: null,
|
||||||
hasValueOperator(),
|
},
|
||||||
switchMap((group: Group) => {
|
required: true,
|
||||||
return observableCombineLatest([
|
});
|
||||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined),
|
const groupCommunityModel = new DynamicInputModel({
|
||||||
this.hasLinkedDSO(group),
|
id: 'groupCommunity',
|
||||||
]).pipe(
|
label: this.translateService.instant(`${this.messagePrefix}.groupCommunity`),
|
||||||
map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO),
|
name: 'groupCommunity',
|
||||||
);
|
required: false,
|
||||||
|
readOnly: true,
|
||||||
|
});
|
||||||
|
const groupDescriptionModel = new DynamicTextAreaModel({
|
||||||
|
id: 'groupDescription',
|
||||||
|
label: this.translateService.instant(`${this.messagePrefix}.groupDescription`),
|
||||||
|
name: 'groupDescription',
|
||||||
|
required: false,
|
||||||
|
spellCheck: environment.form.spellCheck,
|
||||||
|
});
|
||||||
|
this.formModel = [
|
||||||
|
groupNameModel,
|
||||||
|
groupDescriptionModel,
|
||||||
|
];
|
||||||
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
|
this.groupName = this.formGroup.get('groupName');
|
||||||
|
this.groupDescription = this.formGroup.get('groupDescription');
|
||||||
|
|
||||||
|
if (hasValue(this.groupName)) {
|
||||||
|
this.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService));
|
||||||
|
this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subs.push(
|
||||||
|
observableCombineLatest([
|
||||||
|
this.activeGroup$,
|
||||||
|
this.canEdit$,
|
||||||
|
this.activeGroupLinkedDSO$,
|
||||||
|
]).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
||||||
|
|
||||||
|
if (activeGroup != null) {
|
||||||
|
|
||||||
|
// Disable group name exists validator
|
||||||
|
this.formGroup.controls.groupName.clearAsyncValidators();
|
||||||
|
|
||||||
|
if (isNotEmpty(linkedObject?.name)) {
|
||||||
|
if (!this.formGroup.controls.groupCommunity) {
|
||||||
|
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel);
|
||||||
|
this.groupDescription = this.formGroup.get('groupCommunity');
|
||||||
|
}
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
groupName: activeGroup.name,
|
||||||
|
groupCommunity: linkedObject?.name ?? '',
|
||||||
|
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.formModel = [
|
||||||
|
groupNameModel,
|
||||||
|
groupDescriptionModel,
|
||||||
|
];
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
groupName: activeGroup.name,
|
||||||
|
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!canEdit || activeGroup.permanent) {
|
||||||
|
this.formGroup.disable();
|
||||||
|
} else {
|
||||||
|
this.formGroup.enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
observableCombineLatest([
|
|
||||||
this.translateService.get(`${this.messagePrefix}.groupName`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.groupCommunity`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.groupDescription`),
|
|
||||||
]).subscribe(([groupName, groupCommunity, groupDescription]) => {
|
|
||||||
this.groupName = new DynamicInputModel({
|
|
||||||
id: 'groupName',
|
|
||||||
label: groupName,
|
|
||||||
name: 'groupName',
|
|
||||||
validators: {
|
|
||||||
required: null,
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
this.groupCommunity = new DynamicInputModel({
|
|
||||||
id: 'groupCommunity',
|
|
||||||
label: groupCommunity,
|
|
||||||
name: 'groupCommunity',
|
|
||||||
required: false,
|
|
||||||
readOnly: true,
|
|
||||||
});
|
|
||||||
this.groupDescription = new DynamicTextAreaModel({
|
|
||||||
id: 'groupDescription',
|
|
||||||
label: groupDescription,
|
|
||||||
name: 'groupDescription',
|
|
||||||
required: false,
|
|
||||||
spellCheck: environment.form.spellCheck,
|
|
||||||
});
|
|
||||||
this.formModel = [
|
|
||||||
this.groupName,
|
|
||||||
this.groupDescription,
|
|
||||||
];
|
|
||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
|
||||||
|
|
||||||
if (this.formGroup.controls.groupName) {
|
|
||||||
this.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService));
|
|
||||||
this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
|
||||||
this.changeDetectorRef.detectChanges();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.subs.push(
|
|
||||||
observableCombineLatest([
|
|
||||||
this.groupDataService.getActiveGroup(),
|
|
||||||
this.canEdit$,
|
|
||||||
this.groupDataService.getActiveGroup()
|
|
||||||
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload()))),
|
|
||||||
]).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
|
||||||
|
|
||||||
if (activeGroup != null) {
|
|
||||||
|
|
||||||
// Disable group name exists validator
|
|
||||||
this.formGroup.controls.groupName.clearAsyncValidators();
|
|
||||||
|
|
||||||
this.groupBeingEdited = activeGroup;
|
|
||||||
|
|
||||||
if (linkedObject?.name) {
|
|
||||||
if (!this.formGroup.controls.groupCommunity) {
|
|
||||||
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity);
|
|
||||||
this.formGroup.patchValue({
|
|
||||||
groupName: activeGroup.name,
|
|
||||||
groupCommunity: linkedObject?.name ?? '',
|
|
||||||
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.formModel = [
|
|
||||||
this.groupName,
|
|
||||||
this.groupDescription,
|
|
||||||
];
|
|
||||||
this.formGroup.patchValue({
|
|
||||||
groupName: activeGroup.name,
|
|
||||||
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!canEdit || activeGroup.permanent) {
|
|
||||||
this.formGroup.disable();
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -324,9 +327,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created eperson using the EventEmitter submitForm
|
* Emit the updated/created eperson using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe(
|
this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
|
||||||
(group: Group) => {
|
if (group === null) {
|
||||||
const values = {
|
this.createNewGroup({
|
||||||
name: this.groupName.value,
|
name: this.groupName.value,
|
||||||
metadata: {
|
metadata: {
|
||||||
'dc.description': [
|
'dc.description': [
|
||||||
@@ -335,14 +338,11 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
if (group === null) {
|
} else {
|
||||||
this.createNewGroup(values);
|
this.editGroup(group);
|
||||||
} else {
|
}
|
||||||
this.editGroup(group);
|
});
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -448,7 +448,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
* @param groupSelfLink SelfLink of group to set as active
|
* @param groupSelfLink SelfLink of group to set as active
|
||||||
*/
|
*/
|
||||||
setActiveGroupWithLink(groupSelfLink: string) {
|
setActiveGroupWithLink(groupSelfLink: string) {
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
this.activeGroup$.pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||||
if (activeGroup === null) {
|
if (activeGroup === null) {
|
||||||
this.groupDataService.cancelEditGroup();
|
this.groupDataService.cancelEditGroup();
|
||||||
this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object'))
|
this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object'))
|
||||||
@@ -467,7 +467,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||||
*/
|
*/
|
||||||
delete() {
|
delete() {
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((group: Group) => {
|
this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
|
||||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||||
modalRef.componentInstance.name = this.dsoNameService.getName(group);
|
modalRef.componentInstance.name = this.dsoNameService.getName(group);
|
||||||
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header';
|
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header';
|
||||||
@@ -511,52 +511,38 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if group has a linked object (community or collection linked to a workflow group)
|
* Get the active {@link Group}'s linked object if it has one ({@link Community} or {@link Collection} linked to a
|
||||||
* @param group
|
* workflow group)
|
||||||
*/
|
*/
|
||||||
hasLinkedDSO(group: Group): Observable<boolean> {
|
getActiveGroupLinkedDSO(): Observable<DSpaceObject> {
|
||||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
return this.activeGroup$.pipe(
|
||||||
return this.getLinkedDSO(group).pipe(
|
hasValueOperator(),
|
||||||
map((rd: RemoteData<DSpaceObject>) => {
|
switchMap((group: Group) => {
|
||||||
return hasValue(rd) && hasValue(rd.payload);
|
if (group.object === undefined) {
|
||||||
}),
|
return this.dSpaceObjectDataService.findByHref(group._links.object.href);
|
||||||
catchError(() => observableOf(false)),
|
}
|
||||||
);
|
return group.object;
|
||||||
}
|
}),
|
||||||
|
getAllCompletedRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get group's linked object if it has one (community or collection linked to a workflow group)
|
* Get the route to the edit roles tab of the active {@link Group}'s linked object (community or collection linked
|
||||||
* @param group
|
* to a workflow group) if it has one
|
||||||
*/
|
*/
|
||||||
getLinkedDSO(group: Group): Observable<RemoteData<DSpaceObject>> {
|
getLinkedEditRolesRoute(): Observable<string> {
|
||||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
return this.activeGroupLinkedDSO$.pipe(
|
||||||
if (group.object === undefined) {
|
hasValueOperator(),
|
||||||
return this.dSpaceObjectDataService.findByHref(group._links.object.href);
|
map((dso: DSpaceObject) => {
|
||||||
}
|
switch ((dso as any).type) {
|
||||||
return group.object;
|
case Community.type.value:
|
||||||
}
|
return getCommunityEditRolesRoute(dso.id);
|
||||||
}
|
case Collection.type.value:
|
||||||
|
return getCollectionEditRolesRoute(dso.id);
|
||||||
/**
|
}
|
||||||
* Get the route to the edit roles tab of the group's linked object (community or collection linked to a workflow group) if it has one
|
}),
|
||||||
* @param group
|
);
|
||||||
*/
|
|
||||||
getLinkedEditRolesRoute(group: Group): Observable<string> {
|
|
||||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
|
||||||
return this.getLinkedDSO(group).pipe(
|
|
||||||
map((rd: RemoteData<DSpaceObject>) => {
|
|
||||||
if (hasValue(rd) && hasValue(rd.payload)) {
|
|
||||||
const dso = rd.payload;
|
|
||||||
switch ((dso as any).type) {
|
|
||||||
case Community.type.value:
|
|
||||||
return getCommunityEditRolesRoute(rd.payload.id);
|
|
||||||
case Collection.type.value:
|
|
||||||
return getCollectionEditRolesRoute(rd.payload.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
|
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(bitstreamFormats | async)?.payload?.totalElements > 0"
|
*ngIf="(bitstreamFormats$ | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="pageConfig"
|
[paginationOptions]="pageConfig"
|
||||||
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
[collectionSize]="(bitstreamFormats$ | async)?.payload?.totalElements"
|
||||||
[hideGear]="false"
|
[hideGear]="false"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
<tr *ngFor="let bitstreamFormat of (bitstreamFormats$ | async)?.payload?.page">
|
||||||
<td>
|
<td>
|
||||||
<label class="mb-0">
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
|
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
|
||||||
[checked]="isSelected(bitstreamFormat) | async"
|
[checked]="(selectedBitstreamFormatIDs$ | async)?.includes(bitstreamFormat.id)"
|
||||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
|
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
|
||||||
@@ -46,13 +46,13 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
<div *ngIf="(bitstreamFormats | async)?.payload?.totalElements === 0" class="alert alert-info" role="alert">
|
<div *ngIf="(bitstreamFormats$ | async)?.payload?.totalElements === 0" class="alert alert-info" role="alert">
|
||||||
{{'admin.registries.bitstream-formats.no-items' | translate}}
|
{{'admin.registries.bitstream-formats.no-items' | translate}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button *ngIf="(bitstreamFormats | async)?.payload?.page?.length > 0" class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button>
|
<button *ngIf="(bitstreamFormats$ | async)?.payload?.page?.length > 0" class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button>
|
||||||
<button *ngIf="(bitstreamFormats | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button>
|
<button *ngIf="(bitstreamFormats$ | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -8,10 +8,7 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {
|
import { hot } from 'jasmine-marbles';
|
||||||
cold,
|
|
||||||
hot,
|
|
||||||
} from 'jasmine-marbles';
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||||
@@ -191,17 +188,17 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
beforeEach(waitForAsync(initAsync));
|
beforeEach(waitForAsync(initAsync));
|
||||||
beforeEach(initBeforeEach);
|
beforeEach(initBeforeEach);
|
||||||
it('should return an observable of true if the provided bitstream is in the list returned by the service', () => {
|
it('should return an observable of true if the provided bitstream is in the list returned by the service', () => {
|
||||||
const result = comp.isSelected(bitstreamFormat1);
|
comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => {
|
||||||
|
expect(selectedBitstreamFormatIDs).toContain(bitstreamFormat1.id);
|
||||||
expect(result).toBeObservable(cold('b', { b: true }));
|
});
|
||||||
});
|
});
|
||||||
it('should return an observable of false if the provided bitstream is not in the list returned by the service', () => {
|
it('should return an observable of false if the provided bitstream is not in the list returned by the service', () => {
|
||||||
const format = new BitstreamFormat();
|
const format = new BitstreamFormat();
|
||||||
format.uuid = 'new';
|
format.uuid = 'new';
|
||||||
|
|
||||||
const result = comp.isSelected(format);
|
comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => {
|
||||||
|
expect(selectedBitstreamFormatIDs).not.toContain(format.id);
|
||||||
expect(result).toBeObservable(cold('b', { b: false }));
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -13,10 +13,7 @@ import {
|
|||||||
TranslateModule,
|
TranslateModule,
|
||||||
TranslateService,
|
TranslateService,
|
||||||
} from '@ngx-translate/core';
|
} from '@ngx-translate/core';
|
||||||
import {
|
import { Observable } from 'rxjs';
|
||||||
combineLatest as observableCombineLatest,
|
|
||||||
Observable,
|
|
||||||
} from 'rxjs';
|
|
||||||
import {
|
import {
|
||||||
map,
|
map,
|
||||||
mergeMap,
|
mergeMap,
|
||||||
@@ -58,7 +55,12 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* A paginated list of bitstream formats to be shown on the page
|
* A paginated list of bitstream formats to be shown on the page
|
||||||
*/
|
*/
|
||||||
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
bitstreamFormats$: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently selected {@link BitstreamFormat} IDs
|
||||||
|
*/
|
||||||
|
selectedBitstreamFormatIDs$: Observable<string[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current pagination configuration for the page
|
* The current pagination configuration for the page
|
||||||
@@ -125,14 +127,11 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a given bitstream format is selected in the list (checkbox)
|
* Returns the list of all the bitstream formats that are selected in the list (checkbox)
|
||||||
* @param bitstreamFormat
|
|
||||||
*/
|
*/
|
||||||
isSelected(bitstreamFormat: BitstreamFormat): Observable<boolean> {
|
selectedBitstreamFormatIDs(): Observable<string[]> {
|
||||||
return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
|
return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
|
||||||
map((bitstreamFormats: BitstreamFormat[]) => {
|
map((bitstreamFormats: BitstreamFormat[]) => bitstreamFormats.map((selectedFormat) => selectedFormat.id)),
|
||||||
return bitstreamFormats.find((selectedFormat) => selectedFormat.id === bitstreamFormat.id) != null;
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,27 +155,23 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
const prefix = 'admin.registries.bitstream-formats.delete';
|
const prefix = 'admin.registries.bitstream-formats.delete';
|
||||||
const suffix = success ? 'success' : 'failure';
|
const suffix = success ? 'success' : 'failure';
|
||||||
|
|
||||||
const messages = observableCombineLatest(
|
const head: string = this.translateService.instant(`${prefix}.${suffix}.head`);
|
||||||
this.translateService.get(`${prefix}.${suffix}.head`),
|
const content: string = this.translateService.instant(`${prefix}.${suffix}.amount`, { amount: amount });
|
||||||
this.translateService.get(`${prefix}.${suffix}.amount`, { amount: amount }),
|
|
||||||
);
|
|
||||||
messages.subscribe(([head, content]) => {
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this.notificationsService.success(head, content);
|
this.notificationsService.success(head, content);
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(head, content);
|
this.notificationsService.error(head, content);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.bitstreamFormats$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
|
||||||
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
|
|
||||||
switchMap((findListOptions: FindListOptions) => {
|
switchMap((findListOptions: FindListOptions) => {
|
||||||
return this.bitstreamFormatService.findAll(findListOptions);
|
return this.bitstreamFormatService.findAll(findListOptions);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
this.selectedBitstreamFormatIDs$ = this.selectedBitstreamFormatIDs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -27,14 +27,14 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(schema) | async}">
|
[ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}">
|
||||||
<td>
|
<td>
|
||||||
<label class="mb-0">
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[checked]="isSelected(schema) | async"
|
[checked]="(selectedMetadataSchemaIDs$ | async)?.includes(schema.id)"
|
||||||
(change)="selectMetadataSchema(schema, $event)"
|
(change)="selectMetadataSchema(schema, $event)"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{((isSelected(schema) | async) ? 'admin.registries.metadata.schemas.deselect' : 'admin.registries.metadata.schemas.select') | translate}}</span>
|
<span class="sr-only">{{(((selectedMetadataSchemaIDs$ | async)?.includes(schema.id)) ? 'admin.registries.metadata.schemas.deselect' : 'admin.registries.metadata.schemas.select') | translate}}</span>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>
|
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>
|
||||||
|
@@ -17,9 +17,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { FormBuilderService } from 'src/app/shared/form/builder/form-builder.service';
|
import { FormBuilderService } from 'src/app/shared/form/builder/form-builder.service';
|
||||||
|
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
|
||||||
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||||
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
|
||||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
@@ -36,7 +34,9 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.u
|
|||||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
|
import { RegistryServiceStub } from '../../../shared/testing/registry.service.stub';
|
||||||
import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub';
|
import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub';
|
||||||
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||||
import { MetadataRegistryComponent } from './metadata-registry.component';
|
import { MetadataRegistryComponent } from './metadata-registry.component';
|
||||||
import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-schema-form.component';
|
import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-schema-form.component';
|
||||||
@@ -44,9 +44,11 @@ import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-sch
|
|||||||
describe('MetadataRegistryComponent', () => {
|
describe('MetadataRegistryComponent', () => {
|
||||||
let comp: MetadataRegistryComponent;
|
let comp: MetadataRegistryComponent;
|
||||||
let fixture: ComponentFixture<MetadataRegistryComponent>;
|
let fixture: ComponentFixture<MetadataRegistryComponent>;
|
||||||
let registryService: RegistryService;
|
|
||||||
let paginationService;
|
let paginationService: PaginationServiceStub;
|
||||||
const mockSchemasList = [
|
let registryService: RegistryServiceStub;
|
||||||
|
|
||||||
|
const mockSchemasList: MetadataSchema[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
_links: {
|
_links: {
|
||||||
@@ -67,25 +69,7 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
prefix: 'mock',
|
prefix: 'mock',
|
||||||
namespace: 'http://dspace.org/mockschema',
|
namespace: 'http://dspace.org/mockschema',
|
||||||
},
|
},
|
||||||
];
|
] as MetadataSchema[];
|
||||||
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
|
||||||
const registryServiceStub = {
|
|
||||||
getMetadataSchemas: () => mockSchemas,
|
|
||||||
getActiveMetadataSchema: () => observableOf(undefined),
|
|
||||||
getSelectedMetadataSchemas: () => observableOf([]),
|
|
||||||
editMetadataSchema: (schema) => {
|
|
||||||
},
|
|
||||||
cancelEditMetadataSchema: () => {
|
|
||||||
},
|
|
||||||
deleteMetadataSchema: () => observableOf(new RestResponse(true, 200, 'OK')),
|
|
||||||
deselectAllMetadataSchema: () => {
|
|
||||||
},
|
|
||||||
clearMetadataSchemaRequests: () => observableOf(undefined),
|
|
||||||
};
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
|
|
||||||
paginationService = new PaginationServiceStub();
|
|
||||||
|
|
||||||
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||||
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||||
@@ -109,6 +93,10 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
paginationService = new PaginationServiceStub();
|
||||||
|
registryService = new RegistryServiceStub();
|
||||||
|
spyOn(registryService, 'getMetadataSchemas').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)));
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -120,7 +108,7 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
EnumKeysPipe,
|
EnumKeysPipe,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryService },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{
|
{
|
||||||
@@ -190,7 +178,7 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
it('should cancel editing the selected schema when clicked again', waitForAsync(() => {
|
it('should cancel editing the selected schema when clicked again', waitForAsync(() => {
|
||||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0] as MetadataSchema));
|
comp.activeMetadataSchema$ = observableOf(mockSchemasList[0] as MetadataSchema);
|
||||||
spyOn(registryService, 'cancelEditMetadataSchema');
|
spyOn(registryService, 'cancelEditMetadataSchema');
|
||||||
row.click();
|
row.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -205,7 +193,7 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(registryService, 'deleteMetadataSchema').and.callThrough();
|
spyOn(registryService, 'deleteMetadataSchema').and.callThrough();
|
||||||
spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas as MetadataSchema[]));
|
comp.selectedMetadataSchemaIDs$ = observableOf(selectedSchemas.map((selectedSchema: MetadataSchema) => selectedSchema.id));
|
||||||
comp.deleteSchemas();
|
comp.deleteSchemas();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
@@ -7,19 +7,17 @@ import {
|
|||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import { RouterLink } from '@angular/router';
|
||||||
Router,
|
|
||||||
RouterLink,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
import {
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
TranslateService,
|
TranslateService,
|
||||||
} from '@ngx-translate/core';
|
} from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest as observableCombineLatest,
|
|
||||||
Observable,
|
Observable,
|
||||||
|
Subscription,
|
||||||
zip,
|
zip,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
@@ -36,7 +34,6 @@ import { PaginationService } from '../../../core/pagination/pagination.service';
|
|||||||
import { RegistryService } from '../../../core/registry/registry.service';
|
import { RegistryService } from '../../../core/registry/registry.service';
|
||||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||||
@@ -63,13 +60,23 @@ import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-sch
|
|||||||
* A component used for managing all existing metadata schemas within the repository.
|
* A component used for managing all existing metadata schemas within the repository.
|
||||||
* The admin can create, edit or delete metadata schemas here.
|
* The admin can create, edit or delete metadata schemas here.
|
||||||
*/
|
*/
|
||||||
export class MetadataRegistryComponent implements OnDestroy {
|
export class MetadataRegistryComponent implements OnDestroy, OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all the current metadata schemas within the repository
|
* A list of all the current metadata schemas within the repository
|
||||||
*/
|
*/
|
||||||
metadataSchemas: Observable<RemoteData<PaginatedList<MetadataSchema>>>;
|
metadataSchemas: Observable<RemoteData<PaginatedList<MetadataSchema>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link MetadataSchema}that is being edited
|
||||||
|
*/
|
||||||
|
activeMetadataSchema$: Observable<MetadataSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected {@link MetadataSchema} IDs
|
||||||
|
*/
|
||||||
|
selectedMetadataSchemaIDs$: Observable<number[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination config used to display the list of metadata schemas
|
* Pagination config used to display the list of metadata schemas
|
||||||
*/
|
*/
|
||||||
@@ -79,15 +86,25 @@ export class MetadataRegistryComponent implements OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the list of MetadataSchemas needs an update
|
* Whether the list of MetadataSchemas needs an update
|
||||||
*/
|
*/
|
||||||
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
constructor(private registryService: RegistryService,
|
subscriptions: Subscription[] = [];
|
||||||
private notificationsService: NotificationsService,
|
|
||||||
private router: Router,
|
constructor(
|
||||||
private paginationService: PaginationService,
|
protected registryService: RegistryService,
|
||||||
private translateService: TranslateService) {
|
protected notificationsService: NotificationsService,
|
||||||
|
protected paginationService: PaginationService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema();
|
||||||
|
this.selectedMetadataSchemaIDs$ = this.registryService.getSelectedMetadataSchemas().pipe(
|
||||||
|
map((schemas: MetadataSchema[]) => schemas.map((schema: MetadataSchema) => schema.id)),
|
||||||
|
);
|
||||||
this.updateSchemas();
|
this.updateSchemas();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,30 +133,13 @@ export class MetadataRegistryComponent implements OnDestroy {
|
|||||||
* @param schema
|
* @param schema
|
||||||
*/
|
*/
|
||||||
editSchema(schema: MetadataSchema) {
|
editSchema(schema: MetadataSchema) {
|
||||||
this.getActiveSchema().pipe(take(1)).subscribe((activeSchema) => {
|
this.subscriptions.push(this.activeMetadataSchema$.pipe(take(1)).subscribe((activeSchema: MetadataSchema) => {
|
||||||
if (schema === activeSchema) {
|
if (schema === activeSchema) {
|
||||||
this.registryService.cancelEditMetadataSchema();
|
this.registryService.cancelEditMetadataSchema();
|
||||||
} else {
|
} else {
|
||||||
this.registryService.editMetadataSchema(schema);
|
this.registryService.editMetadataSchema(schema);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the given metadata schema is active (being edited)
|
|
||||||
* @param schema
|
|
||||||
*/
|
|
||||||
isActive(schema: MetadataSchema): Observable<boolean> {
|
|
||||||
return this.getActiveSchema().pipe(
|
|
||||||
map((activeSchema) => schema === activeSchema),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the active metadata schema (being edited)
|
|
||||||
*/
|
|
||||||
getActiveSchema(): Observable<MetadataSchema> {
|
|
||||||
return this.registryService.getActiveMetadataSchema();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,42 +153,25 @@ export class MetadataRegistryComponent implements OnDestroy {
|
|||||||
this.registryService.deselectMetadataSchema(schema);
|
this.registryService.deselectMetadataSchema(schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a given metadata schema is selected in the list (checkbox)
|
|
||||||
* @param schema
|
|
||||||
*/
|
|
||||||
isSelected(schema: MetadataSchema): Observable<boolean> {
|
|
||||||
return this.registryService.getSelectedMetadataSchemas().pipe(
|
|
||||||
map((schemas) => schemas.find((selectedSchema) => selectedSchema === schema) != null),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all the selected metadata schemas
|
* Delete all the selected metadata schemas
|
||||||
*/
|
*/
|
||||||
deleteSchemas() {
|
deleteSchemas() {
|
||||||
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
this.subscriptions.push(this.selectedMetadataSchemaIDs$.pipe(
|
||||||
(schemas) => {
|
take(1),
|
||||||
const tasks$ = [];
|
switchMap((schemaIDs: number[]) => zip(schemaIDs.map((schemaID: number) => this.registryService.deleteMetadataSchema(schemaID).pipe(getFirstCompletedRemoteData())))),
|
||||||
for (const schema of schemas) {
|
).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||||
if (hasValue(schema.id)) {
|
const successResponses: RemoteData<NoContent>[] = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||||
tasks$.push(this.registryService.deleteMetadataSchema(schema.id).pipe(getFirstCompletedRemoteData()));
|
const failedResponses: RemoteData<NoContent>[] = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||||
}
|
if (successResponses.length > 0) {
|
||||||
}
|
this.showNotification(true, successResponses.length);
|
||||||
zip(...tasks$).subscribe((responses: RemoteData<NoContent>[]) => {
|
}
|
||||||
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
if (failedResponses.length > 0) {
|
||||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
this.showNotification(false, failedResponses.length);
|
||||||
if (successResponses.length > 0) {
|
}
|
||||||
this.showNotification(true, successResponses.length);
|
this.registryService.deselectAllMetadataSchema();
|
||||||
}
|
this.registryService.cancelEditMetadataSchema();
|
||||||
if (failedResponses.length > 0) {
|
}));
|
||||||
this.showNotification(false, failedResponses.length);
|
|
||||||
}
|
|
||||||
this.registryService.deselectAllMetadataSchema();
|
|
||||||
this.registryService.cancelEditMetadataSchema();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,20 +182,20 @@ export class MetadataRegistryComponent implements OnDestroy {
|
|||||||
showNotification(success: boolean, amount: number) {
|
showNotification(success: boolean, amount: number) {
|
||||||
const prefix = 'admin.registries.schema.notification';
|
const prefix = 'admin.registries.schema.notification';
|
||||||
const suffix = success ? 'success' : 'failure';
|
const suffix = success ? 'success' : 'failure';
|
||||||
const messages = observableCombineLatest(
|
|
||||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
const head: string = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`);
|
||||||
this.translateService.get(`${prefix}.deleted.${suffix}`, { amount: amount }),
|
const content: string = this.translateService.instant(`${prefix}.deleted.${suffix}`, { amount: amount });
|
||||||
);
|
|
||||||
messages.subscribe(([head, content]) => {
|
if (success) {
|
||||||
if (success) {
|
this.notificationsService.success(head, content);
|
||||||
this.notificationsService.success(head, content);
|
} else {
|
||||||
} else {
|
this.notificationsService.error(head, content);
|
||||||
this.notificationsService.error(head, content);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.paginationService.clearPagination(this.config.id);
|
this.paginationService.clearPagination(this.config.id);
|
||||||
|
this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div *ngIf="registryService.getActiveMetadataSchema() | async; then editheader; else createHeader"></div>
|
<div *ngIf="activeMetadataSchema$ | async; then editheader; else createHeader"></div>
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<ng-template #createHeader>
|
||||||
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
inject,
|
inject,
|
||||||
@@ -16,42 +16,26 @@ import { RegistryService } from '../../../../core/registry/registry.service';
|
|||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
import { FormComponent } from '../../../../shared/form/form.component';
|
import { FormComponent } from '../../../../shared/form/form.component';
|
||||||
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
||||||
|
import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub';
|
||||||
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
||||||
import { MetadataSchemaFormComponent } from './metadata-schema-form.component';
|
import { MetadataSchemaFormComponent } from './metadata-schema-form.component';
|
||||||
|
|
||||||
describe('MetadataSchemaFormComponent', () => {
|
describe('MetadataSchemaFormComponent', () => {
|
||||||
let component: MetadataSchemaFormComponent;
|
let component: MetadataSchemaFormComponent;
|
||||||
let fixture: ComponentFixture<MetadataSchemaFormComponent>;
|
let fixture: ComponentFixture<MetadataSchemaFormComponent>;
|
||||||
let registryService: RegistryService;
|
|
||||||
|
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
let registryService: RegistryServiceStub;
|
||||||
const registryServiceStub = {
|
|
||||||
getActiveMetadataSchema: () => observableOf(undefined),
|
|
||||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
|
||||||
cancelEditMetadataSchema: () => {
|
|
||||||
},
|
|
||||||
clearMetadataSchemaRequests: () => observableOf(undefined),
|
|
||||||
};
|
|
||||||
const formBuilderServiceStub = {
|
|
||||||
createFormGroup: () => {
|
|
||||||
return {
|
|
||||||
patchValue: () => {
|
|
||||||
},
|
|
||||||
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
registryService = new RegistryServiceStub();
|
||||||
|
|
||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataSchemaFormComponent, EnumKeysPipe],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataSchemaFormComponent, EnumKeysPipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryService },
|
||||||
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
|
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
})
|
})
|
||||||
.overrideComponent(MetadataSchemaFormComponent, {
|
.overrideComponent(MetadataSchemaFormComponent, {
|
||||||
remove: {
|
remove: {
|
||||||
@@ -88,7 +72,7 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
|
|
||||||
describe('without an active schema', () => {
|
describe('without an active schema', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(undefined));
|
component.activeMetadataSchema$ = observableOf(undefined);
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -107,7 +91,7 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
} as MetadataSchema);
|
} as MetadataSchema);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId));
|
component.activeMetadataSchema$ = observableOf(expectedWithId);
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
@@ -21,13 +21,13 @@ import {
|
|||||||
TranslateService,
|
TranslateService,
|
||||||
} from '@ngx-translate/core';
|
} from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
combineLatest,
|
|
||||||
Observable,
|
Observable,
|
||||||
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
map,
|
||||||
switchMap,
|
switchMap,
|
||||||
take,
|
take,
|
||||||
tap,
|
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||||
@@ -102,64 +102,71 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
constructor(public registryService: RegistryService, private formBuilderService: FormBuilderService, private translateService: TranslateService) {
|
/**
|
||||||
|
* The {@link MetadataSchema} that is currently being edited
|
||||||
|
*/
|
||||||
|
activeMetadataSchema$: Observable<MetadataSchema>;
|
||||||
|
|
||||||
|
subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected registryService: RegistryService,
|
||||||
|
protected formBuilderService: FormBuilderService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
combineLatest([
|
this.name = new DynamicInputModel({
|
||||||
this.translateService.get(`${this.messagePrefix}.name`),
|
id: 'name',
|
||||||
this.translateService.get(`${this.messagePrefix}.namespace`),
|
label: this.translateService.instant(`${this.messagePrefix}.name`),
|
||||||
]).subscribe(([name, namespace]) => {
|
name: 'name',
|
||||||
this.name = new DynamicInputModel({
|
validators: {
|
||||||
id: 'name',
|
required: null,
|
||||||
label: name,
|
pattern: '^[^. ,]*$',
|
||||||
name: 'name',
|
maxLength: 32,
|
||||||
validators: {
|
},
|
||||||
required: null,
|
required: true,
|
||||||
pattern: '^[^. ,]*$',
|
errorMessages: {
|
||||||
maxLength: 32,
|
pattern: 'error.validation.metadata.name.invalid-pattern',
|
||||||
},
|
maxLength: 'error.validation.metadata.name.max-length',
|
||||||
required: true,
|
},
|
||||||
errorMessages: {
|
|
||||||
pattern: 'error.validation.metadata.name.invalid-pattern',
|
|
||||||
maxLength: 'error.validation.metadata.name.max-length',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.namespace = new DynamicInputModel({
|
|
||||||
id: 'namespace',
|
|
||||||
label: namespace,
|
|
||||||
name: 'namespace',
|
|
||||||
validators: {
|
|
||||||
required: null,
|
|
||||||
maxLength: 256,
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
errorMessages: {
|
|
||||||
maxLength: 'error.validation.metadata.namespace.max-length',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.formModel = [
|
|
||||||
new DynamicFormGroupModel(
|
|
||||||
{
|
|
||||||
id: 'metadatadataschemagroup',
|
|
||||||
group:[this.namespace, this.name],
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
|
||||||
this.registryService.getActiveMetadataSchema().subscribe((schema: MetadataSchema) => {
|
|
||||||
if (schema == null) {
|
|
||||||
this.clearFields();
|
|
||||||
} else {
|
|
||||||
this.formGroup.patchValue({
|
|
||||||
metadatadataschemagroup: {
|
|
||||||
name: schema.prefix,
|
|
||||||
namespace: schema.namespace,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.name.disabled = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
this.namespace = new DynamicInputModel({
|
||||||
|
id: 'namespace',
|
||||||
|
label: this.translateService.instant(`${this.messagePrefix}.namespace`),
|
||||||
|
name: 'namespace',
|
||||||
|
validators: {
|
||||||
|
required: null,
|
||||||
|
maxLength: 256,
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
errorMessages: {
|
||||||
|
maxLength: 'error.validation.metadata.namespace.max-length',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.formModel = [
|
||||||
|
new DynamicFormGroupModel(
|
||||||
|
{
|
||||||
|
id: 'metadatadataschemagroup',
|
||||||
|
group:[this.namespace, this.name],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
|
this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema();
|
||||||
|
this.subscriptions.push(this.activeMetadataSchema$.subscribe((schema: MetadataSchema) => {
|
||||||
|
if (schema == null) {
|
||||||
|
this.clearFields();
|
||||||
|
} else {
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
metadatadataschemagroup: {
|
||||||
|
name: schema.prefix,
|
||||||
|
namespace: schema.namespace,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.name.disabled = true;
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,48 +183,29 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created schema using the EventEmitter submitForm
|
* Emit the updated/created schema using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
this.registryService
|
this.activeMetadataSchema$.pipe(
|
||||||
.getActiveMetadataSchema()
|
take(1),
|
||||||
.pipe(
|
switchMap((schema: MetadataSchema) => {
|
||||||
take(1),
|
const metadataValues = {
|
||||||
switchMap((schema: MetadataSchema) => {
|
prefix: this.name.value,
|
||||||
const metadataValues = {
|
namespace: this.namespace.value,
|
||||||
prefix: this.name.value,
|
};
|
||||||
namespace: this.namespace.value,
|
if (schema == null) {
|
||||||
};
|
return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), metadataValues));
|
||||||
|
} else {
|
||||||
let createOrUpdate$: Observable<MetadataSchema>;
|
return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
||||||
|
namespace: metadataValues.namespace,
|
||||||
if (schema == null) {
|
}));
|
||||||
createOrUpdate$ =
|
}
|
||||||
this.registryService.createOrUpdateMetadataSchema(
|
}),
|
||||||
Object.assign(new MetadataSchema(), metadataValues),
|
switchMap((updatedOrCreatedSchema: MetadataSchema) => this.registryService.clearMetadataSchemaRequests().pipe(
|
||||||
);
|
map(() => updatedOrCreatedSchema),
|
||||||
} else {
|
)),
|
||||||
const updatedSchema = Object.assign(
|
).subscribe((updatedOrCreatedSchema: MetadataSchema) => {
|
||||||
new MetadataSchema(),
|
this.submitForm.emit(updatedOrCreatedSchema);
|
||||||
schema,
|
this.clearFields();
|
||||||
{
|
this.registryService.cancelEditMetadataSchema();
|
||||||
namespace: metadataValues.namespace,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
createOrUpdate$ =
|
|
||||||
this.registryService.createOrUpdateMetadataSchema(
|
|
||||||
updatedSchema,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return createOrUpdate$;
|
|
||||||
}),
|
|
||||||
tap(() => {
|
|
||||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.subscribe((updatedOrCreatedSchema: MetadataSchema) => {
|
|
||||||
this.submitForm.emit(updatedOrCreatedSchema);
|
|
||||||
this.clearFields();
|
|
||||||
this.registryService.cancelEditMetadataSchema();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,5 +221,6 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.onCancel();
|
this.onCancel();
|
||||||
|
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
inject,
|
inject,
|
||||||
@@ -17,13 +17,15 @@ import { RegistryService } from '../../../../core/registry/registry.service';
|
|||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
import { FormComponent } from '../../../../shared/form/form.component';
|
import { FormComponent } from '../../../../shared/form/form.component';
|
||||||
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
||||||
|
import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub';
|
||||||
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
||||||
import { MetadataFieldFormComponent } from './metadata-field-form.component';
|
import { MetadataFieldFormComponent } from './metadata-field-form.component';
|
||||||
|
|
||||||
describe('MetadataFieldFormComponent', () => {
|
describe('MetadataFieldFormComponent', () => {
|
||||||
let component: MetadataFieldFormComponent;
|
let component: MetadataFieldFormComponent;
|
||||||
let fixture: ComponentFixture<MetadataFieldFormComponent>;
|
let fixture: ComponentFixture<MetadataFieldFormComponent>;
|
||||||
let registryService: RegistryService;
|
|
||||||
|
let registryService: RegistryServiceStub;
|
||||||
|
|
||||||
const metadataSchema = Object.assign(new MetadataSchema(), {
|
const metadataSchema = Object.assign(new MetadataSchema(), {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -31,37 +33,16 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
prefix: 'fake',
|
prefix: 'fake',
|
||||||
});
|
});
|
||||||
|
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
|
||||||
const registryServiceStub = {
|
|
||||||
getActiveMetadataField: () => observableOf(undefined),
|
|
||||||
createMetadataField: (field: MetadataField) => observableOf(field),
|
|
||||||
updateMetadataField: (field: MetadataField) => observableOf(field),
|
|
||||||
cancelEditMetadataField: () => {
|
|
||||||
},
|
|
||||||
cancelEditMetadataSchema: () => {
|
|
||||||
},
|
|
||||||
clearMetadataFieldRequests: () => observableOf(undefined),
|
|
||||||
};
|
|
||||||
const formBuilderServiceStub = {
|
|
||||||
createFormGroup: () => {
|
|
||||||
return {
|
|
||||||
patchValue: () => {
|
|
||||||
},
|
|
||||||
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
registryService = new RegistryServiceStub();
|
||||||
|
|
||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataFieldFormComponent, EnumKeysPipe],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataFieldFormComponent, EnumKeysPipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryService },
|
||||||
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
|
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
})
|
})
|
||||||
.overrideComponent(MetadataFieldFormComponent, {
|
.overrideComponent(MetadataFieldFormComponent, {
|
||||||
remove: { imports: [FormComponent] },
|
remove: { imports: [FormComponent] },
|
||||||
|
@@ -31,8 +31,8 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let field of fields?.page"
|
<tr *ngFor="let field of fields?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
[ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}">
|
||||||
<td *ngVar="(isSelected(field) | async) as selected">
|
<td *ngVar="(selectedMetadataFieldIDs$ | async)?.includes(field.id) as selected">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate"
|
[attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate"
|
||||||
[checked]="selected"
|
[checked]="selected"
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
inject,
|
inject,
|
||||||
@@ -7,16 +7,12 @@ import {
|
|||||||
waitForAsync,
|
waitForAsync,
|
||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import {
|
import { ActivatedRoute } from '@angular/router';
|
||||||
ActivatedRoute,
|
|
||||||
Router,
|
|
||||||
} from '@angular/router';
|
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
|
||||||
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||||
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||||
@@ -34,7 +30,7 @@ import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
|||||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
import { RegistryServiceStub } from '../../../shared/testing/registry.service.stub';
|
||||||
import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub';
|
import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub';
|
||||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||||
@@ -45,8 +41,12 @@ import { MetadataSchemaComponent } from './metadata-schema.component';
|
|||||||
describe('MetadataSchemaComponent', () => {
|
describe('MetadataSchemaComponent', () => {
|
||||||
let comp: MetadataSchemaComponent;
|
let comp: MetadataSchemaComponent;
|
||||||
let fixture: ComponentFixture<MetadataSchemaComponent>;
|
let fixture: ComponentFixture<MetadataSchemaComponent>;
|
||||||
let registryService: RegistryService;
|
|
||||||
const mockSchemasList = [
|
let registryService: RegistryServiceStub;
|
||||||
|
let activatedRoute: ActivatedRouteStub;
|
||||||
|
let paginationService: PaginationServiceStub;
|
||||||
|
|
||||||
|
const mockSchemasList: MetadataSchema[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
_links: {
|
_links: {
|
||||||
@@ -67,8 +67,8 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
prefix: 'mock',
|
prefix: 'mock',
|
||||||
namespace: 'http://dspace.org/mockschema',
|
namespace: 'http://dspace.org/mockschema',
|
||||||
},
|
},
|
||||||
];
|
] as MetadataSchema[];
|
||||||
const mockFieldsList = [
|
const mockFieldsList: MetadataField[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
_links: {
|
_links: {
|
||||||
@@ -117,33 +117,8 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
scopeNote: null,
|
scopeNote: null,
|
||||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]),
|
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]),
|
||||||
},
|
},
|
||||||
];
|
] as MetadataField[];
|
||||||
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
|
||||||
const registryServiceStub = {
|
|
||||||
getMetadataSchemas: () => mockSchemas,
|
|
||||||
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))),
|
|
||||||
getMetadataSchemaByPrefix: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]),
|
|
||||||
getActiveMetadataField: () => observableOf(undefined),
|
|
||||||
getSelectedMetadataFields: () => observableOf([]),
|
|
||||||
editMetadataField: (schema) => {
|
|
||||||
},
|
|
||||||
cancelEditMetadataField: () => {
|
|
||||||
},
|
|
||||||
deleteMetadataField: () => observableOf(new RestResponse(true, 200, 'OK')),
|
|
||||||
deselectAllMetadataField: () => {
|
|
||||||
},
|
|
||||||
clearMetadataFieldRequests: () => observableOf(undefined),
|
|
||||||
};
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
const schemaNameParam = 'mock';
|
const schemaNameParam = 'mock';
|
||||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
|
||||||
params: observableOf({
|
|
||||||
schemaName: schemaNameParam,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const paginationService = new PaginationServiceStub();
|
|
||||||
|
|
||||||
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||||
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||||
@@ -162,6 +137,14 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
activatedRoute = new ActivatedRouteStub({
|
||||||
|
schemaName: schemaNameParam,
|
||||||
|
});
|
||||||
|
paginationService = new PaginationServiceStub();
|
||||||
|
registryService = new RegistryServiceStub();
|
||||||
|
spyOn(registryService, 'getMetadataFieldsBySchema').and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))));
|
||||||
|
spyOn(registryService, 'getMetadataSchemaByPrefix').and.callFake((schemaName) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]));
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -174,10 +157,9 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
VarDirective,
|
VarDirective,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryService },
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
{ provide: Router, useValue: new RouterStub() },
|
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{
|
{
|
||||||
provide: NotificationsService,
|
provide: NotificationsService,
|
||||||
@@ -187,7 +169,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||||
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
|
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
})
|
})
|
||||||
.overrideComponent(MetadataSchemaComponent, {
|
.overrideComponent(MetadataSchemaComponent, {
|
||||||
remove: {
|
remove: {
|
||||||
@@ -242,7 +224,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
it('should cancel editing the selected field when clicked again', waitForAsync(() => {
|
it('should cancel editing the selected field when clicked again', waitForAsync(() => {
|
||||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2] as MetadataField));
|
comp.activeField$ = observableOf(mockFieldsList[2] as MetadataField);
|
||||||
spyOn(registryService, 'cancelEditMetadataField');
|
spyOn(registryService, 'cancelEditMetadataField');
|
||||||
row.click();
|
row.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -257,7 +239,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(registryService, 'deleteMetadataField').and.callThrough();
|
spyOn(registryService, 'deleteMetadataField').and.callThrough();
|
||||||
spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields as MetadataField[]));
|
comp.selectedMetadataFieldIDs$ = observableOf(selectedFields.map((metadataField: MetadataField) => metadataField.id));
|
||||||
comp.deleteFields();
|
comp.deleteFields();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
@@ -20,9 +20,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
combineLatest as observableCombineLatest,
|
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
|
Subscription,
|
||||||
zip,
|
zip,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
@@ -42,7 +42,6 @@ import {
|
|||||||
getFirstCompletedRemoteData,
|
getFirstCompletedRemoteData,
|
||||||
getFirstSucceededRemoteDataPayload,
|
getFirstSucceededRemoteDataPayload,
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||||
@@ -71,7 +70,7 @@ import { MetadataFieldFormComponent } from './metadata-field-form/metadata-field
|
|||||||
* A component used for managing all existing metadata fields within the current metadata schema.
|
* A component used for managing all existing metadata fields within the current metadata schema.
|
||||||
* The admin can create, edit or delete metadata fields here.
|
* The admin can create, edit or delete metadata fields here.
|
||||||
*/
|
*/
|
||||||
export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
export class MetadataSchemaComponent implements OnDestroy, OnInit {
|
||||||
/**
|
/**
|
||||||
* The metadata schema
|
* The metadata schema
|
||||||
*/
|
*/
|
||||||
@@ -96,26 +95,33 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
constructor(private registryService: RegistryService,
|
/**
|
||||||
private route: ActivatedRoute,
|
* The current {@link MetadataField} that is being edited
|
||||||
private notificationsService: NotificationsService,
|
*/
|
||||||
private paginationService: PaginationService,
|
activeField$: Observable<MetadataField>;
|
||||||
private translateService: TranslateService) {
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected {@link MetadataField} IDs
|
||||||
|
*/
|
||||||
|
selectedMetadataFieldIDs$: Observable<number[]>;
|
||||||
|
|
||||||
|
subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected registryService: RegistryService,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected paginationService: PaginationService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.params.subscribe((params) => {
|
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(this.route.snapshot.params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||||
this.initialize(params);
|
this.activeField$ = this.registryService.getActiveMetadataField();
|
||||||
});
|
this.selectedMetadataFieldIDs$ = this.registryService.getSelectedMetadataFields().pipe(
|
||||||
}
|
map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => metadataField.id)),
|
||||||
|
);
|
||||||
/**
|
|
||||||
* Initialize the component using the params within the url (schemaName)
|
|
||||||
* @param params
|
|
||||||
*/
|
|
||||||
initialize(params) {
|
|
||||||
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
|
||||||
this.updateFields();
|
this.updateFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,30 +154,13 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
|||||||
* @param field
|
* @param field
|
||||||
*/
|
*/
|
||||||
editField(field: MetadataField) {
|
editField(field: MetadataField) {
|
||||||
this.getActiveField().pipe(take(1)).subscribe((activeField) => {
|
this.subscriptions.push(this.activeField$.pipe(take(1)).subscribe((activeField) => {
|
||||||
if (field === activeField) {
|
if (field === activeField) {
|
||||||
this.registryService.cancelEditMetadataField();
|
this.registryService.cancelEditMetadataField();
|
||||||
} else {
|
} else {
|
||||||
this.registryService.editMetadataField(field);
|
this.registryService.editMetadataField(field);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the given metadata field is active (being edited)
|
|
||||||
* @param field
|
|
||||||
*/
|
|
||||||
isActive(field: MetadataField): Observable<boolean> {
|
|
||||||
return this.getActiveField().pipe(
|
|
||||||
map((activeField) => field === activeField),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the active metadata field (being edited)
|
|
||||||
*/
|
|
||||||
getActiveField(): Observable<MetadataField> {
|
|
||||||
return this.registryService.getActiveMetadataField();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -185,42 +174,25 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
|||||||
this.registryService.deselectMetadataField(field);
|
this.registryService.deselectMetadataField(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a given metadata field is selected in the list (checkbox)
|
|
||||||
* @param field
|
|
||||||
*/
|
|
||||||
isSelected(field: MetadataField): Observable<boolean> {
|
|
||||||
return this.registryService.getSelectedMetadataFields().pipe(
|
|
||||||
map((fields) => fields.find((selectedField) => selectedField === field) != null),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all the selected metadata fields
|
* Delete all the selected metadata fields
|
||||||
*/
|
*/
|
||||||
deleteFields() {
|
deleteFields() {
|
||||||
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
|
this.subscriptions.push(this.selectedMetadataFieldIDs$.pipe(
|
||||||
(fields) => {
|
take(1),
|
||||||
const tasks$ = [];
|
switchMap((fieldIDs) => zip(fieldIDs.map((fieldID) => this.registryService.deleteMetadataField(fieldID).pipe(getFirstCompletedRemoteData())))),
|
||||||
for (const field of fields) {
|
).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||||
if (hasValue(field.id)) {
|
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||||
tasks$.push(this.registryService.deleteMetadataField(field.id).pipe(getFirstCompletedRemoteData()));
|
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||||
}
|
if (successResponses.length > 0) {
|
||||||
}
|
this.showNotification(true, successResponses.length);
|
||||||
zip(...tasks$).subscribe((responses: RemoteData<NoContent>[]) => {
|
}
|
||||||
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
if (failedResponses.length > 0) {
|
||||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
this.showNotification(false, failedResponses.length);
|
||||||
if (successResponses.length > 0) {
|
}
|
||||||
this.showNotification(true, successResponses.length);
|
this.registryService.deselectAllMetadataField();
|
||||||
}
|
this.registryService.cancelEditMetadataField();
|
||||||
if (failedResponses.length > 0) {
|
}));
|
||||||
this.showNotification(false, failedResponses.length);
|
|
||||||
}
|
|
||||||
this.registryService.deselectAllMetadataField();
|
|
||||||
this.registryService.cancelEditMetadataField();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -231,21 +203,19 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
|||||||
showNotification(success: boolean, amount: number) {
|
showNotification(success: boolean, amount: number) {
|
||||||
const prefix = 'admin.registries.schema.notification';
|
const prefix = 'admin.registries.schema.notification';
|
||||||
const suffix = success ? 'success' : 'failure';
|
const suffix = success ? 'success' : 'failure';
|
||||||
const messages = observableCombineLatest([
|
const head = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`);
|
||||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
const content = this.translateService.instant(`${prefix}.field.deleted.${suffix}`, { amount: amount });
|
||||||
this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount }),
|
if (success) {
|
||||||
]);
|
this.notificationsService.success(head, content);
|
||||||
messages.subscribe(([head, content]) => {
|
} else {
|
||||||
if (success) {
|
this.notificationsService.error(head, content);
|
||||||
this.notificationsService.success(head, content);
|
}
|
||||||
} else {
|
|
||||||
this.notificationsService.error(head, content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.paginationService.clearPagination(this.config.id);
|
this.paginationService.clearPagination(this.config.id);
|
||||||
this.registryService.deselectAllMetadataField();
|
this.registryService.deselectAllMetadataField();
|
||||||
|
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -11,8 +11,8 @@ import {
|
|||||||
REGISTRIES_MODULE_PATH,
|
REGISTRIES_MODULE_PATH,
|
||||||
REPORTS_MODULE_PATH,
|
REPORTS_MODULE_PATH,
|
||||||
} from './admin-routing-paths';
|
} from './admin-routing-paths';
|
||||||
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
|
import { ThemedAdminSearchPageComponent } from './admin-search-page/themed-admin-search-page.component';
|
||||||
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
import { ThemedAdminWorkflowPageComponent } from './admin-workflow-page/themed-admin-workflow-page.component';
|
||||||
|
|
||||||
export const ROUTES: Route[] = [
|
export const ROUTES: Route[] = [
|
||||||
{
|
{
|
||||||
@@ -28,13 +28,13 @@ export const ROUTES: Route[] = [
|
|||||||
{
|
{
|
||||||
path: 'search',
|
path: 'search',
|
||||||
resolve: { breadcrumb: i18nBreadcrumbResolver },
|
resolve: { breadcrumb: i18nBreadcrumbResolver },
|
||||||
component: AdminSearchPageComponent,
|
component: ThemedAdminSearchPageComponent,
|
||||||
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' },
|
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'workflow',
|
path: 'workflow',
|
||||||
resolve: { breadcrumb: i18nBreadcrumbResolver },
|
resolve: { breadcrumb: i18nBreadcrumbResolver },
|
||||||
component: AdminWorkflowPageComponent,
|
component: ThemedAdminWorkflowPageComponent,
|
||||||
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' },
|
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -4,7 +4,7 @@ import { Context } from '../../core/shared/context.model';
|
|||||||
import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component';
|
import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-admin-search-page',
|
selector: 'ds-base-admin-search-page',
|
||||||
templateUrl: './admin-search-page.component.html',
|
templateUrl: './admin-search-page.component.html',
|
||||||
styleUrls: ['./admin-search-page.component.scss'],
|
styleUrls: ['./admin-search-page.component.scss'],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
|
import { AdminSearchPageComponent } from './admin-search-page.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for {@link AdminSearchPageComponent}
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-admin-search-page',
|
||||||
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
|
standalone: true,
|
||||||
|
imports: [AdminSearchPageComponent],
|
||||||
|
})
|
||||||
|
export class ThemedAdminSearchPageComponent extends ThemedComponent<AdminSearchPageComponent> {
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'AdminSearchPageComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../themes/${themeName}/app/admin/admin-search-page/admin-search-page.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./admin-search-page.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -14,7 +14,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="sidebar-collapsible-element-outer-wrapper">
|
<div class="sidebar-collapsible-element-outer-wrapper">
|
||||||
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item">
|
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item">
|
||||||
<span [id]="adminMenuSectionTitleId(section.id)">{{itemModel.text | translate}}</span>
|
<span [id]="adminMenuSectionTitleId(section.id)" [attr.data-test]="adminMenuSectionTitleId(section.id) | dsBrowserOnly">
|
||||||
|
{{itemModel.text | translate}}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -17,6 +17,7 @@ import { MenuID } from '../../../shared/menu/menu-id.model';
|
|||||||
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
|
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
|
||||||
import { MenuSection } from '../../../shared/menu/menu-section.model';
|
import { MenuSection } from '../../../shared/menu/menu-section.model';
|
||||||
import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component';
|
import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component';
|
||||||
|
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a non-expandable section in the admin sidebar
|
* Represents a non-expandable section in the admin sidebar
|
||||||
@@ -26,7 +27,7 @@ import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-sec
|
|||||||
templateUrl: './admin-sidebar-section.component.html',
|
templateUrl: './admin-sidebar-section.component.html',
|
||||||
styleUrls: ['./admin-sidebar-section.component.scss'],
|
styleUrls: ['./admin-sidebar-section.component.scss'],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgClass, RouterLink, TranslateModule],
|
imports: [NgClass, RouterLink, TranslateModule, BrowserOnlyPipe],
|
||||||
|
|
||||||
})
|
})
|
||||||
export class AdminSidebarSectionComponent extends MenuSectionComponent implements OnInit {
|
export class AdminSidebarSectionComponent extends MenuSectionComponent implements OnInit {
|
||||||
|
@@ -42,6 +42,7 @@
|
|||||||
<div class="sidebar-full-width-container" id="sidebar-collapse-toggle-container">
|
<div class="sidebar-full-width-container" id="sidebar-collapse-toggle-container">
|
||||||
<a class="sidebar-section-wrapper sidebar-full-width-container"
|
<a class="sidebar-section-wrapper sidebar-full-width-container"
|
||||||
id="sidebar-collapse-toggle"
|
id="sidebar-collapse-toggle"
|
||||||
|
[attr.data-test]="'sidebar-collapse-toggle' | dsBrowserOnly"
|
||||||
href="javascript:void(0);"
|
href="javascript:void(0);"
|
||||||
(click)="toggle($event)"
|
(click)="toggle($event)"
|
||||||
(keyup.space)="toggle($event)"
|
(keyup.space)="toggle($event)"
|
||||||
|
@@ -36,6 +36,7 @@ import { MenuService } from '../../shared/menu/menu.service';
|
|||||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||||
import { CSSVariableService } from '../../shared/sass-helper/css-variable.service';
|
import { CSSVariableService } from '../../shared/sass-helper/css-variable.service';
|
||||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||||
|
import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component representing the admin sidebar
|
* Component representing the admin sidebar
|
||||||
@@ -46,7 +47,7 @@ import { ThemeService } from '../../shared/theme-support/theme.service';
|
|||||||
styleUrls: ['./admin-sidebar.component.scss'],
|
styleUrls: ['./admin-sidebar.component.scss'],
|
||||||
animations: [slideSidebar],
|
animations: [slideSidebar],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgIf, NgbDropdownModule, NgClass, NgFor, NgComponentOutlet, AsyncPipe, TranslateModule],
|
imports: [NgIf, NgbDropdownModule, NgClass, NgFor, NgComponentOutlet, AsyncPipe, TranslateModule, BrowserOnlyPipe],
|
||||||
})
|
})
|
||||||
export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="sidebar-collapsible-element-outer-wrapper">
|
<div class="sidebar-collapsible-element-outer-wrapper">
|
||||||
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item toggler-wrapper">
|
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item toggler-wrapper">
|
||||||
<span [id]="adminMenuSectionTitleId(section.id)">
|
<span [id]="adminMenuSectionTitleId(section.id)" [attr.data-test]="adminMenuSectionTitleId(section.id) | dsBrowserOnly">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||||
</span>
|
</span>
|
||||||
|
@@ -25,6 +25,7 @@ import { slide } from '../../../shared/animations/slide';
|
|||||||
import { MenuService } from '../../../shared/menu/menu.service';
|
import { MenuService } from '../../../shared/menu/menu.service';
|
||||||
import { MenuID } from '../../../shared/menu/menu-id.model';
|
import { MenuID } from '../../../shared/menu/menu-id.model';
|
||||||
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
|
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
|
||||||
|
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
|
||||||
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
|
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,7 +37,7 @@ import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sid
|
|||||||
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
||||||
animations: [rotate, slide, bgColor],
|
animations: [rotate, slide, bgColor],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgClass, NgComponentOutlet, NgIf, NgFor, AsyncPipe, TranslateModule],
|
imports: [NgClass, NgComponentOutlet, NgIf, NgFor, AsyncPipe, TranslateModule, BrowserOnlyPipe],
|
||||||
})
|
})
|
||||||
|
|
||||||
export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionComponent implements OnInit {
|
export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionComponent implements OnInit {
|
||||||
|
@@ -4,11 +4,13 @@ import { Context } from '../../core/shared/context.model';
|
|||||||
import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component';
|
import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-admin-workflow-page',
|
selector: 'ds-base-admin-workflow-page',
|
||||||
templateUrl: './admin-workflow-page.component.html',
|
templateUrl: './admin-workflow-page.component.html',
|
||||||
styleUrls: ['./admin-workflow-page.component.scss'],
|
styleUrls: ['./admin-workflow-page.component.scss'],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ThemedConfigurationSearchPageComponent],
|
imports: [
|
||||||
|
ThemedConfigurationSearchPageComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
|
import { AdminWorkflowPageComponent } from './admin-workflow-page.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for {@link AdminWorkflowPageComponent}
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-admin-workflow-page',
|
||||||
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
|
standalone: true,
|
||||||
|
imports: [AdminWorkflowPageComponent],
|
||||||
|
})
|
||||||
|
export class ThemedAdminWorkflowPageComponent extends ThemedComponent<AdminWorkflowPageComponent> {
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'AdminWorkflowPageComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../themes/${themeName}/app/admin/admin-workflow-page/admin-workflow-page.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./admin-workflow-page.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -63,7 +63,7 @@ export const APP_ROUTES: Route[] = [
|
|||||||
path: 'home',
|
path: 'home',
|
||||||
loadChildren: () => import('./home-page/home-page-routes')
|
loadChildren: () => import('./home-page/home-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
data: { showBreadcrumbs: false },
|
data: { showBreadcrumbs: false, enableRSS: true },
|
||||||
providers: [provideSuggestionNotificationsState()],
|
providers: [provideSuggestionNotificationsState()],
|
||||||
canActivate: [endUserAgreementCurrentUserGuard],
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
@@ -101,12 +101,14 @@ export const APP_ROUTES: Route[] = [
|
|||||||
path: COMMUNITY_MODULE_PATH,
|
path: COMMUNITY_MODULE_PATH,
|
||||||
loadChildren: () => import('./community-page/community-page-routes')
|
loadChildren: () => import('./community-page/community-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
|
data: { enableRSS: true },
|
||||||
canActivate: [endUserAgreementCurrentUserGuard],
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: COLLECTION_MODULE_PATH,
|
path: COLLECTION_MODULE_PATH,
|
||||||
loadChildren: () => import('./collection-page/collection-page-routes')
|
loadChildren: () => import('./collection-page/collection-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
|
data: { showBreadcrumbs: false, enableRSS: true },
|
||||||
canActivate: [endUserAgreementCurrentUserGuard],
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -137,6 +139,7 @@ export const APP_ROUTES: Route[] = [
|
|||||||
path: 'mydspace',
|
path: 'mydspace',
|
||||||
loadChildren: () => import('./my-dspace-page/my-dspace-page-routes')
|
loadChildren: () => import('./my-dspace-page/my-dspace-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
|
data: { enableRSS: true },
|
||||||
providers: [provideSuggestionNotificationsState()],
|
providers: [provideSuggestionNotificationsState()],
|
||||||
canActivate: [authenticatedGuard, endUserAgreementCurrentUserGuard],
|
canActivate: [authenticatedGuard, endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
@@ -144,6 +147,7 @@ export const APP_ROUTES: Route[] = [
|
|||||||
path: 'search',
|
path: 'search',
|
||||||
loadChildren: () => import('./search-page/search-page-routes')
|
loadChildren: () => import('./search-page/search-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
|
data: { enableRSS: true },
|
||||||
canActivate: [endUserAgreementCurrentUserGuard],
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -156,6 +160,7 @@ export const APP_ROUTES: Route[] = [
|
|||||||
path: ADMIN_MODULE_PATH,
|
path: ADMIN_MODULE_PATH,
|
||||||
loadChildren: () => import('./admin/admin-routes')
|
loadChildren: () => import('./admin/admin-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
|
data: { enableRSS: true },
|
||||||
canActivate: [siteAdministratorGuard, endUserAgreementCurrentUserGuard],
|
canActivate: [siteAdministratorGuard, endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -200,6 +205,7 @@ export const APP_ROUTES: Route[] = [
|
|||||||
providers: [provideSubmissionState()],
|
providers: [provideSubmissionState()],
|
||||||
loadChildren: () => import('./workflowitems-edit-page/workflowitems-edit-page-routes')
|
loadChildren: () => import('./workflowitems-edit-page/workflowitems-edit-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
|
data: { enableRSS: true },
|
||||||
canActivate: [endUserAgreementCurrentUserGuard],
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -19,7 +19,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
|||||||
import { ResourcePoliciesComponent } from '../../shared/resource-policies/resource-policies.component';
|
import { ResourcePoliciesComponent } from '../../shared/resource-policies/resource-policies.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-collection-authorizations',
|
selector: 'ds-bitstream-authorizations',
|
||||||
templateUrl: './bitstream-authorizations.component.html',
|
templateUrl: './bitstream-authorizations.component.html',
|
||||||
imports: [
|
imports: [
|
||||||
ResourcePoliciesComponent,
|
ResourcePoliciesComponent,
|
||||||
@@ -30,7 +30,7 @@ import { ResourcePoliciesComponent } from '../../shared/resource-policies/resour
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Component that handles the Collection Authorizations
|
* Component that handles the Bitstream Authorizations
|
||||||
*/
|
*/
|
||||||
export class BitstreamAuthorizationsComponent<TDomain extends DSpaceObject> implements OnInit {
|
export class BitstreamAuthorizationsComponent<TDomain extends DSpaceObject> implements OnInit {
|
||||||
|
|
||||||
|
@@ -18,7 +18,10 @@ import {
|
|||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import {
|
||||||
|
map,
|
||||||
|
take,
|
||||||
|
} from 'rxjs/operators';
|
||||||
import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component';
|
import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -53,7 +56,6 @@ import { VarDirective } from '../../shared/utils/var.directive';
|
|||||||
import {
|
import {
|
||||||
BrowseByMetadataComponent,
|
BrowseByMetadataComponent,
|
||||||
browseParamsToOptions,
|
browseParamsToOptions,
|
||||||
getBrowseSearchOptions,
|
|
||||||
} from '../browse-by-metadata/browse-by-metadata.component';
|
} from '../browse-by-metadata/browse-by-metadata.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -104,15 +106,18 @@ export class BrowseByDateComponent extends BrowseByMetadataComponent implements
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
||||||
this.startsWithType = StartsWithType.date;
|
this.startsWithType = StartsWithType.date;
|
||||||
// include the thumbnail configuration in browse search options
|
|
||||||
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails));
|
|
||||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||||
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest([this.route.params, this.route.queryParams, this.scope$, this.route.data,
|
observableCombineLatest(
|
||||||
this.currentPagination$, this.currentSort$]).pipe(
|
[ this.route.params.pipe(take(1)),
|
||||||
map(([routeParams, queryParams, scope, data, currentPage, currentSort]) => {
|
this.route.queryParams,
|
||||||
return [Object.assign({}, routeParams, queryParams, data), scope, currentPage, currentSort];
|
this.scope$,
|
||||||
|
this.currentPagination$,
|
||||||
|
this.currentSort$,
|
||||||
|
]).pipe(
|
||||||
|
map(([routeParams, queryParams, scope, currentPage, currentSort]) => {
|
||||||
|
return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort];
|
||||||
}),
|
}),
|
||||||
).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
|
).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
|
||||||
const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys;
|
const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys;
|
||||||
|
@@ -23,7 +23,10 @@ import {
|
|||||||
of as observableOf,
|
of as observableOf,
|
||||||
Subscription,
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import {
|
||||||
|
map,
|
||||||
|
take,
|
||||||
|
} from 'rxjs/operators';
|
||||||
import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component';
|
import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -216,7 +219,13 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||||
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest([this.route.params, this.route.queryParams, this.scope$, this.currentPagination$, this.currentSort$]).pipe(
|
observableCombineLatest(
|
||||||
|
[ this.route.params.pipe(take(1)),
|
||||||
|
this.route.queryParams,
|
||||||
|
this.scope$,
|
||||||
|
this.currentPagination$,
|
||||||
|
this.currentSort$,
|
||||||
|
]).pipe(
|
||||||
map(([routeParams, queryParams, scope, currentPage, currentSort]) => {
|
map(([routeParams, queryParams, scope, currentPage, currentSort]) => {
|
||||||
return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort];
|
return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort];
|
||||||
}),
|
}),
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { Route } from '@angular/router';
|
import { Route } from '@angular/router';
|
||||||
|
|
||||||
import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
|
||||||
import { browseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
|
import { browseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
|
||||||
import { browseByGuard } from './browse-by-guard';
|
import { browseByGuard } from './browse-by-guard';
|
||||||
import { browseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
|
import { browseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
|
||||||
@@ -11,7 +10,6 @@ export const ROUTES: Route[] = [
|
|||||||
path: '',
|
path: '',
|
||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: browseByDSOBreadcrumbResolver,
|
breadcrumb: browseByDSOBreadcrumbResolver,
|
||||||
menu: dsoEditMenuResolver,
|
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
@@ -23,7 +23,7 @@ import { BrowseBySwitcherComponent } from '../browse-by-switcher/browse-by-switc
|
|||||||
})
|
})
|
||||||
export class BrowseByPageComponent implements OnInit {
|
export class BrowseByPageComponent implements OnInit {
|
||||||
|
|
||||||
browseByType$: Observable<BrowseByDataType>;
|
browseByType$: Observable<{type: BrowseByDataType }>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
@@ -35,7 +35,7 @@ export class BrowseByPageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.browseByType$ = this.route.data.pipe(
|
this.browseByType$ = this.route.data.pipe(
|
||||||
map((data: { browseDefinition: BrowseDefinition }) => data.browseDefinition.getRenderType()),
|
map((data: { browseDefinition: BrowseDefinition }) => ({ type: data.browseDefinition.getRenderType() })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -85,7 +85,7 @@ describe('BrowseBySwitcherComponent', () => {
|
|||||||
types.forEach((type: NonHierarchicalBrowseDefinition) => {
|
types.forEach((type: NonHierarchicalBrowseDefinition) => {
|
||||||
describe(`when switching to a browse-by page for "${type.id}"`, () => {
|
describe(`when switching to a browse-by page for "${type.id}"`, () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
comp.browseByType = type.dataType;
|
comp.browseByType = type as any;
|
||||||
comp.ngOnChanges({
|
comp.ngOnChanges({
|
||||||
browseByType: new SimpleChange(undefined, type.dataType, true),
|
browseByType: new SimpleChange(undefined, type.dataType, true),
|
||||||
});
|
});
|
||||||
|
@@ -24,7 +24,7 @@ export class BrowseBySwitcherComponent extends AbstractComponentLoaderComponent<
|
|||||||
|
|
||||||
@Input() context: Context;
|
@Input() context: Context;
|
||||||
|
|
||||||
@Input() browseByType: BrowseByDataType;
|
@Input() browseByType: { type: BrowseByDataType };
|
||||||
|
|
||||||
@Input() displayTitle: boolean;
|
@Input() displayTitle: boolean;
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ export class BrowseBySwitcherComponent extends AbstractComponentLoaderComponent<
|
|||||||
];
|
];
|
||||||
|
|
||||||
public getComponent(): GenericConstructor<Component> {
|
public getComponent(): GenericConstructor<Component> {
|
||||||
return getComponentByBrowseByType(this.browseByType, this.context, this.themeService.getThemeName());
|
return getComponentByBrowseByType(this.browseByType.type, this.context, this.themeService.getThemeName());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,10 @@ import {
|
|||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import {
|
||||||
|
map,
|
||||||
|
take,
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SortDirection,
|
SortDirection,
|
||||||
@@ -28,7 +31,6 @@ import { VarDirective } from '../../shared/utils/var.directive';
|
|||||||
import {
|
import {
|
||||||
BrowseByMetadataComponent,
|
BrowseByMetadataComponent,
|
||||||
browseParamsToOptions,
|
browseParamsToOptions,
|
||||||
getBrowseSearchOptions,
|
|
||||||
} from '../browse-by-metadata/browse-by-metadata.component';
|
} from '../browse-by-metadata/browse-by-metadata.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -58,12 +60,16 @@ export class BrowseByTitleComponent extends BrowseByMetadataComponent implements
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
// include the thumbnail configuration in browse search options
|
|
||||||
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails));
|
|
||||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||||
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest([this.route.params, this.route.queryParams, this.scope$, this.currentPagination$, this.currentSort$]).pipe(
|
observableCombineLatest(
|
||||||
|
[ this.route.params.pipe(take(1)),
|
||||||
|
this.route.queryParams,
|
||||||
|
this.scope$,
|
||||||
|
this.currentPagination$,
|
||||||
|
this.currentSort$,
|
||||||
|
]).pipe(
|
||||||
map(([routeParams, queryParams, scope, currentPage, currentSort]) => {
|
map(([routeParams, queryParams, scope, currentPage, currentSort]) => {
|
||||||
return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort];
|
return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort];
|
||||||
}),
|
}),
|
||||||
|
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
<li [ngbNavItem]="'mapTab'" role="presentation" data-test="mapTab">
|
<li [ngbNavItem]="'mapTab' | dsBrowserOnly" role="presentation" data-test="mapTab">
|
||||||
<a ngbNavLink>{{'collection.edit.item-mapper.tabs.map' | translate}}</a>
|
<a ngbNavLink>{{'collection.edit.item-mapper.tabs.map' | translate}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
|
@@ -62,6 +62,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
|||||||
import { ItemSelectComponent } from '../../shared/object-select/item-select/item-select.component';
|
import { ItemSelectComponent } from '../../shared/object-select/item-select/item-select.component';
|
||||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||||
import { ThemedSearchFormComponent } from '../../shared/search-form/themed-search-form.component';
|
import { ThemedSearchFormComponent } from '../../shared/search-form/themed-search-form.component';
|
||||||
|
import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe';
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -86,6 +87,7 @@ import { followLink } from '../../shared/utils/follow-link-config.model';
|
|||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
ItemSelectComponent,
|
ItemSelectComponent,
|
||||||
NgIf,
|
NgIf,
|
||||||
|
BrowserOnlyPipe,
|
||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
|
@@ -54,7 +54,6 @@ export const ROUTES: Route[] = [
|
|||||||
resolve: {
|
resolve: {
|
||||||
dso: collectionPageResolver,
|
dso: collectionPageResolver,
|
||||||
breadcrumb: collectionBreadcrumbResolver,
|
breadcrumb: collectionBreadcrumbResolver,
|
||||||
menu: dsoEditMenuResolver,
|
|
||||||
},
|
},
|
||||||
runGuardsAndResolvers: 'always',
|
runGuardsAndResolvers: 'always',
|
||||||
children: [
|
children: [
|
||||||
@@ -83,6 +82,9 @@ export const ROUTES: Route[] = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: ThemedCollectionPageComponent,
|
component: ThemedCollectionPageComponent,
|
||||||
|
resolve: {
|
||||||
|
menu: dsoEditMenuResolver,
|
||||||
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<div class="container" *ngIf="(isLoading$ | async) === false">
|
<div class="container" *ngIf="(isLoading$ | async) === false">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 pb-4">
|
<div class="col-12 pb-4">
|
||||||
<h2 id="sub-header"
|
<h1 id="sub-header"
|
||||||
class="border-bottom pb-2">{{ 'collection.create.sub-head' | translate:{ parent: dsoNameService.getName((parentRD$| async)?.payload) } }}</h2>
|
class="border-bottom pb-2">{{ 'collection.create.sub-head' | translate:{ parent: dsoNameService.getName((parentRD$| async)?.payload) } }}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-collection-form (submitForm)="onSubmit($event)"
|
<ds-collection-form (submitForm)="onSubmit($event)"
|
||||||
|
@@ -51,7 +51,6 @@ export const ROUTES: Route[] = [
|
|||||||
resolve: {
|
resolve: {
|
||||||
dso: communityPageResolver,
|
dso: communityPageResolver,
|
||||||
breadcrumb: communityBreadcrumbResolver,
|
breadcrumb: communityBreadcrumbResolver,
|
||||||
menu: dsoEditMenuResolver,
|
|
||||||
},
|
},
|
||||||
runGuardsAndResolvers: 'always',
|
runGuardsAndResolvers: 'always',
|
||||||
children: [
|
children: [
|
||||||
@@ -70,6 +69,9 @@ export const ROUTES: Route[] = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: ThemedCommunityPageComponent,
|
component: ThemedCommunityPageComponent,
|
||||||
|
resolve: {
|
||||||
|
menu: dsoEditMenuResolver,
|
||||||
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 pb-4">
|
<div class="col-12 pb-4">
|
||||||
<ng-container *ngVar="(parentRD$ | async)?.payload as parent">
|
<ng-container *ngVar="(parentRD$ | async)?.payload as parent">
|
||||||
<h2 *ngIf="!parent" id="header" class="border-bottom p-2">{{ 'community.create.head' | translate }}</h2>
|
<h1 *ngIf="!parent" id="header" class="border-bottom p-2">{{ 'community.create.head' | translate }}</h1>
|
||||||
<h2 *ngIf="parent" id="sub-header"
|
<h1 *ngIf="parent" id="sub-header"
|
||||||
class="border-bottom pb-2">{{ 'community.create.sub-head' | translate:{ parent: dsoNameService.getName(parent) } }}</h2>
|
class="border-bottom pb-2">{{ 'community.create.sub-head' | translate:{ parent: dsoNameService.getName(parent) } }}</h1>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
42
src/app/core/cache/builders/link.service.spec.ts
vendored
42
src/app/core/cache/builders/link.service.spec.ts
vendored
@@ -31,10 +31,12 @@ class TestModel implements HALResource {
|
|||||||
self: HALLink;
|
self: HALLink;
|
||||||
predecessor: HALLink;
|
predecessor: HALLink;
|
||||||
successor: HALLink;
|
successor: HALLink;
|
||||||
|
standardLinkName: HALLink;
|
||||||
};
|
};
|
||||||
|
|
||||||
predecessor?: TestModel;
|
predecessor?: TestModel;
|
||||||
successor?: TestModel;
|
successor?: TestModel;
|
||||||
|
renamedProperty?: TestModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockDataServiceMap: any = new Map([
|
const mockDataServiceMap: any = new Map([
|
||||||
@@ -66,6 +68,24 @@ describe('LinkService', () => {
|
|||||||
testDataService = new TestDataService();
|
testDataService = new TestDataService();
|
||||||
spyOn(testDataService, 'findListByHref').and.callThrough();
|
spyOn(testDataService, 'findListByHref').and.callThrough();
|
||||||
spyOn(testDataService, 'findByHref').and.callThrough();
|
spyOn(testDataService, 'findByHref').and.callThrough();
|
||||||
|
|
||||||
|
const linksDefinitions = new Map();
|
||||||
|
linksDefinitions.set('predecessor', {
|
||||||
|
resourceType: TEST_MODEL,
|
||||||
|
linkName: 'predecessor',
|
||||||
|
propertyName: 'predecessor',
|
||||||
|
});
|
||||||
|
linksDefinitions.set('successor', {
|
||||||
|
resourceType: TEST_MODEL,
|
||||||
|
linkName: 'successor',
|
||||||
|
propertyName: 'successor',
|
||||||
|
});
|
||||||
|
linksDefinitions.set('standardLinkName', {
|
||||||
|
resourceType: TEST_MODEL,
|
||||||
|
linkName: 'standardLinkName',
|
||||||
|
propertyName: 'renamedProperty',
|
||||||
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
LinkService,
|
LinkService,
|
||||||
@@ -87,18 +107,7 @@ describe('LinkService', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: LINK_DEFINITION_MAP_FACTORY,
|
provide: LINK_DEFINITION_MAP_FACTORY,
|
||||||
useValue: jasmine.createSpy('getLinkDefinitions').and.returnValue([
|
useValue: jasmine.createSpy('getLinkDefinitions').and.returnValue(linksDefinitions),
|
||||||
{
|
|
||||||
resourceType: TEST_MODEL,
|
|
||||||
linkName: 'predecessor',
|
|
||||||
propertyName: 'predecessor',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
resourceType: TEST_MODEL,
|
|
||||||
linkName: 'successor',
|
|
||||||
propertyName: 'successor',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -117,6 +126,15 @@ describe('LinkService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe(`when the propertyName is different than linkName`, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
result = service.resolveLink(testModel, followLink('standardLinkName', {}));
|
||||||
|
});
|
||||||
|
it('link should be assign to custom property', () => {
|
||||||
|
expect(result.renamedProperty).toBeDefined();
|
||||||
|
expect(result.standardLinkName).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
describe(`when the linkdefinition concerns a list`, () => {
|
describe(`when the linkdefinition concerns a list`, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({
|
((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({
|
||||||
|
11
src/app/core/cache/builders/link.service.ts
vendored
11
src/app/core/cache/builders/link.service.ts
vendored
@@ -112,7 +112,16 @@ export class LinkService {
|
|||||||
* @param linkToFollow the {@link FollowLinkConfig} to resolve
|
* @param linkToFollow the {@link FollowLinkConfig} to resolve
|
||||||
*/
|
*/
|
||||||
public resolveLink<T extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): T {
|
public resolveLink<T extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): T {
|
||||||
model[linkToFollow.name] = this.resolveLinkWithoutAttaching(model, linkToFollow);
|
const linkDefinitions = this.getLinkDefinitions(model.constructor as GenericConstructor<T>);
|
||||||
|
const linkDef = linkDefinitions.get(linkToFollow.name);
|
||||||
|
|
||||||
|
if (isNotEmpty(linkDef)) {
|
||||||
|
// If link exist in definition we can resolve it and use a real property name
|
||||||
|
model[linkDef.propertyName] = this.resolveLinkWithoutAttaching(model, linkToFollow);
|
||||||
|
} else {
|
||||||
|
// For some links we don't have a definition, so we use the link name as property name
|
||||||
|
model[linkToFollow.name] = this.resolveLinkWithoutAttaching(model, linkToFollow);
|
||||||
|
}
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,20 +4,16 @@ import {
|
|||||||
inheritSerialization,
|
inheritSerialization,
|
||||||
} from 'cerialize';
|
} from 'cerialize';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SectionScope,
|
||||||
|
SectionVisibility,
|
||||||
|
} from '../../../submission/objects/section-visibility.model';
|
||||||
import { SectionsType } from '../../../submission/sections/sections-type';
|
import { SectionsType } from '../../../submission/sections/sections-type';
|
||||||
import { typedObject } from '../../cache/builders/build-decorators';
|
import { typedObject } from '../../cache/builders/build-decorators';
|
||||||
import { HALLink } from '../../shared/hal-link.model';
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
import { ConfigObject } from './config.model';
|
import { ConfigObject } from './config.model';
|
||||||
import { SUBMISSION_SECTION_TYPE } from './config-type';
|
import { SUBMISSION_SECTION_TYPE } from './config-type';
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface that define section visibility and its properties.
|
|
||||||
*/
|
|
||||||
export interface SubmissionSectionVisibility {
|
|
||||||
main: any;
|
|
||||||
other: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
@typedObject
|
@typedObject
|
||||||
@inheritSerialization(ConfigObject)
|
@inheritSerialization(ConfigObject)
|
||||||
export class SubmissionSectionModel extends ConfigObject {
|
export class SubmissionSectionModel extends ConfigObject {
|
||||||
@@ -35,6 +31,12 @@ export class SubmissionSectionModel extends ConfigObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
mandatory: boolean;
|
mandatory: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The submission scope for this section
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
scope: SectionScope;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A string representing the kind of section object
|
* A string representing the kind of section object
|
||||||
*/
|
*/
|
||||||
@@ -42,10 +44,10 @@ export class SubmissionSectionModel extends ConfigObject {
|
|||||||
sectionType: SectionsType;
|
sectionType: SectionsType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [SubmissionSectionVisibility] object for this section
|
* The [SectionVisibility] object for this section
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
visibility: SubmissionSectionVisibility;
|
visibility: SectionVisibility;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link HALLink}s for this SubmissionSectionModel
|
* The {@link HALLink}s for this SubmissionSectionModel
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
*
|
*
|
||||||
* http://www.dspace.org/license/
|
* http://www.dspace.org/license/
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line max-classes-per-file
|
||||||
import {
|
import {
|
||||||
fakeAsync,
|
fakeAsync,
|
||||||
tick,
|
tick,
|
||||||
@@ -26,12 +27,20 @@ import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-ser
|
|||||||
import { ObjectCacheServiceStub } from '../../../shared/testing/object-cache-service.stub';
|
import { ObjectCacheServiceStub } from '../../../shared/testing/object-cache-service.stub';
|
||||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import {
|
||||||
|
link,
|
||||||
|
typedObject,
|
||||||
|
} from '../../cache/builders/build-decorators';
|
||||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheEntry } from '../../cache/object-cache.reducer';
|
import { ObjectCacheEntry } from '../../cache/object-cache.reducer';
|
||||||
import { ObjectCacheService } from '../../cache/object-cache.service';
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { BITSTREAM } from '../../shared/bitstream.resource-type';
|
||||||
|
import { COLLECTION } from '../../shared/collection.resource-type';
|
||||||
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
import { HALLink } from '../../shared/hal-link.model';
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
import { FindListOptions } from '../find-list-options.model';
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { PaginatedList } from '../paginated-list.model';
|
||||||
import { RemoteData } from '../remote-data';
|
import { RemoteData } from '../remote-data';
|
||||||
import { RequestService } from '../request.service';
|
import { RequestService } from '../request.service';
|
||||||
import { RequestEntryState } from '../request-entry-state.model';
|
import { RequestEntryState } from '../request-entry-state.model';
|
||||||
@@ -56,6 +65,25 @@ class TestService extends BaseDataService<any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@typedObject
|
||||||
|
class BaseData {
|
||||||
|
static type = new ResourceType('test');
|
||||||
|
|
||||||
|
foo: string;
|
||||||
|
|
||||||
|
_links: {
|
||||||
|
followLink1: HALLink;
|
||||||
|
followLink2: HALLink[];
|
||||||
|
self: HALLink;
|
||||||
|
};
|
||||||
|
|
||||||
|
@link(COLLECTION)
|
||||||
|
followLink1: Observable<any>;
|
||||||
|
|
||||||
|
@link(BITSTREAM, true, 'followLink2')
|
||||||
|
followLink2CustomVariableName: Observable<PaginatedList<any>>;
|
||||||
|
}
|
||||||
|
|
||||||
describe('BaseDataService', () => {
|
describe('BaseDataService', () => {
|
||||||
let service: TestService;
|
let service: TestService;
|
||||||
let requestService;
|
let requestService;
|
||||||
@@ -65,8 +93,9 @@ describe('BaseDataService', () => {
|
|||||||
let selfLink;
|
let selfLink;
|
||||||
let linksToFollow;
|
let linksToFollow;
|
||||||
let testScheduler;
|
let testScheduler;
|
||||||
let remoteDataMocks: { [responseType: string]: RemoteData<any> };
|
let remoteDataTimestamp: number;
|
||||||
let remoteDataPageMocks: { [responseType: string]: RemoteData<any> };
|
let remoteDataMocks: { [responseType: string]: RemoteData<BaseData> };
|
||||||
|
let remoteDataPageMocks: { [responseType: string]: RemoteData<PaginatedList<BaseData>> };
|
||||||
|
|
||||||
function initTestService(): TestService {
|
function initTestService(): TestService {
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
@@ -85,12 +114,14 @@ describe('BaseDataService', () => {
|
|||||||
expect(actual).toEqual(expected);
|
expect(actual).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeStamp = new Date().getTime();
|
// The response's lastUpdated equals the time of 60 seconds after the test started, ensuring they are not perceived
|
||||||
|
// as cached values.
|
||||||
|
remoteDataTimestamp = new Date().getTime() + 60 * 1000;
|
||||||
const msToLive = 15 * 60 * 1000;
|
const msToLive = 15 * 60 * 1000;
|
||||||
const payload = {
|
const payload: BaseData = Object.assign(new BaseData(), {
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
followLink1: {},
|
followLink1: observableOf({}),
|
||||||
followLink2: {},
|
followLink2CustomVariableName: observableOf(createPaginatedList()),
|
||||||
_links: {
|
_links: {
|
||||||
self: Object.assign(new HALLink(), {
|
self: Object.assign(new HALLink(), {
|
||||||
href: 'self-test-link',
|
href: 'self-test-link',
|
||||||
@@ -107,27 +138,27 @@ describe('BaseDataService', () => {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
const statusCodeSuccess = 200;
|
const statusCodeSuccess = 200;
|
||||||
const statusCodeError = 404;
|
const statusCodeError = 404;
|
||||||
const errorMessage = 'not found';
|
const errorMessage = 'not found';
|
||||||
remoteDataMocks = {
|
remoteDataMocks = {
|
||||||
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
RequestPending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
||||||
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
ResponsePending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
||||||
ResponsePendingStale: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined),
|
ResponsePendingStale: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined),
|
||||||
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess),
|
Success: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess),
|
||||||
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess),
|
SuccessStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess),
|
||||||
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
Error: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
||||||
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
ErrorStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
||||||
};
|
};
|
||||||
remoteDataPageMocks = {
|
remoteDataPageMocks = {
|
||||||
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
RequestPending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
||||||
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
ResponsePending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
||||||
ResponsePendingStale: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined),
|
ResponsePendingStale: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined),
|
||||||
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, createPaginatedList([payload]), statusCodeSuccess),
|
Success: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Success, undefined, createPaginatedList([payload]), statusCodeSuccess),
|
||||||
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, createPaginatedList([payload]), statusCodeSuccess),
|
SuccessStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.SuccessStale, undefined, createPaginatedList([payload]), statusCodeSuccess),
|
||||||
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
Error: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
||||||
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
ErrorStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
||||||
};
|
};
|
||||||
|
|
||||||
return new TestService(
|
return new TestService(
|
||||||
@@ -361,11 +392,15 @@ describe('BaseDataService', () => {
|
|||||||
spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source);
|
spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not emit a cached completed RemoteData', () => {
|
||||||
it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
// Old cached value from 1 minute before the test started
|
||||||
|
const oldCachedSucceededData: RemoteData<any> = Object.assign({}, remoteDataPageMocks.Success, {
|
||||||
|
timeCompleted: remoteDataTimestamp - 2 * 60 * 1000,
|
||||||
|
lastUpdated: remoteDataTimestamp - 2 * 60 * 1000,
|
||||||
|
} as RemoteData<any>);
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', {
|
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', {
|
||||||
a: remoteDataMocks.Success,
|
a: oldCachedSucceededData,
|
||||||
b: remoteDataMocks.RequestPending,
|
b: remoteDataMocks.RequestPending,
|
||||||
c: remoteDataMocks.ResponsePending,
|
c: remoteDataMocks.ResponsePending,
|
||||||
d: remoteDataMocks.Success,
|
d: remoteDataMocks.Success,
|
||||||
@@ -383,6 +418,22 @@ describe('BaseDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should emit the first completed RemoteData since the request was made', () => {
|
||||||
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
|
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b', {
|
||||||
|
a: remoteDataMocks.Success,
|
||||||
|
b: remoteDataMocks.SuccessStale,
|
||||||
|
}));
|
||||||
|
const expected = 'a-b';
|
||||||
|
const values = {
|
||||||
|
a: remoteDataMocks.Success,
|
||||||
|
b: remoteDataMocks.SuccessStale,
|
||||||
|
};
|
||||||
|
|
||||||
|
expectObservable(service.findByHref(selfLink, false, true, ...linksToFollow)).toBe(expected, values);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e-f-g', {
|
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e-f-g', {
|
||||||
@@ -411,17 +462,12 @@ describe('BaseDataService', () => {
|
|||||||
it('should link all the followLinks of a cached object by calling addDependency', () => {
|
it('should link all the followLinks of a cached object by calling addDependency', () => {
|
||||||
spyOn(objectCache, 'addDependency').and.callThrough();
|
spyOn(objectCache, 'addDependency').and.callThrough();
|
||||||
testScheduler.run(({ cold, expectObservable, flush }) => {
|
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||||
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d', {
|
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a', {
|
||||||
a: remoteDataMocks.Success,
|
a: remoteDataMocks.Success,
|
||||||
b: remoteDataMocks.RequestPending,
|
|
||||||
c: remoteDataMocks.ResponsePending,
|
|
||||||
d: remoteDataMocks.Success,
|
|
||||||
}));
|
}));
|
||||||
const expected = '--b-c-d';
|
const expected = 'a';
|
||||||
const values = {
|
const values = {
|
||||||
b: remoteDataMocks.RequestPending,
|
a: remoteDataMocks.Success,
|
||||||
c: remoteDataMocks.ResponsePending,
|
|
||||||
d: remoteDataMocks.Success,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expectObservable(service.findByHref(selfLink, false, false, ...linksToFollow)).toBe(expected, values);
|
expectObservable(service.findByHref(selfLink, false, false, ...linksToFollow)).toBe(expected, values);
|
||||||
@@ -570,11 +616,15 @@ describe('BaseDataService', () => {
|
|||||||
spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source);
|
spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not emit a cached completed RemoteData', () => {
|
||||||
it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
|
// Old cached value from 1 minute before the test started
|
||||||
|
const oldCachedSucceededData: RemoteData<any> = Object.assign({}, remoteDataPageMocks.Success, {
|
||||||
|
timeCompleted: remoteDataTimestamp - 2 * 60 * 1000,
|
||||||
|
lastUpdated: remoteDataTimestamp - 2 * 60 * 1000,
|
||||||
|
} as RemoteData<any>);
|
||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
|
||||||
a: remoteDataPageMocks.Success,
|
a: oldCachedSucceededData,
|
||||||
b: remoteDataPageMocks.RequestPending,
|
b: remoteDataPageMocks.RequestPending,
|
||||||
c: remoteDataPageMocks.ResponsePending,
|
c: remoteDataPageMocks.ResponsePending,
|
||||||
d: remoteDataPageMocks.Success,
|
d: remoteDataPageMocks.Success,
|
||||||
@@ -592,6 +642,22 @@ describe('BaseDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should emit the first completed RemoteData since the request was made', () => {
|
||||||
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b', {
|
||||||
|
a: remoteDataPageMocks.Success,
|
||||||
|
b: remoteDataPageMocks.SuccessStale,
|
||||||
|
}));
|
||||||
|
const expected = 'a-b';
|
||||||
|
const values = {
|
||||||
|
a: remoteDataPageMocks.Success,
|
||||||
|
b: remoteDataPageMocks.SuccessStale,
|
||||||
|
};
|
||||||
|
|
||||||
|
expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', {
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', {
|
||||||
|
@@ -28,11 +28,16 @@ import {
|
|||||||
isNotEmptyOperator,
|
isNotEmptyOperator,
|
||||||
} from '../../../shared/empty.util';
|
} from '../../../shared/empty.util';
|
||||||
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import {
|
||||||
|
getLinkDefinition,
|
||||||
|
LinkDefinition,
|
||||||
|
} from '../../cache/builders/build-decorators';
|
||||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
import { CacheableObject } from '../../cache/cacheable-object.model';
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
import { RequestParam } from '../../cache/models/request-param.model';
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
import { ObjectCacheEntry } from '../../cache/object-cache.reducer';
|
import { ObjectCacheEntry } from '../../cache/object-cache.reducer';
|
||||||
import { ObjectCacheService } from '../../cache/object-cache.service';
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { GenericConstructor } from '../../shared/generic-constructor';
|
||||||
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
import { HALLink } from '../../shared/hal-link.model';
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
||||||
@@ -285,6 +290,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow)),
|
map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const startTime: number = new Date().getTime();
|
||||||
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
||||||
|
|
||||||
const response$: Observable<RemoteData<T>> = this.rdbService.buildSingle<T>(requestHref$, ...linksToFollow).pipe(
|
const response$: Observable<RemoteData<T>> = this.rdbService.buildSingle<T>(requestHref$, ...linksToFollow).pipe(
|
||||||
@@ -292,7 +298,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
// call it isn't immediately returned, but we wait until the remote data for the new request
|
// call it isn't immediately returned, but we wait until the remote data for the new request
|
||||||
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
||||||
// cached completed object
|
// cached completed object
|
||||||
skipWhile((rd: RemoteData<T>) => rd.isStale || (!useCachedVersionIfAvailable && rd.hasCompleted)),
|
skipWhile((rd: RemoteData<T>) => rd.isStale || (!useCachedVersionIfAvailable && rd.lastUpdated < startTime)),
|
||||||
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||||
this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
|
this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
|
||||||
);
|
);
|
||||||
@@ -300,9 +306,10 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
// Ensure all followLinks from the cached object are automatically invalidated when invalidating the cached object
|
// Ensure all followLinks from the cached object are automatically invalidated when invalidating the cached object
|
||||||
tap((remoteDataObject: RemoteData<T>) => {
|
tap((remoteDataObject: RemoteData<T>) => {
|
||||||
if (hasValue(remoteDataObject?.payload?._links)) {
|
if (hasValue(remoteDataObject?.payload?._links)) {
|
||||||
for (const followLinkName of Object.keys(remoteDataObject.payload._links)) {
|
for (const followLinkName of Object.keys(remoteDataObject.payload._links) as (keyof typeof remoteDataObject.payload._links)[]) {
|
||||||
// only add the followLinks if they are embedded
|
// only add the followLinks if they are embedded, and we get only links from the linkMap with the correct name
|
||||||
if (hasValue(remoteDataObject.payload[followLinkName]) && followLinkName !== 'self') {
|
const linkDefinition: LinkDefinition<T> = getLinkDefinition(remoteDataObject.payload.constructor as GenericConstructor<T>, followLinkName);
|
||||||
|
if (linkDefinition?.propertyName && hasValue(remoteDataObject.payload[linkDefinition.propertyName]) && followLinkName !== 'self') {
|
||||||
// followLink can be either an individual HALLink or a HALLink[]
|
// followLink can be either an individual HALLink or a HALLink[]
|
||||||
const followLinksList: HALLink[] = [].concat(remoteDataObject.payload._links[followLinkName]);
|
const followLinksList: HALLink[] = [].concat(remoteDataObject.payload._links[followLinkName]);
|
||||||
for (const individualFollowLink of followLinksList) {
|
for (const individualFollowLink of followLinksList) {
|
||||||
@@ -338,6 +345,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
map((href: string) => this.buildHrefFromFindOptions(href, options, [], ...linksToFollow)),
|
map((href: string) => this.buildHrefFromFindOptions(href, options, [], ...linksToFollow)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const startTime: number = new Date().getTime();
|
||||||
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
||||||
|
|
||||||
const response$: Observable<RemoteData<PaginatedList<T>>> = this.rdbService.buildList<T>(requestHref$, ...linksToFollow).pipe(
|
const response$: Observable<RemoteData<PaginatedList<T>>> = this.rdbService.buildList<T>(requestHref$, ...linksToFollow).pipe(
|
||||||
@@ -345,7 +353,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
// call it isn't immediately returned, but we wait until the remote data for the new request
|
// call it isn't immediately returned, but we wait until the remote data for the new request
|
||||||
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
||||||
// cached completed object
|
// cached completed object
|
||||||
skipWhile((rd: RemoteData<PaginatedList<T>>) => rd.isStale || (!useCachedVersionIfAvailable && rd.hasCompleted)),
|
skipWhile((rd: RemoteData<PaginatedList<T>>) => rd.isStale || (!useCachedVersionIfAvailable && rd.lastUpdated < startTime)),
|
||||||
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||||
this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
|
this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
|
||||||
);
|
);
|
||||||
@@ -355,9 +363,10 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
if (hasValue(remoteDataObject?.payload?.page)) {
|
if (hasValue(remoteDataObject?.payload?.page)) {
|
||||||
for (const object of remoteDataObject.payload.page) {
|
for (const object of remoteDataObject.payload.page) {
|
||||||
if (hasValue(object?._links)) {
|
if (hasValue(object?._links)) {
|
||||||
for (const followLinkName of Object.keys(object._links)) {
|
for (const followLinkName of Object.keys(object._links) as (keyof typeof object._links)[]) {
|
||||||
// only add the followLinks if they are embedded
|
// only add the followLinks if they are embedded, and we get only links from the linkMap with the correct name
|
||||||
if (hasValue(object[followLinkName]) && followLinkName !== 'self') {
|
const linkDefinition: LinkDefinition<PaginatedList<T>> = getLinkDefinition(object.constructor as GenericConstructor<PaginatedList<T>>, followLinkName);
|
||||||
|
if (linkDefinition?.propertyName && followLinkName !== 'self' && hasValue(object[linkDefinition.propertyName])) {
|
||||||
// followLink can be either an individual HALLink or a HALLink[]
|
// followLink can be either an individual HALLink or a HALLink[]
|
||||||
const followLinksList: HALLink[] = [].concat(object._links[followLinkName]);
|
const followLinksList: HALLink[] = [].concat(object._links[followLinkName]);
|
||||||
for (const individualFollowLink of followLinksList) {
|
for (const individualFollowLink of followLinksList) {
|
||||||
|
@@ -0,0 +1,28 @@
|
|||||||
|
export class ObjectUpdatesServiceStub {
|
||||||
|
|
||||||
|
initialize = jasmine.createSpy('initialize');
|
||||||
|
saveFieldUpdate = jasmine.createSpy('saveFieldUpdate');
|
||||||
|
getObjectEntry = jasmine.createSpy('getObjectEntry');
|
||||||
|
getFieldState = jasmine.createSpy('getFieldState');
|
||||||
|
getFieldUpdates = jasmine.createSpy('getFieldUpdates');
|
||||||
|
getFieldUpdatesExclusive = jasmine.createSpy('getFieldUpdatesExclusive');
|
||||||
|
isValid = jasmine.createSpy('isValid');
|
||||||
|
isValidPage = jasmine.createSpy('isValidPage');
|
||||||
|
saveAddFieldUpdate = jasmine.createSpy('saveAddFieldUpdate');
|
||||||
|
saveRemoveFieldUpdate = jasmine.createSpy('saveRemoveFieldUpdate');
|
||||||
|
saveChangeFieldUpdate = jasmine.createSpy('saveChangeFieldUpdate');
|
||||||
|
isSelectedVirtualMetadata = jasmine.createSpy('isSelectedVirtualMetadata');
|
||||||
|
setSelectedVirtualMetadata = jasmine.createSpy('setSelectedVirtualMetadata');
|
||||||
|
setEditableFieldUpdate = jasmine.createSpy('setEditableFieldUpdate');
|
||||||
|
setValidFieldUpdate = jasmine.createSpy('setValidFieldUpdate');
|
||||||
|
discardFieldUpdates = jasmine.createSpy('discardFieldUpdates');
|
||||||
|
discardAllFieldUpdates = jasmine.createSpy('discardAllFieldUpdates');
|
||||||
|
reinstateFieldUpdates = jasmine.createSpy('reinstateFieldUpdates');
|
||||||
|
removeSingleFieldUpdate = jasmine.createSpy('removeSingleFieldUpdate');
|
||||||
|
getUpdateFields = jasmine.createSpy('getUpdateFields');
|
||||||
|
hasUpdates = jasmine.createSpy('hasUpdates');
|
||||||
|
isReinstatable = jasmine.createSpy('isReinstatable');
|
||||||
|
getLastModified = jasmine.createSpy('getLastModified');
|
||||||
|
createPatch = jasmine.createSpy('getPatch');
|
||||||
|
|
||||||
|
}
|
@@ -30,7 +30,7 @@ describe('GoogleRecaptchaService', () => {
|
|||||||
findByPropertyName: createSuccessfulRemoteDataObject$({ values: ['googleRecaptchaToken'] }),
|
findByPropertyName: createSuccessfulRemoteDataObject$({ values: ['googleRecaptchaToken'] }),
|
||||||
});
|
});
|
||||||
cookieService = jasmine.createSpyObj('cookieService', {
|
cookieService = jasmine.createSpyObj('cookieService', {
|
||||||
get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22klaro%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}',
|
get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22orejime%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}',
|
||||||
set: () => {
|
set: () => {
|
||||||
/* empty */
|
/* empty */
|
||||||
},
|
},
|
||||||
|
@@ -105,7 +105,7 @@ export class GoogleRecaptchaService {
|
|||||||
tap(([recaptchaVersionRD, recaptchaModeRD, recaptchaKeyRD]) => {
|
tap(([recaptchaVersionRD, recaptchaModeRD, recaptchaKeyRD]) => {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.cookieService.get('klaro-anonymous') && this.cookieService.get('klaro-anonymous')[CAPTCHA_NAME] &&
|
this.cookieService.get('orejime-anonymous') && this.cookieService.get('orejime-anonymous')[CAPTCHA_NAME] &&
|
||||||
recaptchaKeyRD.hasSucceeded && recaptchaVersionRD.hasSucceeded &&
|
recaptchaKeyRD.hasSucceeded && recaptchaVersionRD.hasSucceeded &&
|
||||||
isNotEmpty(recaptchaVersionRD.payload?.values) && isNotEmpty(recaptchaKeyRD.payload?.values)
|
isNotEmpty(recaptchaVersionRD.payload?.values) && isNotEmpty(recaptchaKeyRD.payload?.values)
|
||||||
) {
|
) {
|
||||||
|
@@ -27,7 +27,7 @@ export class MetadataService {
|
|||||||
* Returns undefined otherwise.
|
* Returns undefined otherwise.
|
||||||
*/
|
*/
|
||||||
public virtualValue(metadataValue: MetadataValue | undefined): string {
|
public virtualValue(metadataValue: MetadataValue | undefined): string {
|
||||||
if (this.isVirtual) {
|
if (this.isVirtual(metadataValue)) {
|
||||||
return metadataValue.authority.substring(metadataValue.authority.indexOf(VIRTUAL_METADATA_PREFIX) + VIRTUAL_METADATA_PREFIX.length);
|
return metadataValue.authority.substring(metadataValue.authority.indexOf(VIRTUAL_METADATA_PREFIX) + VIRTUAL_METADATA_PREFIX.length);
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@@ -16,6 +16,7 @@ import { AccessStatusObject } from '../shared/object-collection/shared/badges/ac
|
|||||||
import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model';
|
import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model';
|
||||||
import { Subscription } from '../shared/subscriptions/models/subscription.model';
|
import { Subscription } from '../shared/subscriptions/models/subscription.model';
|
||||||
import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config';
|
import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config';
|
||||||
|
import { SystemWideAlert } from '../system-wide-alert/system-wide-alert.model';
|
||||||
import { AuthStatus } from './auth/models/auth-status.model';
|
import { AuthStatus } from './auth/models/auth-status.model';
|
||||||
import { ShortLivedToken } from './auth/models/short-lived-token.model';
|
import { ShortLivedToken } from './auth/models/short-lived-token.model';
|
||||||
import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model';
|
import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model';
|
||||||
@@ -186,4 +187,5 @@ export const models =
|
|||||||
Itemfilter,
|
Itemfilter,
|
||||||
SubmissionCoarNotifyConfig,
|
SubmissionCoarNotifyConfig,
|
||||||
NotifyRequestsStatus,
|
NotifyRequestsStatus,
|
||||||
|
SystemWideAlert,
|
||||||
];
|
];
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
import { autoserialize } from 'cerialize';
|
import {
|
||||||
|
autoserialize,
|
||||||
|
autoserializeAs,
|
||||||
|
} from 'cerialize';
|
||||||
|
|
||||||
import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type';
|
import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type';
|
||||||
import { CacheableObject } from '../cache/cacheable-object.model';
|
import { CacheableObject } from '../cache/cacheable-object.model';
|
||||||
@@ -11,6 +14,9 @@ export abstract class BrowseDefinition extends CacheableObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
@autoserializeAs('metadata')
|
||||||
|
metadataKeys: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the render type of the BrowseDefinition model
|
* Get the render type of the BrowseDefinition model
|
||||||
*/
|
*/
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
autoserialize,
|
autoserialize,
|
||||||
autoserializeAs,
|
|
||||||
deserialize,
|
deserialize,
|
||||||
inheritSerialization,
|
inheritSerialization,
|
||||||
} from 'cerialize';
|
} from 'cerialize';
|
||||||
@@ -33,9 +32,6 @@ export class HierarchicalBrowseDefinition extends BrowseDefinition {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
vocabulary: string;
|
vocabulary: string;
|
||||||
|
|
||||||
@autoserializeAs('metadata')
|
|
||||||
metadataKeys: string[];
|
|
||||||
|
|
||||||
get self(): string {
|
get self(): string {
|
||||||
return this._links.self.href;
|
return this._links.self.href;
|
||||||
}
|
}
|
||||||
|
@@ -77,7 +77,7 @@ export class Metadata {
|
|||||||
/**
|
/**
|
||||||
* Gets the first matching MetadataValue object in the map(s), or `undefined`.
|
* Gets the first matching MetadataValue object in the map(s), or `undefined`.
|
||||||
*
|
*
|
||||||
* @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s).
|
* @param {MetadataMapInterface|MetadataMapInterface[]} mdMapOrMaps The source map(s).
|
||||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||||
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||||
* @returns {MetadataValue} the first matching value, or `undefined`.
|
* @returns {MetadataValue} the first matching value, or `undefined`.
|
||||||
@@ -98,7 +98,7 @@ export class Metadata {
|
|||||||
/**
|
/**
|
||||||
* Like [[Metadata.first]], but only returns a string value, or `undefined`.
|
* Like [[Metadata.first]], but only returns a string value, or `undefined`.
|
||||||
*
|
*
|
||||||
* @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s).
|
* @param {MetadataMapInterface|MetadataMapInterface[]} mdMapOrMaps The source map(s).
|
||||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||||
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||||
* @returns {string} the first matching string value, or `undefined`.
|
* @returns {string} the first matching string value, or `undefined`.
|
||||||
@@ -112,7 +112,7 @@ export class Metadata {
|
|||||||
/**
|
/**
|
||||||
* Checks for a matching metadata value in the given map(s).
|
* Checks for a matching metadata value in the given map(s).
|
||||||
*
|
*
|
||||||
* @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s).
|
* @param {MetadataMapInterface|MetadataMapInterface[]} mdMapOrMaps The source map(s).
|
||||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||||
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||||
* @returns {boolean} whether a match is found.
|
* @returns {boolean} whether a match is found.
|
||||||
|
@@ -21,9 +21,6 @@ export abstract class NonHierarchicalBrowseDefinition extends BrowseDefinition {
|
|||||||
@autoserializeAs('order')
|
@autoserializeAs('order')
|
||||||
defaultSortOrder: string;
|
defaultSortOrder: string;
|
||||||
|
|
||||||
@autoserializeAs('metadata')
|
|
||||||
metadataKeys: string[];
|
|
||||||
|
|
||||||
@autoserialize
|
@autoserialize
|
||||||
dataType: BrowseByDataType;
|
dataType: BrowseByDataType;
|
||||||
}
|
}
|
||||||
|
@@ -3,20 +3,26 @@
|
|||||||
[ngClass]="{ 'ds-warning': mdValue.reordered || mdValue.change === DsoEditMetadataChangeTypeEnum.UPDATE, 'ds-danger': mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE, 'ds-success': mdValue.change === DsoEditMetadataChangeTypeEnum.ADD, 'h-100': isOnlyValue }">
|
[ngClass]="{ 'ds-warning': mdValue.reordered || mdValue.change === DsoEditMetadataChangeTypeEnum.UPDATE, 'ds-danger': mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE, 'ds-success': mdValue.change === DsoEditMetadataChangeTypeEnum.ADD, 'h-100': isOnlyValue }">
|
||||||
<div class="flex-grow-1 ds-flex-cell ds-value-cell d-flex flex-column" *ngVar="(mdRepresentation$ | async) as mdRepresentation" role="cell">
|
<div class="flex-grow-1 ds-flex-cell ds-value-cell d-flex flex-column" *ngVar="(mdRepresentation$ | async) as mdRepresentation" role="cell">
|
||||||
<div class="dont-break-out preserve-line-breaks" *ngIf="!mdValue.editing && !mdRepresentation">{{ mdValue.newValue.value }}</div>
|
<div class="dont-break-out preserve-line-breaks" *ngIf="!mdValue.editing && !mdRepresentation">{{ mdValue.newValue.value }}</div>
|
||||||
<textarea class="form-control" rows="5" *ngIf="mdValue.editing && !mdRepresentation && (isAuthorityControlled() | async) !== true" [(ngModel)]="mdValue.newValue.value"
|
<textarea class="form-control" rows="5" *ngIf="mdValue.editing && !mdRepresentation && ((isAuthorityControlled() | async) !== true || (enabledFreeTextEditing && (isSuggesterVocabulary() | async) !== true))" [(ngModel)]="mdValue.newValue.value"
|
||||||
[attr.aria-label]="(dsoType + '.edit.metadata.edit.value') | translate"
|
[attr.aria-label]="(dsoType + '.edit.metadata.edit.value') | translate"
|
||||||
[dsDebounce]="300" (onDebounce)="confirm.emit(false)"></textarea>
|
[dsDebounce]="300" (onDebounce)="confirm.emit(false)"></textarea>
|
||||||
<ds-dynamic-scrollable-dropdown *ngIf="mdValue.editing && (isScrollableVocabulary() | async)"
|
<ds-dynamic-scrollable-dropdown *ngIf="mdValue.editing && (isScrollableVocabulary() | async) && !enabledFreeTextEditing"
|
||||||
[bindId]="mdField"
|
[bindId]="mdField"
|
||||||
[group]="group"
|
[group]="group"
|
||||||
[model]="getModel()"
|
[model]="getModel()"
|
||||||
(change)="onChangeAuthorityField($event)">
|
(change)="onChangeAuthorityField($event)">
|
||||||
</ds-dynamic-scrollable-dropdown>
|
</ds-dynamic-scrollable-dropdown>
|
||||||
<ds-dynamic-onebox *ngIf="mdValue.editing && ((isHierarchicalVocabulary() | async) || (isSuggesterVocabulary() | async))"
|
<ds-dynamic-onebox *ngIf="mdValue.editing && (((isHierarchicalVocabulary() | async) && !enabledFreeTextEditing) || (isSuggesterVocabulary() | async))"
|
||||||
[group]="group"
|
[group]="group"
|
||||||
[model]="getModel()"
|
[model]="getModel()"
|
||||||
(change)="onChangeAuthorityField($event)">
|
(change)="onChangeAuthorityField($event)">
|
||||||
</ds-dynamic-onebox>
|
</ds-dynamic-onebox>
|
||||||
|
<button class="btn btn-secondary mt-2" *ngIf="mdValue.editing && ((isScrollableVocabulary() | async) || (isHierarchicalVocabulary() | async))"
|
||||||
|
[title]="enabledFreeTextEditing ? dsoType + '.edit.metadata.edit.buttons.disable-free-text-editing' : dsoType + '.edit.metadata.edit.buttons.enable-free-text-editing' | translate"
|
||||||
|
(click)="toggleFreeTextEdition()">
|
||||||
|
<i class="fas fa-fw" [ngClass]="enabledFreeTextEditing ? 'fa-lock' : 'fa-unlock'"></i>
|
||||||
|
{{ (enabledFreeTextEditing ? dsoType + '.edit.metadata.edit.buttons.disable-free-text-editing' : dsoType + '.edit.metadata.edit.buttons.enable-free-text-editing') | translate }}
|
||||||
|
</button>
|
||||||
<div *ngIf="!isVirtual && !mdValue.editing && mdValue.newValue.authority && mdValue.newValue.confidence !== ConfidenceTypeEnum.CF_UNSET && mdValue.newValue.confidence !== ConfidenceTypeEnum.CF_NOVALUE">
|
<div *ngIf="!isVirtual && !mdValue.editing && mdValue.newValue.authority && mdValue.newValue.confidence !== ConfidenceTypeEnum.CF_UNSET && mdValue.newValue.confidence !== ConfidenceTypeEnum.CF_NOVALUE">
|
||||||
<span class="badge badge-light border" >
|
<span class="badge badge-light border" >
|
||||||
<i dsAuthorityConfidenceState
|
<i dsAuthorityConfidenceState
|
||||||
|
@@ -191,6 +191,12 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
|||||||
*/
|
*/
|
||||||
public editingAuthority = false;
|
public editingAuthority = false;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the free-text editing is enabled when scrollable dropdown or hierarchical vocabulary is used
|
||||||
|
*/
|
||||||
|
public enabledFreeTextEditing = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field group used by authority field
|
* Field group used by authority field
|
||||||
* @type {UntypedFormGroup}
|
* @type {UntypedFormGroup}
|
||||||
@@ -438,15 +444,23 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
|||||||
* Process the change of authority field value updating the authority key and confidence as necessary
|
* Process the change of authority field value updating the authority key and confidence as necessary
|
||||||
*/
|
*/
|
||||||
onChangeAuthorityField(event): void {
|
onChangeAuthorityField(event): void {
|
||||||
this.mdValue.newValue.value = event.value;
|
if (event) {
|
||||||
if (event.authority) {
|
this.mdValue.newValue.value = event.value;
|
||||||
this.mdValue.newValue.authority = event.authority;
|
if (event.authority) {
|
||||||
this.mdValue.newValue.confidence = ConfidenceType.CF_ACCEPTED;
|
this.mdValue.newValue.authority = event.authority;
|
||||||
|
this.mdValue.newValue.confidence = ConfidenceType.CF_ACCEPTED;
|
||||||
|
} else {
|
||||||
|
this.mdValue.newValue.authority = null;
|
||||||
|
this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET;
|
||||||
|
}
|
||||||
|
this.confirm.emit(false);
|
||||||
} else {
|
} else {
|
||||||
|
// The event is undefined when the user clears the selection in scrollable dropdown
|
||||||
|
this.mdValue.newValue.value = '';
|
||||||
this.mdValue.newValue.authority = null;
|
this.mdValue.newValue.authority = null;
|
||||||
this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET;
|
this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET;
|
||||||
|
this.confirm.emit(false);
|
||||||
}
|
}
|
||||||
this.confirm.emit(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -480,4 +494,17 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the free-text ediitng mode
|
||||||
|
*/
|
||||||
|
toggleFreeTextEdition() {
|
||||||
|
if (this.enabledFreeTextEditing) {
|
||||||
|
if (this.getModel().value !== this.mdValue.newValue.value) {
|
||||||
|
// Reload the model to adapt it to the new possible value modified during free text editing
|
||||||
|
this.initAuthorityProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.enabledFreeTextEditing = !this.enabledFreeTextEditing;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,13 +6,30 @@
|
|||||||
[formControl]="input"
|
[formControl]="input"
|
||||||
(focusin)="query$.next(mdField)"
|
(focusin)="query$.next(mdField)"
|
||||||
(dsClickOutside)="query$.next(null)"
|
(dsClickOutside)="query$.next(null)"
|
||||||
(click)="$event.stopPropagation();" />
|
(click)="$event.stopPropagation();"
|
||||||
|
(keyup)="this.selectedValueLoading = false"
|
||||||
|
/>
|
||||||
<div class="invalid-feedback show-feedback" *ngIf="showInvalid">{{ dsoType + '.edit.metadata.metadatafield.invalid' | translate }}</div>
|
<div class="invalid-feedback show-feedback" *ngIf="showInvalid">{{ dsoType + '.edit.metadata.metadatafield.invalid' | translate }}</div>
|
||||||
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (mdFieldOptions$ | async)?.length > 0}">
|
<div id="scrollable-metadata-field-selector" class="dropdown-menu scrollable-menu" [ngClass]="{'show': (mdFieldOptions$ | async)?.length > 0}">
|
||||||
<div class="dropdown-list">
|
<div class="dropdown-list">
|
||||||
<div *ngFor="let mdFieldOption of (mdFieldOptions$ | async)">
|
<div
|
||||||
<button class="d-block dropdown-item" (click)="select(mdFieldOption)">
|
infiniteScroll
|
||||||
<span [innerHTML]="mdFieldOption"></span>
|
[infiniteScrollDistance]="1"
|
||||||
|
[infiniteScrollThrottle]="0"
|
||||||
|
[infiniteScrollContainer]="'#scrollable-metadata-field-selector'"
|
||||||
|
[fromRoot]="true"
|
||||||
|
(scrolled)="onScrollDown()">
|
||||||
|
<ng-container *ngIf="mdFieldOptions$ | async">
|
||||||
|
<button *ngFor="let listEntry of (mdFieldOptions$ | async)"
|
||||||
|
class="d-block dropdown-item"
|
||||||
|
dsHoverClass="ds-hover"
|
||||||
|
(click)="select(listEntry)" #listEntryElement>
|
||||||
|
<span [innerHTML]="listEntry"></span>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
<button *ngIf="loading"
|
||||||
|
class="list-group-item list-group-item-action border-0 list-entry">
|
||||||
|
<ds-loading [showMessage]="false"></ds-loading>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -0,0 +1,5 @@
|
|||||||
|
.scrollable-menu {
|
||||||
|
height: auto;
|
||||||
|
max-height: var(--ds-dso-selector-list-max-height);
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
@@ -39,7 +39,8 @@ describe('MetadataFieldSelectorComponent', () => {
|
|||||||
metadataSchema = Object.assign(new MetadataSchema(), {
|
metadataSchema = Object.assign(new MetadataSchema(), {
|
||||||
id: 0,
|
id: 0,
|
||||||
prefix: 'dc',
|
prefix: 'dc',
|
||||||
namespace: 'http://dublincore.org/documents/dcmi-terms/',
|
namespace: 'https://schema.org/CreativeWork',
|
||||||
|
field: '.',
|
||||||
});
|
});
|
||||||
metadataFields = [
|
metadataFields = [
|
||||||
Object.assign(new MetadataField(), {
|
Object.assign(new MetadataField(), {
|
||||||
@@ -78,10 +79,10 @@ describe('MetadataFieldSelectorComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when a query is entered', () => {
|
describe('when a query is entered', () => {
|
||||||
const query = 'test query';
|
const query = 'dc.d';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
component.showInvalid = true;
|
component.showInvalid = false;
|
||||||
component.query$.next(query);
|
component.query$.next(query);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ describe('MetadataFieldSelectorComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should query the registry service for metadata fields and include the schema', () => {
|
it('should query the registry service for metadata fields and include the schema', () => {
|
||||||
expect(registryService.queryMetadataFields).toHaveBeenCalledWith(query, { elementsPerPage: 10, sort: new SortOptions('fieldName', SortDirection.ASC) }, true, false, followLink('schema'));
|
expect(registryService.queryMetadataFields).toHaveBeenCalledWith(query, { elementsPerPage: 20, sort: new SortOptions('fieldName', SortDirection.ASC), currentPage: 1 }, true, false, followLink('schema'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -24,16 +24,18 @@ import {
|
|||||||
TranslateModule,
|
TranslateModule,
|
||||||
TranslateService,
|
TranslateService,
|
||||||
} from '@ngx-translate/core';
|
} from '@ngx-translate/core';
|
||||||
|
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
of,
|
of,
|
||||||
Subscription,
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
debounceTime,
|
debounceTime,
|
||||||
distinctUntilChanged,
|
|
||||||
map,
|
map,
|
||||||
|
startWith,
|
||||||
switchMap,
|
switchMap,
|
||||||
take,
|
take,
|
||||||
tap,
|
tap,
|
||||||
@@ -50,6 +52,7 @@ import {
|
|||||||
metadataFieldsToString,
|
metadataFieldsToString,
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { ClickOutsideDirective } from '../../../shared/utils/click-outside.directive';
|
import { ClickOutsideDirective } from '../../../shared/utils/click-outside.directive';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
@@ -59,7 +62,7 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
|
|||||||
styleUrls: ['./metadata-field-selector.component.scss'],
|
styleUrls: ['./metadata-field-selector.component.scss'],
|
||||||
templateUrl: './metadata-field-selector.component.html',
|
templateUrl: './metadata-field-selector.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [FormsModule, NgClass, ReactiveFormsModule, ClickOutsideDirective, NgIf, NgFor, AsyncPipe, TranslateModule],
|
imports: [FormsModule, NgClass, ReactiveFormsModule, ClickOutsideDirective, NgIf, NgFor, AsyncPipe, TranslateModule, ThemedLoadingComponent, InfiniteScrollModule],
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Component displaying a searchable input for metadata-fields
|
* Component displaying a searchable input for metadata-fields
|
||||||
@@ -96,7 +99,7 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
|
|||||||
* List of available metadata field options to choose from, dependent on the current query the user entered
|
* List of available metadata field options to choose from, dependent on the current query the user entered
|
||||||
* Shows up in a dropdown below the input
|
* Shows up in a dropdown below the input
|
||||||
*/
|
*/
|
||||||
mdFieldOptions$: Observable<string[]>;
|
mdFieldOptions$: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FormControl for the input
|
* FormControl for the input
|
||||||
@@ -131,6 +134,30 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
|
|||||||
*/
|
*/
|
||||||
subs: Subscription[] = [];
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current page to load
|
||||||
|
* Dynamically goes up as the user scrolls down until it reaches the last page possible
|
||||||
|
*/
|
||||||
|
currentPage$ = new BehaviorSubject(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the list contains a next page to load
|
||||||
|
* This allows us to avoid next pages from trying to load when there are none
|
||||||
|
*/
|
||||||
|
hasNextPage = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not new results are currently loading
|
||||||
|
*/
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default page option for this feature
|
||||||
|
*/
|
||||||
|
pageOptions = { elementsPerPage: 20, sort: new SortOptions('fieldName', SortDirection.ASC) };
|
||||||
|
|
||||||
|
|
||||||
constructor(protected registryService: RegistryService,
|
constructor(protected registryService: RegistryService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected translate: TranslateService) {
|
protected translate: TranslateService) {
|
||||||
@@ -141,32 +168,33 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
|
|||||||
* Update the mdFieldOptions$ depending on the query$ fired by querying the server
|
* Update the mdFieldOptions$ depending on the query$ fired by querying the server
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.subs.push(this.input.valueChanges.pipe(
|
||||||
|
debounceTime(this.debounceTime),
|
||||||
|
startWith(''),
|
||||||
|
).subscribe((valueChange) => {
|
||||||
|
this.currentPage$.next(1);
|
||||||
|
if (!this.selectedValueLoading) {
|
||||||
|
this.query$.next(valueChange);
|
||||||
|
}
|
||||||
|
this.mdField = valueChange;
|
||||||
|
this.mdFieldChange.emit(this.mdField);
|
||||||
|
}));
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
this.input.valueChanges.pipe(
|
observableCombineLatest(
|
||||||
debounceTime(this.debounceTime),
|
this.query$,
|
||||||
).subscribe((valueChange) => {
|
this.currentPage$,
|
||||||
if (!this.selectedValueLoading) {
|
)
|
||||||
this.query$.next(valueChange);
|
.pipe(
|
||||||
}
|
switchMap(([query, page]: [string, number]) => {
|
||||||
this.selectedValueLoading = false;
|
this.loading = true;
|
||||||
this.mdField = valueChange;
|
if (page === 1) {
|
||||||
this.mdFieldChange.emit(this.mdField);
|
this.mdFieldOptions$.next([]);
|
||||||
}),
|
}
|
||||||
);
|
return this.search(query as string, page as number);
|
||||||
this.mdFieldOptions$ = this.query$.pipe(
|
}),
|
||||||
distinctUntilChanged(),
|
).subscribe((rd ) => {
|
||||||
switchMap((query: string) => {
|
if (!this.selectedValueLoading) {this.updateList(rd);}
|
||||||
this.showInvalid = false;
|
}));
|
||||||
if (query !== null) {
|
|
||||||
return this.registryService.queryMetadataFields(query, { elementsPerPage: 10, sort: new SortOptions('fieldName', SortDirection.ASC) }, true, false, followLink('schema')).pipe(
|
|
||||||
getAllSucceededRemoteData(),
|
|
||||||
metadataFieldsToString(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return [[]];
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -210,6 +238,41 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
|
|||||||
this.input.setValue(mdFieldOption);
|
this.input.setValue(mdFieldOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the user reaches the bottom of the page (or almost) and there's a next page available, increase the current page
|
||||||
|
*/
|
||||||
|
onScrollDown() {
|
||||||
|
if (this.hasNextPage && !this.loading) {
|
||||||
|
this.currentPage$.next(this.currentPage$.value + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description It update the mdFieldOptions$ according the query result page
|
||||||
|
* */
|
||||||
|
updateList(list: string[]) {
|
||||||
|
this.loading = false;
|
||||||
|
this.hasNextPage = list.length > 0;
|
||||||
|
const currentEntries = this.mdFieldOptions$.getValue();
|
||||||
|
this.mdFieldOptions$.next([...currentEntries, ...list]);
|
||||||
|
this.selectedValueLoading = false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Perform a search for the current query and page
|
||||||
|
* @param query Query to search objects for
|
||||||
|
* @param page Page to retrieve
|
||||||
|
* @param useCache Whether or not to use the cache
|
||||||
|
*/
|
||||||
|
search(query: string, page: number, useCache: boolean = true) {
|
||||||
|
return this.registryService.queryMetadataFields(query,{
|
||||||
|
elementsPerPage: this.pageOptions.elementsPerPage, sort: this.pageOptions.sort,
|
||||||
|
currentPage: page }, useCache, false, followLink('schema'))
|
||||||
|
.pipe(
|
||||||
|
getAllSucceededRemoteData(),
|
||||||
|
metadataFieldsToString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Unsubscribe from any open subscriptions
|
* Unsubscribe from any open subscriptions
|
||||||
*/
|
*/
|
||||||
|
@@ -22,21 +22,21 @@
|
|||||||
class="lead item-list-title dont-break-out"
|
class="lead item-list-title dont-break-out"
|
||||||
[innerHTML]="dsoTitle"></span>
|
[innerHTML]="dsoTitle"></span>
|
||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||||
<span *ngIf="dso.allMetadata(['publicationvolume.volumeNumber']).length > 0"
|
<span *ngIf="dso.allMetadata(['publicationvolume.volumeNumber']).length > 0"
|
||||||
class="item-list-journal-issues">
|
class="item-list-journal-issues">
|
||||||
<span *ngFor="let value of allMetadataValues(['publicationvolume.volumeNumber']); let last=last;">
|
<span *ngFor="let value of allMetadataValues(['publicationvolume.volumeNumber']); let last=last;">
|
||||||
<span [innerHTML]="value"><span [innerHTML]="value"></span></span>
|
<span [innerHTML]="value"></span><span *ngIf="!last">; </span>
|
||||||
</span>
|
|
||||||
<span *ngIf="dso.allMetadata(['publicationissue.issueNumber']).length > 0"
|
|
||||||
class="item-list-journal-issue-numbers">
|
|
||||||
<span *ngFor="let value of allMetadataValues(['publicationissue.issueNumber']); let last=last;">
|
|
||||||
<span> - </span><span [innerHTML]="value"><span [innerHTML]="value"></span></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
<span *ngIf="dso.allMetadata(['publicationissue.issueNumber']).length > 0"
|
||||||
|
class="item-list-journal-issue-numbers">
|
||||||
|
<span *ngFor="let value of allMetadataValues(['publicationissue.issueNumber']); let first=first; let last=last;">
|
||||||
|
<span *ngIf="first"> - </span><span [innerHTML]="value"></span><span *ngIf="!last">; </span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -26,13 +26,13 @@
|
|||||||
<span *ngIf="dso.allMetadata(['journal.title']).length > 0"
|
<span *ngIf="dso.allMetadata(['journal.title']).length > 0"
|
||||||
class="item-list-journal-volumes">
|
class="item-list-journal-volumes">
|
||||||
<span *ngFor="let value of allMetadataValues(['journal.title']); let last=last;">
|
<span *ngFor="let value of allMetadataValues(['journal.title']); let last=last;">
|
||||||
<span [innerHTML]="value"><span [innerHTML]="value"></span></span>
|
<span [innerHTML]="value"></span><span *ngIf="!last">; </span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="dso.allMetadata(['publicationvolume.volumeNumber']).length > 0"
|
<span *ngIf="dso.allMetadata(['publicationvolume.volumeNumber']).length > 0"
|
||||||
class="item-list-journal-volume-identifiers">
|
class="item-list-journal-volume-identifiers">
|
||||||
<span *ngFor="let value of allMetadataValues(['publicationvolume.volumeNumber']); let last=last;">
|
<span *ngFor="let value of allMetadataValues(['publicationvolume.volumeNumber']); let last=last;">
|
||||||
<span> (</span><span [innerHTML]="value"><span [innerHTML]="value"></span></span><span>)</span>
|
<span> (</span><span [innerHTML]="value"></span><span>)</span><span *ngIf="!last">;</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
|
@@ -24,7 +24,7 @@
|
|||||||
<span *ngIf="dso.allMetadata(['creativeworkseries.issn']).length > 0"
|
<span *ngIf="dso.allMetadata(['creativeworkseries.issn']).length > 0"
|
||||||
class="item-list-journals">
|
class="item-list-journals">
|
||||||
<span *ngFor="let value of allMetadataValues(['creativeworkseries.issn']); let last=last;">
|
<span *ngFor="let value of allMetadataValues(['creativeworkseries.issn']); let last=last;">
|
||||||
<span [innerHTML]="value"><span [innerHTML]="value"></span></span>
|
<span [innerHTML]="value"></span><span *ngIf="!last">; </span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
|
@@ -32,7 +32,7 @@
|
|||||||
<!--<span *ngIf="dso.allMetadata(['project.identifier.status']).length > 0"-->
|
<!--<span *ngIf="dso.allMetadata(['project.identifier.status']).length > 0"-->
|
||||||
<!--class="item-list-status">-->
|
<!--class="item-list-status">-->
|
||||||
<!--<span *ngFor="let value of allMetadataValues(['project.identifier.status']); let last=last;">-->
|
<!--<span *ngFor="let value of allMetadataValues(['project.identifier.status']); let last=last;">-->
|
||||||
<!--<span [innerHTML]="value"><span [innerHTML]="value"></span></span>-->
|
<!--<span [innerHTML]="value"></span><span *ngIf="!last">; </span>-->
|
||||||
<!--</span>-->
|
<!--</span>-->
|
||||||
<!--</span>-->
|
<!--</span>-->
|
||||||
<!--</ds-truncatable-part>-->
|
<!--</ds-truncatable-part>-->
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<span *ngIf="mdRepresentation.allMetadata(['person.jobTitle']).length > 0"
|
<span *ngIf="mdRepresentation.allMetadata(['person.jobTitle']).length > 0"
|
||||||
class="item-list-job-title">
|
class="item-list-job-title">
|
||||||
<span *ngFor="let value of mdRepresentation.allMetadataValues(['person.jobTitle']); let last=last;">
|
<span *ngFor="let value of mdRepresentation.allMetadataValues(['person.jobTitle']); let last=last;">
|
||||||
<span [innerHTML]="value"><span [innerHTML]="value"></span></span>
|
<span [innerHTML]="value"></span><span *ngIf="!last">; </span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -12,4 +12,9 @@
|
|||||||
<a [routerLink]="[itemPageRoute]"
|
<a [routerLink]="[itemPageRoute]"
|
||||||
[innerHTML]="mdRepresentation.getValue()"
|
[innerHTML]="mdRepresentation.getValue()"
|
||||||
[ngbTooltip]="mdRepresentation.allMetadata(['person.jobTitle']).length > 0 ? descTemplate : null"></a>
|
[ngbTooltip]="mdRepresentation.allMetadata(['person.jobTitle']).length > 0 ? descTemplate : null"></a>
|
||||||
|
<ds-orcid-badge-and-tooltip class="ml-1"
|
||||||
|
*ngIf="mdRepresentation.firstMetadata('person.identifier.orcid')"
|
||||||
|
[orcid]="mdRepresentation.firstMetadata('person.identifier.orcid')"
|
||||||
|
[authenticatedTimestamp]="mdRepresentation.firstMetadata('dspace.orcid.authenticated')">
|
||||||
|
</ds-orcid-badge-and-tooltip>
|
||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
|
@@ -7,13 +7,14 @@ import { RouterLink } from '@angular/router';
|
|||||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';
|
import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';
|
||||||
|
import { OrcidBadgeAndTooltipComponent } from '../../../../shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component';
|
||||||
import { TruncatableComponent } from '../../../../shared/truncatable/truncatable.component';
|
import { TruncatableComponent } from '../../../../shared/truncatable/truncatable.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-person-item-metadata-list-element',
|
selector: 'ds-person-item-metadata-list-element',
|
||||||
templateUrl: './person-item-metadata-list-element.component.html',
|
templateUrl: './person-item-metadata-list-element.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgIf, NgFor, TruncatableComponent, RouterLink, NgbTooltipModule],
|
imports: [NgIf, NgFor, TruncatableComponent, RouterLink, NgbTooltipModule, OrcidBadgeAndTooltipComponent],
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* The component for displaying an item of the type Person as a metadata field
|
* The component for displaying an item of the type Person as a metadata field
|
||||||
|
@@ -11,11 +11,11 @@
|
|||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
<span *ngIf="dso.allMetadata('organization.address.addressLocality').length > 0"
|
<span *ngIf="dso.allMetadata('organization.address.addressLocality').length > 0"
|
||||||
class="item-list-address-locality">
|
class="item-list-address-locality">
|
||||||
<span [innerHTML]="firstMetadataValue(['organization.address.addressLocality'])"><span [innerHTML]="firstMetadataValue(['organization.address.addressLocality'])"></span></span><span *ngIf="dso.allMetadata('organization.address.addressCountry').length > 0">, </span>
|
<span [innerHTML]="firstMetadataValue(['organization.address.addressLocality'])"></span><span *ngIf="dso.allMetadata('organization.address.addressCountry').length > 0">, </span>
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="dso.allMetadata('organization.address.addressCountry').length > 0"
|
<span *ngIf="dso.allMetadata('organization.address.addressCountry').length > 0"
|
||||||
class="item-list-address-country">
|
class="item-list-address-country">
|
||||||
<span [innerHTML]="firstMetadataValue(['organization.address.addressCountry'])"><span [innerHTML]="firstMetadataValue(['organization.address.addressCountry'])"></span></span>
|
<span [innerHTML]="firstMetadataValue(['organization.address.addressCountry'])"></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -33,7 +33,7 @@ import { OrgUnitInputSuggestionsComponent } from './org-unit-suggestions/org-uni
|
|||||||
@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModal)
|
@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModal)
|
||||||
@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModalWithNameVariants)
|
@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModalWithNameVariants)
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-person-search-result-list-submission-element',
|
selector: 'ds-org-unit-search-result-list-submission-element',
|
||||||
styleUrls: ['./org-unit-search-result-list-submission-element.component.scss'],
|
styleUrls: ['./org-unit-search-result-list-submission-element.component.scss'],
|
||||||
templateUrl: './org-unit-search-result-list-submission-element.component.html',
|
templateUrl: './org-unit-search-result-list-submission-element.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
@@ -23,13 +23,13 @@
|
|||||||
(clickSuggestion)="select($event)"
|
(clickSuggestion)="select($event)"
|
||||||
(submitSuggestion)="selectCustom($event)"></ds-person-input-suggestions>
|
(submitSuggestion)="selectCustom($event)"></ds-person-input-suggestions>
|
||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
<span *ngIf="dso.allMetadata(['person.jobTitle']).length > 0"
|
<span *ngIf="dso.allMetadata(['person.jobTitle']).length > 0"
|
||||||
class="item-list-job-title">
|
class="item-list-job-title">
|
||||||
<span *ngFor="let value of allMetadataValues(['person.jobTitle']); let last=last;">
|
<span *ngFor="let value of allMetadataValues(['person.jobTitle']); let last=last;">
|
||||||
<span [innerHTML]="value"><span [innerHTML]="value"></span></span>
|
<span [innerHTML]="value"></span><span *ngIf="!last">; </span>
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -23,7 +23,7 @@ import {
|
|||||||
import { NotifyInfoService } from '../core/coar-notify/notify-info/notify-info.service';
|
import { NotifyInfoService } from '../core/coar-notify/notify-info/notify-info.service';
|
||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
import { KlaroService } from '../shared/cookies/klaro.service';
|
import { OrejimeService } from '../shared/cookies/orejime.service';
|
||||||
import { hasValue } from '../shared/empty.util';
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -46,7 +46,7 @@ export class FooterComponent implements OnInit {
|
|||||||
coarLdnEnabled$: Observable<boolean>;
|
coarLdnEnabled$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Optional() public cookies: KlaroService,
|
@Optional() public cookies: OrejimeService,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected notifyInfoService: NotifyInfoService,
|
protected notifyInfoService: NotifyInfoService,
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
<span class="align-self-center">{{'item.alerts.withdrawn' | translate}}</span>
|
<span class="align-self-center">{{'item.alerts.withdrawn' | translate}}</span>
|
||||||
<div class="gap-2 d-flex">
|
<div class="gap-2 d-flex">
|
||||||
<a routerLink="/home" class="btn btn-primary btn-sm">{{"404.link.home-page" | translate}}</a>
|
<a routerLink="/home" class="btn btn-primary btn-sm">{{"404.link.home-page" | translate}}</a>
|
||||||
<a *ngIf="showReinstateButton$() | async" class="btn btn-primary btn-sm" (click)="openReinstateModal()">{{ 'item.alerts.reinstate-request' | translate}}</a>
|
<a *ngIf="showReinstateButton$ | async" class="btn btn-primary btn-sm" (click)="openReinstateModal()">{{ 'item.alerts.reinstate-request' | translate}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ds-alert>
|
</ds-alert>
|
||||||
|
@@ -161,7 +161,7 @@ describe('ItemAlertsComponent', () => {
|
|||||||
(authorizationService.isAuthorized).and.returnValue(isAdmin$);
|
(authorizationService.isAuthorized).and.returnValue(isAdmin$);
|
||||||
(correctionTypeDataService.findByItem).and.returnValue(correction$);
|
(correctionTypeDataService.findByItem).and.returnValue(correction$);
|
||||||
|
|
||||||
expectObservable(component.showReinstateButton$()).toBe(expectedMarble, expectedValues);
|
expectObservable(component.shouldShowReinstateButton()).toBe(expectedMarble, expectedValues);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user