mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'main' into task/main/DSC-1966
# Conflicts: # src/themes/dspace/app/header/header.component.html # src/themes/dspace/app/navbar/navbar.component.html
This commit is contained in:
@@ -12,7 +12,6 @@
|
||||
"eslint-plugin-rxjs",
|
||||
"eslint-plugin-simple-import-sort",
|
||||
"eslint-plugin-import-newlines",
|
||||
"eslint-plugin-jsonc",
|
||||
"dspace-angular-ts",
|
||||
"dspace-angular-html"
|
||||
],
|
||||
@@ -293,7 +292,9 @@
|
||||
],
|
||||
"rules": {
|
||||
// Custom DSpace Angular rules
|
||||
"dspace-angular-html/themed-component-usages": "error"
|
||||
"dspace-angular-html/themed-component-usages": "error",
|
||||
"dspace-angular-html/no-disabled-attribute-on-button": "error",
|
||||
"@angular-eslint/template/prefer-control-flow": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -301,10 +302,13 @@
|
||||
"*.json5"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:jsonc/recommended-with-jsonc"
|
||||
"plugin:jsonc/recommended-with-json5"
|
||||
],
|
||||
"rules": {
|
||||
"no-irregular-whitespace": "error",
|
||||
// The ESLint core no-irregular-whitespace rule doesn't work well in JSON
|
||||
// See: https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-irregular-whitespace.html
|
||||
"no-irregular-whitespace": "off",
|
||||
"jsonc/no-irregular-whitespace": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"jsonc/comma-dangle": [
|
||||
"error",
|
||||
|
155
.github/dependabot.yml
vendored
155
.github/dependabot.yml
vendored
@@ -1,12 +1,16 @@
|
||||
# Enable Dependabot NPM updates for all dependencies on a weekly basis
|
||||
#-------------------
|
||||
# DSpace's dependabot rules. Enables npm updates for all dependencies on a weekly basis
|
||||
# for main and any maintenance branches. Security updates only apply to main.
|
||||
#-------------------
|
||||
version: 2
|
||||
updates:
|
||||
###############
|
||||
## Main branch
|
||||
###############
|
||||
# NOTE: At this time, "security-updates" rules only apply if "target-branch" is unspecified
|
||||
# So, only this first section can include "applies-to: security-updates"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
target-branch: main
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
# Allow up to 10 open PRs for dependencies
|
||||
@@ -68,6 +72,7 @@ updates:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "@cypress*"
|
||||
- "axe-*"
|
||||
- "cypress*"
|
||||
- "jasmine*"
|
||||
- "karma*"
|
||||
@@ -76,10 +81,11 @@ updates:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any testing related security updates
|
||||
testing:
|
||||
testing-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "@cypress*"
|
||||
- "axe-*"
|
||||
- "cypress*"
|
||||
- "jasmine*"
|
||||
- "karma*"
|
||||
@@ -96,7 +102,7 @@ updates:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any postcss related security updates
|
||||
postcss:
|
||||
postcss-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "postcss*"
|
||||
@@ -112,8 +118,8 @@ updates:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any sass related security updates
|
||||
sass:
|
||||
applies-to: version-updates
|
||||
sass-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "sass*"
|
||||
update-types:
|
||||
@@ -128,7 +134,7 @@ updates:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any webpack related seurity updates
|
||||
webpack:
|
||||
webpack-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "webpack*"
|
||||
@@ -159,14 +165,6 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together all security updates for Angular. Only accept minor/patch types.
|
||||
angular-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "@angular*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together all minor/patch version updates for NgRx in a single PR
|
||||
ngrx:
|
||||
applies-to: version-updates
|
||||
@@ -175,14 +173,6 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together all security updates for NgRx. Only accept minor/patch types.
|
||||
ngrx-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "@ngrx*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together all patch version updates for eslint in a single PR
|
||||
eslint:
|
||||
applies-to: version-updates
|
||||
@@ -192,32 +182,12 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together all security updates for eslint.
|
||||
eslint-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "@typescript-eslint*"
|
||||
- "eslint*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any testing related version updates
|
||||
testing:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "@cypress*"
|
||||
- "cypress*"
|
||||
- "jasmine*"
|
||||
- "karma*"
|
||||
- "ng-mocks"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any testing related security updates
|
||||
testing:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "@cypress*"
|
||||
- "axe-*"
|
||||
- "cypress*"
|
||||
- "jasmine*"
|
||||
- "karma*"
|
||||
@@ -233,23 +203,7 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any postcss related security updates
|
||||
postcss:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "postcss*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any sass related version updates
|
||||
sass:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "sass*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any sass related security updates
|
||||
sass:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
@@ -265,14 +219,6 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any webpack related seurity updates
|
||||
webpack:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "webpack*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
ignore:
|
||||
# Ignore all major version updates for all dependencies. We'll only automate minor/patch updates.
|
||||
- dependency-name: "*"
|
||||
@@ -297,14 +243,6 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together all security updates for Angular. Only accept minor/patch types.
|
||||
angular-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "@angular*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together all minor/patch version updates for NgRx in a single PR
|
||||
ngrx:
|
||||
applies-to: version-updates
|
||||
@@ -313,14 +251,6 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together all security updates for NgRx. Only accept minor/patch types.
|
||||
ngrx-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "@ngrx*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together all patch version updates for eslint in a single PR
|
||||
eslint:
|
||||
applies-to: version-updates
|
||||
@@ -330,32 +260,12 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together all security updates for eslint.
|
||||
eslint-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "@typescript-eslint*"
|
||||
- "eslint*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any testing related version updates
|
||||
testing:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "@cypress*"
|
||||
- "cypress*"
|
||||
- "jasmine*"
|
||||
- "karma*"
|
||||
- "ng-mocks"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any testing related security updates
|
||||
testing:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "@cypress*"
|
||||
- "axe-*"
|
||||
- "cypress*"
|
||||
- "jasmine*"
|
||||
- "karma*"
|
||||
@@ -371,14 +281,6 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any postcss related security updates
|
||||
postcss:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "postcss*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any sass related version updates
|
||||
sass:
|
||||
applies-to: version-updates
|
||||
@@ -387,31 +289,10 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any sass related security updates
|
||||
sass:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "sass*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any webpack related version updates
|
||||
webpack:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "webpack*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# Group together any webpack related seurity updates
|
||||
webpack:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "webpack*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
ignore:
|
||||
# 7.x Cannot update Webpack past v5.76.1 as later versions not supported by Angular 15
|
||||
# See also https://github.com/DSpace/dspace-angular/pull/3283#issuecomment-2372488489
|
||||
- dependency-name: "webpack"
|
||||
# Ignore all major version updates for all dependencies. We'll only automate minor/patch updates.
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
|
88
.github/workflows/build.yml
vendored
88
.github/workflows/build.yml
vendored
@@ -7,7 +7,8 @@ name: Build
|
||||
on: [push, pull_request]
|
||||
|
||||
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:
|
||||
tests:
|
||||
@@ -35,6 +36,9 @@ jobs:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
# Project name to use when running "docker compose" prior to e2e tests
|
||||
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:
|
||||
# Create a matrix of Node versions to test against (in parallel)
|
||||
matrix:
|
||||
@@ -114,6 +118,14 @@ jobs:
|
||||
path: 'coverage/dspace-angular/lcov.info'
|
||||
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
|
||||
# and load assetstore from a cached copy
|
||||
- name: Start DSpace REST Backend via Docker (for e2e tests)
|
||||
@@ -178,12 +190,84 @@ jobs:
|
||||
# Get homepage and verify that the <meta name="title"> tag includes "DSpace".
|
||||
# If it does, then SSR is working, as this tag is created by our MetadataService.
|
||||
# This step also prints entire HTML of homepage for easier debugging if grep fails.
|
||||
- name: Verify SSR (server-side rendering)
|
||||
- name: Verify SSR (server-side rendering) on Homepage
|
||||
run: |
|
||||
result=$(wget -O- -q http://127.0.0.1:4000/home)
|
||||
echo "$result"
|
||||
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace
|
||||
|
||||
# Get a specific community in our test data and verify that the "<h1>" tag includes "Publications" (the community name).
|
||||
# If it does, then SSR is working.
|
||||
- name: Verify SSR on a Community page
|
||||
run: |
|
||||
result=$(wget -O- -q http://127.0.0.1:4000/communities/0958c910-2037-42a9-81c7-dca80e3892b4)
|
||||
echo "$result"
|
||||
echo "$result" | grep -oE "<h1 [^>]*>[^><]*</h1>" | grep Publications
|
||||
|
||||
# Get a specific collection in our test data and verify that the "<h1>" tag includes "Articles" (the collection name).
|
||||
# If it does, then SSR is working.
|
||||
- name: Verify SSR on a Collection page
|
||||
run: |
|
||||
result=$(wget -O- -q http://127.0.0.1:4000/collections/282164f5-d325-4740-8dd1-fa4d6d3e7200)
|
||||
echo "$result"
|
||||
echo "$result" | grep -oE "<h1 [^>]*>[^><]*</h1>" | grep Articles
|
||||
|
||||
# Get a specific publication in our test data and verify that the <meta name="title"> tag includes
|
||||
# the title of this publication. If it does, then SSR is working.
|
||||
- name: Verify SSR on a Publication page
|
||||
run: |
|
||||
result=$(wget -O- -q http://127.0.0.1:4000/entities/publication/6160810f-1e53-40db-81ef-f6621a727398)
|
||||
echo "$result"
|
||||
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "An Economic Model of Mortality Salience"
|
||||
|
||||
# Get a specific person in our test data and verify that the <meta name="title"> tag includes
|
||||
# the name of the person. If it does, then SSR is working.
|
||||
- name: Verify SSR on a Person page
|
||||
run: |
|
||||
result=$(wget -O- -q http://127.0.0.1:4000/entities/person/b1b2c768-bda1-448a-a073-fc541e8b24d9)
|
||||
echo "$result"
|
||||
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Simmons, Cameron"
|
||||
|
||||
# Get a specific project in our test data and verify that the <meta name="title"> tag includes
|
||||
# the name of the project. If it does, then SSR is working.
|
||||
- name: Verify SSR on a Project page
|
||||
run: |
|
||||
result=$(wget -O- -q http://127.0.0.1:4000/entities/project/46ccb608-a74c-4bf6-bc7a-e29cc7defea9)
|
||||
echo "$result"
|
||||
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "University Research Fellowship"
|
||||
|
||||
# Get a specific orgunit in our test data and verify that the <meta name="title"> tag includes
|
||||
# the name of the orgunit. If it does, then SSR is working.
|
||||
- name: Verify SSR on an OrgUnit page
|
||||
run: |
|
||||
result=$(wget -O- -q http://127.0.0.1:4000/entities/orgunit/9851674d-bd9a-467b-8d84-068deb568ccf)
|
||||
echo "$result"
|
||||
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Law and Development"
|
||||
|
||||
# Get a specific journal in our test data and verify that the <meta name="title"> tag includes
|
||||
# the name of the journal. If it does, then SSR is working.
|
||||
- name: Verify SSR on a Journal page
|
||||
run: |
|
||||
result=$(wget -O- -q http://127.0.0.1:4000/entities/journal/d4af6c3e-53d0-4757-81eb-566f3b45d63a)
|
||||
echo "$result"
|
||||
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental & Architectural Phenomenology"
|
||||
|
||||
# Get a specific journal volume in our test data and verify that the <meta name="title"> tag includes
|
||||
# the name of the volume. If it does, then SSR is working.
|
||||
- name: Verify SSR on a Journal Volume page
|
||||
run: |
|
||||
result=$(wget -O- -q http://127.0.0.1:4000/entities/journalvolume/07c6249f-4bf7-494d-9ce3-6ffdb2aed538)
|
||||
echo "$result"
|
||||
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental & Architectural Phenomenology Volume 28 (2017)"
|
||||
|
||||
# Get a specific journal issue in our test data and verify that the <meta name="title"> tag includes
|
||||
# the name of the issue. If it does, then SSR is working.
|
||||
- name: Verify SSR on a Journal Issue page
|
||||
run: |
|
||||
result=$(wget -O- -q http://127.0.0.1:4000/entities/journalissue/44c29473-5de2-48fa-b005-e5029aa1a50b)
|
||||
echo "$result"
|
||||
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental & Architectural Phenomenology Vol. 28, No. 1"
|
||||
|
||||
- name: Stop running app
|
||||
run: kill -9 $(lsof -t -i:4000)
|
||||
|
||||
|
3
.github/workflows/docker.yml
vendored
3
.github/workflows/docker.yml
vendored
@@ -16,7 +16,8 @@ on:
|
||||
pull_request:
|
||||
|
||||
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:
|
||||
#############################################################
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# This image will be published as dspace/dspace-angular
|
||||
# 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
|
||||
# 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.
|
||||
# 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
|
||||
ENV NODE_ENV development
|
||||
ENV NODE_ENV=development
|
||||
CMD npm run serve -- --host 0.0.0.0
|
||||
|
@@ -4,7 +4,7 @@
|
||||
# Test build:
|
||||
# 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
|
||||
# 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
|
||||
USER node
|
||||
ENV NODE_ENV production
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 4000
|
||||
CMD pm2-runtime start dspace-ui.json --json
|
||||
|
@@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
|
||||
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
|
||||
# clone the repo
|
||||
@@ -90,7 +90,7 @@ Requirements
|
||||
------------
|
||||
|
||||
- [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.
|
||||
|
||||
|
@@ -30,7 +30,6 @@
|
||||
"lodash",
|
||||
"jwt-decode",
|
||||
"uuid",
|
||||
"webfontloader",
|
||||
"zone.js"
|
||||
],
|
||||
"outputPath": "dist/browser",
|
||||
@@ -59,7 +58,10 @@
|
||||
"input": "src/themes/dspace/styles/theme.scss",
|
||||
"inject": false,
|
||||
"bundleName": "dspace-theme"
|
||||
}
|
||||
},
|
||||
"node_modules/leaflet/dist/leaflet.css",
|
||||
"node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css",
|
||||
"node_modules/leaflet.markercluster/dist/MarkerCluster.css"
|
||||
],
|
||||
"scripts": [],
|
||||
"baseHref": "/"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# NOTE: will log all redux actions and transfers in console
|
||||
debug: false
|
||||
|
||||
# Angular Universal server settings
|
||||
# Angular User Inteface settings
|
||||
# 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:
|
||||
@@ -17,12 +17,37 @@ ui:
|
||||
# Trust X-FORWARDED-* headers from proxies (default = true)
|
||||
useProxies: true
|
||||
|
||||
universal:
|
||||
# Whether to inline "critical" styles into the server-side rendered HTML.
|
||||
# Determining which styles are critical is a relatively expensive operation;
|
||||
# this option can be disabled to boost server performance at the expense of
|
||||
# loading smoothness.
|
||||
inlineCriticalCss: true
|
||||
# Angular Server Side Rendering (SSR) settings
|
||||
ssr:
|
||||
# Whether to tell Angular to inline "critical" styles into the server-side rendered HTML.
|
||||
# Determining which styles are critical is a relatively expensive operation; this option is
|
||||
# disabled (false) by default to boost server performance at the expense of loading smoothness.
|
||||
inlineCriticalCss: false
|
||||
# Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects.
|
||||
# NOTE: The "/handle/" path ensures Handle redirects work via SSR. The "/reload/" path ensures
|
||||
# hard refreshes (e.g. after login) trigger SSR while fully reloading the page.
|
||||
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/', '/reload/' ]
|
||||
# Whether to enable rendering of Search component on SSR.
|
||||
# If set to true the component will be included in the HTML returned from the server side rendering.
|
||||
# If set to false the component will not be included in the HTML returned from the server side rendering.
|
||||
enableSearchComponent: false
|
||||
# Whether to enable rendering of Browse component on SSR.
|
||||
# If set to true the component will be included in the HTML returned from the server side rendering.
|
||||
# If set to false the component will not be included in the HTML returned from the server side rendering.
|
||||
enableBrowseComponent: false
|
||||
# Enable state transfer from the server-side application to the client-side application.
|
||||
# Defaults to true.
|
||||
# Note: When using an external application cache layer, it's recommended not to transfer the state to avoid caching it.
|
||||
# Disabling it ensures that dynamic state information is not inadvertently cached, which can improve security and
|
||||
# ensure that users always use the most up-to-date state.
|
||||
transferState: true
|
||||
# When a different REST base URL is used for the server-side application, the generated state contains references to
|
||||
# REST resources with the internal URL configured. By default, these internal URLs are replaced with public URLs.
|
||||
# Disable this setting to avoid URL replacement during SSR. In this the state is not transferred to avoid security issues.
|
||||
replaceRestUrl: true
|
||||
# Enable request performance profiling data collection and printing the results in the server console.
|
||||
# Defaults to false. Enabling in production is NOT recommended
|
||||
#enablePerformanceProfiler: false
|
||||
|
||||
# The REST API server settings
|
||||
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
||||
@@ -33,6 +58,9 @@ rest:
|
||||
port: 443
|
||||
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||
nameSpace: /server
|
||||
# Provide a different REST url to be used during SSR execution. It must contain the whole url including protocol, server port and
|
||||
# server namespace (uncomment to use it).
|
||||
#ssrBaseUrl: http://localhost:8080/server
|
||||
|
||||
# Caching settings
|
||||
cache:
|
||||
@@ -322,15 +350,25 @@ item:
|
||||
# Rounded to the nearest size in the list of selectable sizes on the
|
||||
# settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'.
|
||||
pageSize: 5
|
||||
# Show the bitstream access status label on the item page
|
||||
showAccessStatuses: false
|
||||
|
||||
# Community Page Config
|
||||
community:
|
||||
# Default tab to be shown when browsing a Community. Valid values are: comcols, search, or browse_<field>
|
||||
# <field> must be any of the configured "browse by" fields, e.g., dateissued, author, title, or subject
|
||||
# When the default tab is not the 'search' tab, the search tab is moved to the last position
|
||||
defaultBrowseTab: search
|
||||
# Search tab config
|
||||
searchSection:
|
||||
showSidebar: true
|
||||
|
||||
# Collection Page Config
|
||||
collection:
|
||||
# Default tab to be shown when browsing a Collection. Valid values are: search, or browse_<field>
|
||||
# <field> must be any of the configured "browse by" fields, e.g., dateissued, author, title, or subject
|
||||
# When the default tab is not the 'search' tab, the search tab is moved to the last position
|
||||
defaultBrowseTab: search
|
||||
# Search tab config
|
||||
searchSection:
|
||||
showSidebar: true
|
||||
@@ -448,6 +486,12 @@ search:
|
||||
enabled: false
|
||||
# List of filters to enable in "Advanced Search" dropdown
|
||||
filter: [ 'title', 'author', 'subject', 'entityType' ]
|
||||
#
|
||||
# Number used to render n UI elements called loading skeletons that act as placeholders.
|
||||
# These elements indicate that some content will be loaded in their stead.
|
||||
# Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count.
|
||||
# e.g. If we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved.
|
||||
defaultFiltersCount: 5
|
||||
|
||||
|
||||
# Notify metrics
|
||||
@@ -502,7 +546,6 @@ notifyMetrics:
|
||||
config: 'NOTIFY.outgoing.delivered'
|
||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description'
|
||||
|
||||
|
||||
# Live Region configuration
|
||||
# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms:
|
||||
# Live regions are perceivable regions of a web page that are typically updated as a
|
||||
@@ -516,3 +559,37 @@ liveRegion:
|
||||
messageTimeOutDurationMs: 30000
|
||||
# The visibility of the live region. Setting this to true is only useful for debugging purposes.
|
||||
isVisible: false
|
||||
|
||||
# Geospatial Map display options
|
||||
geospatialMapViewer:
|
||||
# Which fields to use for parsing as geospatial points in search maps
|
||||
# (note, the item page field component allows any field(s) to be used
|
||||
# and is set as an input when declaring the component)
|
||||
spatialMetadataFields:
|
||||
- 'dcterms.spatial'
|
||||
# Which discovery configuration to use for 'geospatial search', used
|
||||
# in the browse map
|
||||
spatialFacetDiscoveryConfiguration: 'geospatial'
|
||||
# Which filter / facet name to use for faceted geospatial search
|
||||
# used in the browse map
|
||||
spatialPointFilterName: 'point'
|
||||
# Whether item page geospatial metadata should be displayed
|
||||
# (assumes they are wrapped in a test for this config in the template as
|
||||
# per the default templates supplied with DSpace for untyped-item and publication)
|
||||
enableItemPageFields: false
|
||||
# Whether the browse map should be enabled and included in the browse menu
|
||||
enableBrowseMap: false
|
||||
# Whether a 'map view' mode should be included alongside list and grid views
|
||||
# in search result pages
|
||||
enableSearchViewMode: false
|
||||
# The tile provider(s) to use for the map tiles drawn in the leaflet maps.
|
||||
# (see https://leaflet-extras.github.io/leaflet-providers/preview/) for a full list
|
||||
tileProviders:
|
||||
- 'OpenStreetMap.Mapnik'
|
||||
# Starting centre point for the map, as lat and lng coordinates. This is useful
|
||||
# to set the centre of the map when the map is first loaded and if there are no
|
||||
# points, shapes or markers to display.
|
||||
# Defaults to the centre of Istanbul
|
||||
defaultCentrePoint:
|
||||
lat: 41.015137
|
||||
lng: 28.979530
|
||||
|
@@ -9,10 +9,12 @@ describe('Admin Add New Modals', () => {
|
||||
|
||||
it('Add new Community modal should pass accessibility tests', () => {
|
||||
// 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
|
||||
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();
|
||||
|
||||
@@ -22,10 +24,12 @@ describe('Admin Add New Modals', () => {
|
||||
|
||||
it('Add new Collection modal should pass accessibility tests', () => {
|
||||
// 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
|
||||
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();
|
||||
|
||||
@@ -35,10 +39,12 @@ describe('Admin Add New Modals', () => {
|
||||
|
||||
it('Add new Item modal should pass accessibility tests', () => {
|
||||
// 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
|
||||
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();
|
||||
|
||||
|
@@ -9,10 +9,12 @@ describe('Admin Edit Modals', () => {
|
||||
|
||||
it('Edit Community modal should pass accessibility tests', () => {
|
||||
// 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
|
||||
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();
|
||||
|
||||
@@ -22,10 +24,12 @@ describe('Admin Edit Modals', () => {
|
||||
|
||||
it('Edit Collection modal should pass accessibility tests', () => {
|
||||
// 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
|
||||
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();
|
||||
|
||||
@@ -35,10 +39,12 @@ describe('Admin Edit Modals', () => {
|
||||
|
||||
it('Edit Item modal should pass accessibility tests', () => {
|
||||
// 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
|
||||
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();
|
||||
|
||||
|
@@ -9,10 +9,12 @@ describe('Admin Export Modals', () => {
|
||||
|
||||
it('Export metadata modal should pass accessibility tests', () => {
|
||||
// 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
|
||||
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();
|
||||
|
||||
@@ -22,10 +24,12 @@ describe('Admin Export Modals', () => {
|
||||
|
||||
it('Export batch modal should pass accessibility tests', () => {
|
||||
// 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
|
||||
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();
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
import { Options } from 'cypress-axe';
|
||||
|
||||
describe('Admin Sidebar', () => {
|
||||
beforeEach(() => {
|
||||
@@ -10,19 +9,12 @@ describe('Admin Sidebar', () => {
|
||||
|
||||
it('should be pinnable and pass accessibility tests', () => {
|
||||
// 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
|
||||
cy.get('ds-expandable-admin-sidebar-section').click({ multiple: true });
|
||||
|
||||
// Analyze <ds-admin-sidebar> for accessibility
|
||||
testA11y('ds-admin-sidebar',
|
||||
{
|
||||
rules: {
|
||||
// Currently all expandable sections have nested interactive elements
|
||||
// See https://github.com/DSpace/dspace-angular/issues/2178
|
||||
'nested-interactive': { enabled: false },
|
||||
},
|
||||
} as Options);
|
||||
testA11y('ds-admin-sidebar');
|
||||
});
|
||||
});
|
||||
|
@@ -12,6 +12,13 @@ describe('Community List Page', () => {
|
||||
cy.get('[data-test="expand-button"]').click({ multiple: true });
|
||||
|
||||
// Analyze <ds-community-list-page> for accessibility issues
|
||||
testA11y('ds-community-list-page');
|
||||
testA11y('ds-community-list-page', {
|
||||
rules: {
|
||||
// When expanding a cdk node on the community-list page, the 'aria-posinset' property becomes 0.
|
||||
// 0 is not a valid value for 'aria-posinset' so the test fails.
|
||||
// see https://github.com/DSpace/dspace-angular/issues/4068
|
||||
'aria-valid-attr-value': { enabled: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -9,18 +9,15 @@ beforeEach(() => {
|
||||
|
||||
// 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'));
|
||||
|
||||
// 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', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="metadata"]').should('be.visible');
|
||||
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');
|
||||
|
||||
// <ds-edit-item-page> tag must be loaded
|
||||
@@ -39,9 +36,11 @@ describe('Edit Item > Edit Metadata tab', () => {
|
||||
describe('Edit Item > Status tab', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="status"]').should('be.visible');
|
||||
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');
|
||||
|
||||
// <ds-item-status> tag must be loaded
|
||||
@@ -55,9 +54,11 @@ describe('Edit Item > Status tab', () => {
|
||||
describe('Edit Item > Bitstreams tab', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="bitstreams"]').should('be.visible');
|
||||
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');
|
||||
|
||||
// <ds-item-bitstreams> tag must be loaded
|
||||
@@ -82,9 +83,11 @@ describe('Edit Item > Bitstreams tab', () => {
|
||||
describe('Edit Item > Curate tab', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="curate"]').should('be.visible');
|
||||
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');
|
||||
|
||||
// <ds-item-curate> tag must be loaded
|
||||
@@ -98,9 +101,11 @@ describe('Edit Item > Curate tab', () => {
|
||||
describe('Edit Item > Relationships tab', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="relationships"]').should('be.visible');
|
||||
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');
|
||||
|
||||
// <ds-item-relationships> tag must be loaded
|
||||
@@ -114,9 +119,11 @@ describe('Edit Item > Relationships tab', () => {
|
||||
describe('Edit Item > Version History tab', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="versionhistory"]').should('be.visible');
|
||||
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');
|
||||
|
||||
// <ds-item-version-history> tag must be loaded
|
||||
@@ -130,9 +137,11 @@ describe('Edit Item > Version History tab', () => {
|
||||
describe('Edit Item > Access Control tab', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="access-control"]').should('be.visible');
|
||||
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');
|
||||
|
||||
// <ds-item-access-control> tag must be loaded
|
||||
@@ -146,9 +155,11 @@ describe('Edit Item > Access Control tab', () => {
|
||||
describe('Edit Item > Collection Mapper tab', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="mapper"]').should('be.visible');
|
||||
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');
|
||||
|
||||
// <ds-item-collection-mapper> tag must be loaded
|
||||
|
@@ -7,7 +7,7 @@ describe('Item Statistics Page', () => {
|
||||
it('should load if you click on "Statistics" from an Item/Entity page', () => {
|
||||
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
|
||||
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
|
||||
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
||||
cy.location('pathname').should('eq', '/statistics/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
|
||||
});
|
||||
|
||||
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
|
||||
|
@@ -217,7 +217,7 @@ describe('New Submission page', () => {
|
||||
});
|
||||
|
||||
// Close popup window
|
||||
cy.get('ds-dynamic-lookup-relation-modal button.close').click();
|
||||
cy.get('ds-dynamic-lookup-relation-modal button.btn-close').click();
|
||||
|
||||
// Back on the form, click the discard button to remove new submission
|
||||
// Clicking it will display a confirmation, which we will confirm with another click
|
||||
|
@@ -54,9 +54,9 @@ before(() => {
|
||||
|
||||
// Runs once before the first test in each "block"
|
||||
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.
|
||||
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
|
||||
cy.clearCookie(DSPACE_XSRF_COOKIE);
|
||||
|
@@ -1,10 +1,16 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
"**/*.ts",
|
||||
"../cypress.config.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"sourceMap": false,
|
||||
"typeRoots": [
|
||||
"../node_modules",
|
||||
"../node_modules/@types",
|
||||
"../src/typings.d.ts"
|
||||
],
|
||||
"types": [
|
||||
"cypress",
|
||||
"cypress-axe",
|
||||
|
@@ -21,7 +21,7 @@ networks:
|
||||
external: true
|
||||
services:
|
||||
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
|
||||
environment:
|
||||
# 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
|
||||
services:
|
||||
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:
|
||||
# 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
|
||||
|
@@ -33,7 +33,7 @@ services:
|
||||
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
|
||||
solr__D__statistics__P__autoCommit: 'false'
|
||||
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:
|
||||
- dspacedb
|
||||
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
|
||||
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:
|
||||
# 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
|
||||
@@ -81,7 +81,7 @@ services:
|
||||
# DSpace Solr container
|
||||
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:
|
||||
- dspacenet
|
||||
ports:
|
||||
@@ -93,7 +93,10 @@ services:
|
||||
volumes:
|
||||
# Keep Solr data directory between reboots
|
||||
- solr_data:/var/solr/data
|
||||
# Initialize all DSpace Solr cores using the mounted configsets (see above), then start Solr
|
||||
# NOTE: We are not running Solr as "root", but we need root permissions to copy our cores to the mounted
|
||||
# /var/solr/data directory. Then we start Solr as the "solr" user.
|
||||
user: root
|
||||
# Initialize all DSpace Solr cores, then start Solr
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
@@ -111,7 +114,8 @@ services:
|
||||
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
|
||||
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
|
||||
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
|
||||
exec solr -f
|
||||
chown -R solr:solr /var/solr
|
||||
runuser -u solr -- solr-foreground
|
||||
volumes:
|
||||
assetstore:
|
||||
pgdata:
|
||||
|
@@ -26,7 +26,7 @@ services:
|
||||
DSPACE_REST_HOST: sandbox.dspace.org
|
||||
DSPACE_REST_PORT: 443
|
||||
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:
|
||||
context: ..
|
||||
dockerfile: Dockerfile.dist
|
||||
|
@@ -40,7 +40,7 @@ services:
|
||||
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
||||
proxies__P__trusted__P__ipranges: '172.23.0'
|
||||
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:
|
||||
- dspacedb
|
||||
networks:
|
||||
@@ -68,7 +68,7 @@ services:
|
||||
dspacedb:
|
||||
container_name: dspacedb
|
||||
# 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:
|
||||
PGDATA: /pgdata
|
||||
POSTGRES_PASSWORD: dspace
|
||||
@@ -85,7 +85,7 @@ services:
|
||||
# DSpace Solr container
|
||||
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:
|
||||
- dspacenet
|
||||
ports:
|
||||
@@ -97,11 +97,16 @@ services:
|
||||
volumes:
|
||||
# Keep Solr data directory between reboots
|
||||
- solr_data:/var/solr/data
|
||||
# NOTE: We are not running Solr as "root", but we need root permissions to copy our cores to the mounted
|
||||
# /var/solr/data directory. Then we start Solr as the "solr" user.
|
||||
user: root
|
||||
# Initialize all DSpace Solr cores using the mounted local configsets (see above), then start Solr
|
||||
# * First, run precreate-core to create the core (if it doesn't yet exist). If exists already, this is a no-op
|
||||
# * Second, copy configsets to this core:
|
||||
# Updates to Solr configs require the container to be rebuilt/restarted:
|
||||
# `docker compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr`
|
||||
# * Third, ensure all new folders are owned by "solr" user
|
||||
# * Finally, start Solr as the "solr" user via the provided solr-foreground script
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
@@ -119,7 +124,8 @@ services:
|
||||
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
|
||||
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
|
||||
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
|
||||
exec solr -f
|
||||
chown -R solr:solr /var/solr
|
||||
runuser -u solr -- solr-foreground
|
||||
volumes:
|
||||
assetstore:
|
||||
pgdata:
|
||||
|
@@ -23,7 +23,7 @@ services:
|
||||
DSPACE_REST_HOST: localhost
|
||||
DSPACE_REST_PORT: 8080
|
||||
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:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
|
@@ -2,3 +2,4 @@
|
||||
_______
|
||||
|
||||
- [`dspace-angular-html/themed-component-usages`](./rules/themed-component-usages.md): Themeable components should be used via the selector of their `ThemedComponent` wrapper class
|
||||
- [`dspace-angular-html/no-disabled-attribute-on-button`](./rules/no-disabled-attribute-on-button.md): Buttons should use the `dsBtnDisabled` directive instead of the HTML `disabled` attribute.
|
||||
|
78
docs/lint/html/rules/no-disabled-attribute-on-button.md
Normal file
78
docs/lint/html/rules/no-disabled-attribute-on-button.md
Normal file
@@ -0,0 +1,78 @@
|
||||
[DSpace ESLint plugins](../../../../lint/README.md) > [HTML rules](../index.md) > `dspace-angular-html/no-disabled-attribute-on-button`
|
||||
_______
|
||||
|
||||
Buttons should use the `dsBtnDisabled` directive instead of the HTML `disabled` attribute.
|
||||
This should be done to ensure that users with a screen reader are able to understand that the a button button is present, and that it is disabled.
|
||||
The native html disabled attribute does not allow users to navigate to the button by keyboard, and thus they have no way of knowing that the button is present.
|
||||
|
||||
_______
|
||||
|
||||
[Source code](../../../../lint/src/rules/html/no-disabled-attribute-on-button.ts)
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
#### Valid code
|
||||
|
||||
##### should use [dsBtnDisabled] in HTML templates
|
||||
|
||||
```html
|
||||
<button [dsBtnDisabled]="true">Submit</button>
|
||||
```
|
||||
|
||||
##### disabled attribute is still valid on non-button elements
|
||||
|
||||
```html
|
||||
<input disabled>
|
||||
```
|
||||
|
||||
##### [disabled] attribute is still valid on non-button elements
|
||||
|
||||
```html
|
||||
<input [disabled]="true">
|
||||
```
|
||||
|
||||
##### angular dynamic attributes that use disabled are still valid
|
||||
|
||||
```html
|
||||
<button [class.disabled]="isDisabled">Submit</button>
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
#### Invalid code & automatic fixes
|
||||
|
||||
##### should not use disabled attribute in HTML templates
|
||||
|
||||
```html
|
||||
<button disabled>Submit</button>
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```html
|
||||
<button [dsBtnDisabled]="true">Submit</button>
|
||||
```
|
||||
|
||||
|
||||
##### should not use [disabled] attribute in HTML templates
|
||||
|
||||
```html
|
||||
<button [disabled]="true">Submit</button>
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```html
|
||||
<button [dsBtnDisabled]="true">Submit</button>
|
||||
```
|
||||
|
||||
|
||||
|
@@ -10,10 +10,13 @@ import {
|
||||
bundle,
|
||||
RuleExports,
|
||||
} from '../../util/structure';
|
||||
import * as noDisabledAttributeOnButton from './no-disabled-attribute-on-button';
|
||||
import * as themedComponentUsages from './themed-component-usages';
|
||||
|
||||
const index = [
|
||||
themedComponentUsages,
|
||||
noDisabledAttributeOnButton,
|
||||
|
||||
] as unknown as RuleExports[];
|
||||
|
||||
export = {
|
||||
|
147
lint/src/rules/html/no-disabled-attribute-on-button.ts
Normal file
147
lint/src/rules/html/no-disabled-attribute-on-button.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
TmplAstBoundAttribute,
|
||||
TmplAstTextAttribute,
|
||||
} from '@angular-eslint/bundled-angular-compiler';
|
||||
import { TemplateParserServices } from '@angular-eslint/utils';
|
||||
import {
|
||||
ESLintUtils,
|
||||
TSESLint,
|
||||
} from '@typescript-eslint/utils';
|
||||
|
||||
import {
|
||||
DSpaceESLintRuleInfo,
|
||||
NamedTests,
|
||||
} from '../../util/structure';
|
||||
import { getSourceCode } from '../../util/typescript';
|
||||
|
||||
export enum Message {
|
||||
USE_DSBTN_DISABLED = 'mustUseDsBtnDisabled',
|
||||
}
|
||||
|
||||
export const info = {
|
||||
name: 'no-disabled-attribute-on-button',
|
||||
meta: {
|
||||
docs: {
|
||||
description: `Buttons should use the \`dsBtnDisabled\` directive instead of the HTML \`disabled\` attribute.
|
||||
This should be done to ensure that users with a screen reader are able to understand that the a button button is present, and that it is disabled.
|
||||
The native html disabled attribute does not allow users to navigate to the button by keyboard, and thus they have no way of knowing that the button is present.`,
|
||||
},
|
||||
type: 'problem',
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
messages: {
|
||||
[Message.USE_DSBTN_DISABLED]: 'Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.',
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
} as DSpaceESLintRuleInfo;
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
...info,
|
||||
create(context: TSESLint.RuleContext<Message, unknown[]>) {
|
||||
const parserServices = getSourceCode(context).parserServices as TemplateParserServices;
|
||||
|
||||
/**
|
||||
* Some dynamic angular inputs will have disabled as name because of how Angular handles this internally (e.g [class.disabled]="isDisabled")
|
||||
* But these aren't actually the disabled attribute we're looking for, we can determine this by checking the details of the keySpan
|
||||
*/
|
||||
function isOtherAttributeDisabled(node: TmplAstBoundAttribute | TmplAstTextAttribute): boolean {
|
||||
// if the details are not null, and the details are not 'disabled', then it's not the disabled attribute we're looking for
|
||||
return node.keySpan?.details !== null && node.keySpan?.details !== 'disabled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the disabled text with [dsBtnDisabled] in the template
|
||||
*/
|
||||
function replaceDisabledText(text: string ): string {
|
||||
const hasBrackets = text.includes('[') && text.includes(']');
|
||||
const newDisabledText = hasBrackets ? 'dsBtnDisabled' : '[dsBtnDisabled]="true"';
|
||||
return text.replace('disabled', newDisabledText);
|
||||
}
|
||||
|
||||
function inputIsChildOfButton(node: any): boolean {
|
||||
return (node.parent?.tagName === 'button' || node.parent?.name === 'button');
|
||||
}
|
||||
|
||||
function reportAndFix(node: TmplAstBoundAttribute | TmplAstTextAttribute) {
|
||||
if (!inputIsChildOfButton(node) || isOtherAttributeDisabled(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceSpan = node.sourceSpan;
|
||||
context.report({
|
||||
messageId: Message.USE_DSBTN_DISABLED,
|
||||
loc: parserServices.convertNodeSourceSpanToLoc(sourceSpan),
|
||||
fix(fixer) {
|
||||
const templateText = sourceSpan.start.file.content;
|
||||
const disabledText = templateText.slice(sourceSpan.start.offset, sourceSpan.end.offset);
|
||||
const newText = replaceDisabledText(disabledText);
|
||||
return fixer.replaceTextRange([sourceSpan.start.offset, sourceSpan.end.offset], newText);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
'BoundAttribute[name="disabled"]'(node: TmplAstBoundAttribute) {
|
||||
reportAndFix(node);
|
||||
},
|
||||
'TextAttribute[name="disabled"]'(node: TmplAstTextAttribute) {
|
||||
reportAndFix(node);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const tests = {
|
||||
plugin: info.name,
|
||||
valid: [
|
||||
{
|
||||
name: 'should use [dsBtnDisabled] in HTML templates',
|
||||
code: `
|
||||
<button [dsBtnDisabled]="true">Submit</button>
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'disabled attribute is still valid on non-button elements',
|
||||
code: `
|
||||
<input disabled>
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: '[disabled] attribute is still valid on non-button elements',
|
||||
code: `
|
||||
<input [disabled]="true">
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'angular dynamic attributes that use disabled are still valid',
|
||||
code: `
|
||||
<button [class.disabled]="isDisabled">Submit</button>
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'should not use disabled attribute in HTML templates',
|
||||
code: `
|
||||
<button disabled>Submit</button>
|
||||
`,
|
||||
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
|
||||
output: `
|
||||
<button [dsBtnDisabled]="true">Submit</button>
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'should not use [disabled] attribute in HTML templates',
|
||||
code: `
|
||||
<button [disabled]="true">Submit</button>
|
||||
`,
|
||||
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
|
||||
output: `
|
||||
<button [dsBtnDisabled]="true">Submit</button>
|
||||
`,
|
||||
},
|
||||
],
|
||||
} as NamedTests;
|
||||
|
||||
export default rule;
|
13929
package-lock.json
generated
13929
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
187
package.json
187
package.json
@@ -57,153 +57,150 @@
|
||||
"private": true,
|
||||
"overrides": {
|
||||
"@kolkov/ngx-gallery": {
|
||||
"@angular/animations": "^17.3.11",
|
||||
"@angular/common": "^17.3.11",
|
||||
"@angular/core": "^17.3.11"
|
||||
"@angular/animations": "^18.2.12",
|
||||
"@angular/common": "^18.2.12",
|
||||
"@angular/core": "^18.2.12"
|
||||
},
|
||||
"@ng-bootstrap/ng-bootstrap": {
|
||||
"@angular/common": "^17.3.11",
|
||||
"@angular/core": "^17.3.11",
|
||||
"@angular/forms": "^17.3.11",
|
||||
"@angular/localize": "^17.3.11"
|
||||
"@angular/common": "^18.2.12",
|
||||
"@angular/core": "^18.2.12",
|
||||
"@angular/forms": "^18.2.12",
|
||||
"@angular/localize": "^18.2.12"
|
||||
},
|
||||
"@ng-dynamic-forms/core": {
|
||||
"@angular/common": "^17.3.11",
|
||||
"@angular/core": "^17.3.11",
|
||||
"@angular/forms": "^17.3.11"
|
||||
"@angular/common": "^18.2.12",
|
||||
"@angular/core": "^18.2.12",
|
||||
"@angular/forms": "^18.2.12"
|
||||
},
|
||||
"@ng-dynamic-forms/ui-ng-bootstrap": {
|
||||
"ngx-mask": "14.2.4"
|
||||
},
|
||||
"@ngtools/webpack": {
|
||||
"@angular/compiler-cli": "^17.3.11",
|
||||
"typescript": "~5.3.3"
|
||||
"ngx-mask": "14.2.4",
|
||||
"@ng-bootstrap/ng-bootstrap": "^12.0.0",
|
||||
"bootstrap": "^5.3"
|
||||
},
|
||||
"@nicky-lenaers/ngx-scroll-to": {
|
||||
"@angular/common": "^17.3.11",
|
||||
"@angular/core": "^17.3.11"
|
||||
"@angular/common": "^18.2.12",
|
||||
"@angular/core": "^18.2.12"
|
||||
},
|
||||
"eslint-plugin-unused-imports": {
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0"
|
||||
},
|
||||
"ng2-file-upload": {
|
||||
"@angular/common": "^17.3.11",
|
||||
"@angular/core": "^17.3.11"
|
||||
},
|
||||
"ngx-infinite-scroll": {
|
||||
"@angular/common": "^17.3.11",
|
||||
"@angular/core": "^17.3.11"
|
||||
}
|
||||
"@angular/common": "^18.2.12",
|
||||
"@angular/core": "^18.2.12"
|
||||
},
|
||||
"notistack": "3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.3.12",
|
||||
"@angular/cdk": "^17.3.10",
|
||||
"@angular/common": "^17.3.12",
|
||||
"@angular/compiler": "^17.3.12",
|
||||
"@angular/core": "^17.3.12",
|
||||
"@angular/forms": "^17.3.12",
|
||||
"@angular/localize": "^17.3.12",
|
||||
"@angular/platform-browser": "^17.3.12",
|
||||
"@angular/platform-browser-dynamic": "^17.3.12",
|
||||
"@angular/platform-server": "^17.3.12",
|
||||
"@angular/router": "^17.3.12",
|
||||
"@angular/ssr": "^17.3.10",
|
||||
"@babel/runtime": "7.25.7",
|
||||
"@angular/animations": "^18.2.12",
|
||||
"@angular/cdk": "^18.2.12",
|
||||
"@angular/common": "^18.2.12",
|
||||
"@angular/compiler": "^18.2.12",
|
||||
"@angular/core": "^18.2.12",
|
||||
"@angular/forms": "^18.2.12",
|
||||
"@angular/localize": "^18.2.12",
|
||||
"@angular/platform-browser": "^18.2.12",
|
||||
"@angular/platform-browser-dynamic": "^18.2.12",
|
||||
"@angular/platform-server": "^18.2.12",
|
||||
"@angular/router": "^18.2.12",
|
||||
"@angular/ssr": "^18.2.18",
|
||||
"@babel/runtime": "7.27.0",
|
||||
"@kolkov/ngx-gallery": "^2.0.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^12.0.0",
|
||||
"@ng-dynamic-forms/core": "^16.0.0",
|
||||
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
|
||||
"@ngrx/effects": "^17.1.1",
|
||||
"@ngrx/router-store": "^17.1.1",
|
||||
"@ngrx/store": "^17.1.1",
|
||||
"@ngx-translate/core": "^14.0.0",
|
||||
"@ngrx/effects": "^18.1.1",
|
||||
"@ngrx/operators": "^18.0.0",
|
||||
"@ngrx/router-store": "^18.1.1",
|
||||
"@ngrx/store": "^18.1.1",
|
||||
"@ngx-translate/core": "^16.0.3",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
|
||||
"@types/grecaptcha": "^3.0.4",
|
||||
"angular-idle-preload": "3.0.0",
|
||||
"@terraformer/wkt": "^2.2.1",
|
||||
"altcha": "^0.9.0",
|
||||
"angulartics2": "^12.2.0",
|
||||
"axios": "^1.7.4",
|
||||
"bootstrap": "^4.6.1",
|
||||
"axios": "^1.8.4",
|
||||
"bootstrap": "^5.3",
|
||||
"cerialize": "0.1.18",
|
||||
"cli-progress": "^3.12.0",
|
||||
"colors": "^1.4.0",
|
||||
"compression": "^1.7.4",
|
||||
"compression": "^1.7.5",
|
||||
"cookie-parser": "1.4.7",
|
||||
"core-js": "^3.38.1",
|
||||
"core-js": "^3.41.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"date-fns-tz": "^1.3.7",
|
||||
"deepmerge": "^4.3.1",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.21.1",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^5.1.3",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"filesize": "^6.1.0",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"filesize": "^10.1.6",
|
||||
"http-proxy-middleware": "^2.0.9",
|
||||
"http-terminator": "^3.2.0",
|
||||
"isbot": "^5.1.17",
|
||||
"isbot": "^5.1.26",
|
||||
"js-cookie": "2.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json5": "^2.2.3",
|
||||
"jsonschema": "1.4.1",
|
||||
"jsonschema": "1.5.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"klaro": "^0.7.18",
|
||||
"lodash": "^4.17.21",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-providers": "^2.0.0",
|
||||
"leaflet.markercluster": "^1.5.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lru-cache": "^7.14.1",
|
||||
"markdown-it": "^13.0.1",
|
||||
"mirador": "^3.3.0",
|
||||
"mirador": "^3.4.3",
|
||||
"mirador-dl-plugin": "^0.13.0",
|
||||
"mirador-share-plugin": "^0.11.0",
|
||||
"mirador-share-plugin": "^0.16.0",
|
||||
"morgan": "^1.10.0",
|
||||
"ng2-file-upload": "5.0.0",
|
||||
"ng2-file-upload": "7.0.1",
|
||||
"ng2-nouislider": "^2.0.0",
|
||||
"ngx-infinite-scroll": "^16.0.0",
|
||||
"ngx-infinite-scroll": "^18.0.0",
|
||||
"ngx-matomo-client": "^6.4.1",
|
||||
"ngx-pagination": "6.0.3",
|
||||
"ngx-ui-switch": "^14.1.0",
|
||||
"ngx-skeleton-loader": "^9.0.0",
|
||||
"ngx-ui-switch": "^15.0.0",
|
||||
"nouislider": "^15.7.1",
|
||||
"orejime": "^2.3.1",
|
||||
"pem": "1.14.8",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.0",
|
||||
"sanitize-html": "^2.12.1",
|
||||
"sortablejs": "1.15.0",
|
||||
"uuid": "^8.3.2",
|
||||
"webfontloader": "1.6.28",
|
||||
"zone.js": "~0.14.4"
|
||||
"zone.js": "~0.14.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "~17.0.2",
|
||||
"@angular-devkit/build-angular": "^17.3.8",
|
||||
"@angular-eslint/builder": "^17.5.3",
|
||||
"@angular-eslint/bundled-angular-compiler": "^17.5.3",
|
||||
"@angular-eslint/eslint-plugin": "^17.5.3",
|
||||
"@angular-eslint/eslint-plugin-template": "^17.5.3",
|
||||
"@angular-eslint/schematics": "^17.5.3",
|
||||
"@angular-eslint/template-parser": "^17.5.3",
|
||||
"@angular-eslint/utils": "^17.5.3",
|
||||
"@angular/cli": "^17.3.10",
|
||||
"@angular/compiler-cli": "^17.3.11",
|
||||
"@angular/language-service": "^17.3.12",
|
||||
"@angular-builders/custom-webpack": "~18.0.0",
|
||||
"@angular-devkit/build-angular": "^18.2.18",
|
||||
"@angular-eslint/builder": "^18.4.1",
|
||||
"@angular-eslint/bundled-angular-compiler": "^18.4.1",
|
||||
"@angular-eslint/eslint-plugin": "^18.4.1",
|
||||
"@angular-eslint/eslint-plugin-template": "^18.4.1",
|
||||
"@angular-eslint/schematics": "^18.4.1",
|
||||
"@angular-eslint/template-parser": "^18.4.1",
|
||||
"@angular-eslint/utils": "^18.4.1",
|
||||
"@angular/cli": "^18.2.18",
|
||||
"@angular/compiler-cli": "^18.2.12",
|
||||
"@angular/language-service": "^18.2.12",
|
||||
"@cypress/schematic": "^1.5.0",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@ngrx/store-devtools": "^17.1.1",
|
||||
"@ngtools/webpack": "^16.2.16",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@ngrx/store-devtools": "^18.1.1",
|
||||
"@ngtools/webpack": "^18.2.18",
|
||||
"@types/deep-freeze": "0.1.5",
|
||||
"@types/ejs": "^3.1.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/js-cookie": "2.2.6",
|
||||
"@types/lodash": "^4.17.12",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^14.14.9",
|
||||
"@types/sanitize-html": "^2.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@typescript-eslint/rule-tester": "^7.18.0",
|
||||
"@typescript-eslint/utils": "^7.18.0",
|
||||
"axe-core": "^4.10.0",
|
||||
"browser-sync": "^3.0.3",
|
||||
"axe-core": "^4.10.2",
|
||||
"compression-webpack-plugin": "^9.2.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.15.0",
|
||||
"cypress-axe": "^1.5.0",
|
||||
"cypress": "^13.17.0",
|
||||
"cypress-axe": "^1.6.0",
|
||||
"deep-freeze": "0.0.1",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-plugin-deprecation": "^1.4.1",
|
||||
@@ -212,12 +209,12 @@
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-import-newlines": "^1.3.1",
|
||||
"eslint-plugin-jsdoc": "^45.0.0",
|
||||
"eslint-plugin-jsonc": "^2.6.0",
|
||||
"eslint-plugin-jsonc": "^2.20.0",
|
||||
"eslint-plugin-lodash": "^7.4.0",
|
||||
"eslint-plugin-rxjs": "^5.0.3",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.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-core": "^3.8.0",
|
||||
"jasmine-marbles": "0.9.2",
|
||||
@@ -227,26 +224,20 @@
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"ng-mocks": "^14.13.1",
|
||||
"ng-mocks": "^14.13.4",
|
||||
"ngx-mask": "14.2.4",
|
||||
"nodemon": "^2.0.22",
|
||||
"postcss": "^8.4",
|
||||
"postcss-apply": "0.12.0",
|
||||
"postcss": "^8.5",
|
||||
"postcss-import": "^14.0.0",
|
||||
"postcss-loader": "^4.0.3",
|
||||
"postcss-preset-env": "^7.4.2",
|
||||
"postcss-responsive-type": "1.0.0",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs-spy": "^8.0.2",
|
||||
"sass": "~1.80.3",
|
||||
"sass": "~1.86.3",
|
||||
"sass-loader": "^12.6.0",
|
||||
"sass-resources-loader": "^2.2.5",
|
||||
"ts-node": "^8.10.2",
|
||||
"typescript": "~5.3.3",
|
||||
"webpack": "5.94.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
"typescript": "~5.4.5",
|
||||
"webpack": "5.99.6",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1"
|
||||
}
|
||||
|
@@ -1,8 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-import')(),
|
||||
require('postcss-preset-env')(),
|
||||
require('postcss-apply')(),
|
||||
require('postcss-responsive-type')()
|
||||
require('postcss-preset-env')()
|
||||
]
|
||||
};
|
||||
|
20
server.ts
20
server.ts
@@ -20,10 +20,10 @@ import 'reflect-metadata';
|
||||
|
||||
/* eslint-disable import/no-namespace */
|
||||
import * as morgan from 'morgan';
|
||||
import * as express from 'express';
|
||||
import express from 'express';
|
||||
import * as ejs from 'ejs';
|
||||
import * as compression from 'compression';
|
||||
import * as expressStaticGzip from 'express-static-gzip';
|
||||
import expressStaticGzip from 'express-static-gzip';
|
||||
/* eslint-enable import/no-namespace */
|
||||
import axios from 'axios';
|
||||
import LRU from 'lru-cache';
|
||||
@@ -81,6 +81,9 @@ let anonymousCache: LRU<string, any>;
|
||||
// extend environment with app config for server
|
||||
extendEnvironmentWithAppConfig(environment, appConfig);
|
||||
|
||||
// The REST server base URL
|
||||
const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl;
|
||||
|
||||
// The Express app is exported so that it can be used by serverless Functions.
|
||||
export function app() {
|
||||
|
||||
@@ -156,7 +159,7 @@ export function app() {
|
||||
* Proxy the sitemaps
|
||||
*/
|
||||
router.use('/sitemap**', createProxyMiddleware({
|
||||
target: `${environment.rest.baseUrl}/sitemaps`,
|
||||
target: `${REST_BASE_URL}/sitemaps`,
|
||||
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||
changeOrigin: true,
|
||||
}));
|
||||
@@ -165,7 +168,7 @@ export function app() {
|
||||
* Proxy the linksets
|
||||
*/
|
||||
router.use('/signposting**', createProxyMiddleware({
|
||||
target: `${environment.rest.baseUrl}`,
|
||||
target: `${REST_BASE_URL}`,
|
||||
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||
changeOrigin: true,
|
||||
}));
|
||||
@@ -218,7 +221,7 @@ export function app() {
|
||||
* The callback function to serve server side angular
|
||||
*/
|
||||
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)
|
||||
serverSideRender(req, res, next);
|
||||
} else {
|
||||
@@ -266,6 +269,11 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) {
|
||||
})
|
||||
.then((html) => {
|
||||
if (hasValue(html)) {
|
||||
// Replace REST URL with UI URL
|
||||
if (environment.ssr.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
|
||||
html = html.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
|
||||
}
|
||||
|
||||
// save server side rendered page to cache (if any are enabled)
|
||||
saveToCache(req, html);
|
||||
if (sendToUser) {
|
||||
@@ -623,7 +631,7 @@ function start() {
|
||||
* The callback function to serve health check requests
|
||||
*/
|
||||
function healthCheck(req, res) {
|
||||
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
|
||||
const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
|
||||
axios.get(baseUrl)
|
||||
.then((response) => {
|
||||
res.status(response.status).send(response.data);
|
||||
|
@@ -1,19 +1,13 @@
|
||||
<ngb-accordion #acc="ngbAccordion" [activeIds]="'browse'">
|
||||
<ngb-panel [id]="'browse'">
|
||||
<ng-template ngbPanelHeader>
|
||||
<div class="w-100 d-flex gap-3 justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('browse')"
|
||||
data-test="browse">
|
||||
<ng-template ngbPanelTitle>
|
||||
<div class="w-100 d-flex gap-3 justify-content-between collapse-toggle" (click)="acc.toggle('browse')"
|
||||
data-test="browse">
|
||||
<button type="button" class="btn btn-link p-0" (click)="$event.preventDefault()"
|
||||
[attr.aria-expanded]="acc.isExpanded('browse')"
|
||||
aria-controls="bulk-access-browse-panel-content">
|
||||
[attr.aria-expanded]="acc.isExpanded('browse')"
|
||||
aria-controls="bulk-access-browse-panel-content">
|
||||
{{ 'admin.access-control.bulk-access-browse.header' | translate }}
|
||||
</button>
|
||||
<div class="text-right d-flex gap-2">
|
||||
<div class="d-flex my-auto">
|
||||
<span *ngIf="acc.isExpanded('browse')" class="fas fa-chevron-up fa-fw"></span>
|
||||
<span *ngIf="!acc.isExpanded('browse')" class="fas fa-chevron-down fa-fw"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
@@ -22,11 +16,11 @@
|
||||
<li [ngbNavItem]="'search'" role="presentation">
|
||||
<a ngbNavLink>{{'admin.access-control.bulk-access-browse.search.header' | translate}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="mx-n3">
|
||||
<div class="bulk-access-search">
|
||||
<ds-search [configuration]="'administrativeBulkAccess'"
|
||||
[selectable]="true"
|
||||
[selectionConfig]="{ repeatable: true, listId: listId }"
|
||||
[showThumbnails]="false"></ds-search>
|
||||
[selectable]="true"
|
||||
[selectionConfig]="{ repeatable: true, listId: listId }"
|
||||
[showThumbnails]="false"></ds-search>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
@@ -42,21 +36,25 @@
|
||||
[showPaginator]="false"
|
||||
(prev)="pagePrev()"
|
||||
(next)="pageNext()">
|
||||
<ul *ngIf="(objectsSelected$|async)?.hasSucceeded" class="list-unstyled ml-4">
|
||||
<li *ngFor='let object of (objectsSelected$|async)?.payload?.page | paginate: { itemsPerPage: (paginationOptions$ | async).pageSize,
|
||||
currentPage: (paginationOptions$ | async).currentPage, totalItems: (objectsSelected$|async)?.payload?.page.length }; let i = index; let last = last '
|
||||
class="mt-4 mb-4 d-flex"
|
||||
[attr.data-test]="'list-object' | dsBrowserOnly">
|
||||
<ds-selectable-list-item-control [index]="i"
|
||||
[object]="object"
|
||||
[selectionConfig]="{ repeatable: true, listId: listId }"></ds-selectable-list-item-control>
|
||||
<ds-listable-object-component-loader [listID]="listId"
|
||||
[index]="i"
|
||||
[object]="object"
|
||||
[showThumbnails]="false"
|
||||
[viewMode]="'list'"></ds-listable-object-component-loader>
|
||||
</li>
|
||||
</ul>
|
||||
@if ((objectsSelected$|async)?.hasSucceeded) {
|
||||
<ul class="list-unstyled ms-4">
|
||||
@for (object of (objectsSelected$|async)?.payload?.page | paginate: { itemsPerPage: (paginationOptions$ | async).pageSize,
|
||||
currentPage: (paginationOptions$ | async).currentPage, totalItems: (objectsSelected$|async)?.payload?.page.length }; track object; let i = $index; let last = $last) {
|
||||
<li
|
||||
class="mt-4 mb-4 d-flex"
|
||||
[attr.data-test]="'list-object' | dsBrowserOnly">
|
||||
<ds-selectable-list-item-control [index]="i"
|
||||
[object]="object"
|
||||
[selectionConfig]="{ repeatable: true, listId: listId }"></ds-selectable-list-item-control>
|
||||
<ds-listable-object-component-loader [listID]="listId"
|
||||
[index]="i"
|
||||
[object]="object"
|
||||
[showThumbnails]="false"
|
||||
[viewMode]="'list'"></ds-listable-object-component-loader>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</ds-pagination>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
@@ -0,0 +1,4 @@
|
||||
.bulk-access-search {
|
||||
margin-right: calc(var(--bs-gutter-x, 1.5rem) / -2);
|
||||
margin-left: calc(var(--bs-gutter-x, 1.5rem) / -2);
|
||||
}
|
||||
|
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
@@ -59,11 +55,9 @@ import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
|
||||
AsyncPipe,
|
||||
NgbAccordionModule,
|
||||
TranslateModule,
|
||||
NgIf,
|
||||
NgbNavModule,
|
||||
ThemedSearchComponent,
|
||||
BrowserOnlyPipe,
|
||||
NgForOf,
|
||||
NgxPaginationModule,
|
||||
SelectableListItemControlComponent,
|
||||
ListableObjectComponentLoaderComponent,
|
||||
|
@@ -7,10 +7,10 @@
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button class="btn btn-outline-primary mr-3" (click)="reset()">
|
||||
<button class="btn btn-outline-primary me-3" (click)="reset()">
|
||||
{{ 'access-control-cancel' | translate }}
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!canExport()" (click)="submit()">
|
||||
<button class="btn btn-primary" [dsBtnDisabled]="!canExport()" (click)="submit()">
|
||||
{{ 'access-control-execute' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -1,4 +1,7 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
NO_ERRORS_SCHEMA,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
@@ -62,10 +65,17 @@ describe('BulkAccessComponent', () => {
|
||||
'file': { },
|
||||
};
|
||||
|
||||
const mockSettings: any = jasmine.createSpyObj('AccessControlFormContainerComponent', {
|
||||
getValue: jasmine.createSpy('getValue'),
|
||||
reset: jasmine.createSpy('reset'),
|
||||
});
|
||||
@Component({
|
||||
selector: 'ds-bulk-access-settings',
|
||||
template: '',
|
||||
exportAs: 'dsBulkSettings',
|
||||
standalone: true,
|
||||
})
|
||||
class MockBulkAccessSettingsComponent {
|
||||
isFormValid = jasmine.createSpy('isFormValid').and.returnValue(false);
|
||||
getValue = jasmine.createSpy('getValue');
|
||||
reset = jasmine.createSpy('reset');
|
||||
}
|
||||
const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }];
|
||||
const selectableListState: SelectableListState = { id: 'test', selection };
|
||||
const expectedIdList = ['1234', '5678'];
|
||||
@@ -93,6 +103,9 @@ describe('BulkAccessComponent', () => {
|
||||
BulkAccessSettingsComponent,
|
||||
],
|
||||
},
|
||||
add: {
|
||||
imports: [MockBulkAccessSettingsComponent],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
@@ -109,13 +122,12 @@ describe('BulkAccessComponent', () => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
describe('when there are no elements selected', () => {
|
||||
describe('when there are no elements selected and step two form is invalid', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListStateEmpty));
|
||||
fixture.detectChanges();
|
||||
component.settings = mockSettings;
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
@@ -138,7 +150,6 @@ describe('BulkAccessComponent', () => {
|
||||
|
||||
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState));
|
||||
fixture.detectChanges();
|
||||
component.settings = mockSettings;
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
@@ -149,15 +160,29 @@ describe('BulkAccessComponent', () => {
|
||||
expect(component.objectsSelected$.value).toEqual(expectedIdList);
|
||||
});
|
||||
|
||||
it('should enable the execute button when there are objects selected', () => {
|
||||
it('should not enable the execute button when there are objects selected and step two form is invalid', () => {
|
||||
component.objectsSelected$.next(['1234']);
|
||||
expect(component.canExport()).toBe(true);
|
||||
expect(component.canExport()).toBe(false);
|
||||
});
|
||||
|
||||
it('should call the settings reset method when reset is called', () => {
|
||||
component.reset();
|
||||
expect(component.settings.reset).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('when there are elements selected and the step two form is valid', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState));
|
||||
fixture.detectChanges();
|
||||
(component as any).settings.isFormValid.and.returnValue(true);
|
||||
});
|
||||
|
||||
it('should enable the execute button when there are objects selected and step two form is valid', () => {
|
||||
component.objectsSelected$.next(['1234']);
|
||||
expect(component.canExport()).toBe(true);
|
||||
});
|
||||
|
||||
it('should call the bulkAccessControlService executeScript method when submit is called', () => {
|
||||
(component.settings as any).getValue.and.returnValue(mockFormState);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer';
|
||||
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { BulkAccessBrowseComponent } from './browse/bulk-access-browse.component';
|
||||
@@ -27,8 +29,10 @@ import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.com
|
||||
TranslateModule,
|
||||
BulkAccessSettingsComponent,
|
||||
BulkAccessBrowseComponent,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BulkAccessComponent implements OnInit {
|
||||
|
||||
@@ -68,7 +72,7 @@ export class BulkAccessComponent implements OnInit {
|
||||
}
|
||||
|
||||
canExport(): boolean {
|
||||
return this.objectsSelected$.value?.length > 0;
|
||||
return this.objectsSelected$.value?.length > 0 && this.settings?.isFormValid();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,17 +1,11 @@
|
||||
<ngb-accordion #acc="ngbAccordion" [activeIds]="'settings'">
|
||||
<ngb-panel [id]="'settings'">
|
||||
<ng-template ngbPanelHeader>
|
||||
<ng-template ngbPanelTitle>
|
||||
<div class="w-100 d-flex gap-3 justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('settings')" data-test="settings">
|
||||
<button type="button" class="btn btn-link p-0" (click)="$event.preventDefault()" [attr.aria-expanded]="acc.isExpanded('settings')"
|
||||
aria-controls="bulk-access-settings-panel-content">
|
||||
{{ 'admin.access-control.bulk-access-settings.header' | translate }}
|
||||
</button>
|
||||
<div class="text-right d-flex gap-2">
|
||||
<div class="d-flex my-auto">
|
||||
<span *ngIf="acc.isExpanded('settings')" class="fas fa-chevron-up fa-fw"></span>
|
||||
<span *ngIf="!acc.isExpanded('settings')" class="fas fa-chevron-down fa-fw"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { NgIf } from '@angular/common';
|
||||
|
||||
import {
|
||||
Component,
|
||||
ViewChild,
|
||||
@@ -16,7 +16,6 @@ import { AccessControlFormContainerComponent } from '../../../shared/access-cont
|
||||
imports: [
|
||||
NgbAccordionModule,
|
||||
TranslateModule,
|
||||
NgIf,
|
||||
AccessControlFormContainerComponent,
|
||||
],
|
||||
standalone: true,
|
||||
@@ -43,4 +42,8 @@ export class BulkAccessSettingsComponent {
|
||||
this.controlForm.reset();
|
||||
}
|
||||
|
||||
isFormValid() {
|
||||
return this.controlForm.isValid();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -5,10 +5,10 @@
|
||||
<h1 id="header" class="pb-2">{{labelPrefix + 'head' | translate}}</h1>
|
||||
|
||||
<div>
|
||||
<button class="mr-auto btn btn-success addEPerson-button"
|
||||
[routerLink]="'create'">
|
||||
<button class="me-auto btn btn-success addEPerson-button"
|
||||
[routerLink]="'create'">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline ml-1">{{labelPrefix + 'button.add' | translate}}</span>
|
||||
<span class="d-none d-sm-inline ms-1">{{labelPrefix + 'button.add' | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,77 +18,84 @@
|
||||
</h2>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||
<div>
|
||||
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
||||
<select name="scope" id="scope" formControlName="scope" class="form-select" aria-label="Search scope">
|
||||
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
|
||||
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-grow-1 mr-3 ml-3">
|
||||
<div class="form-group input-group">
|
||||
<div class="flex-grow-1 me-3 ms-3">
|
||||
<div class="mb-3 input-group">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
|
||||
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
|
||||
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
|
||||
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
|
||||
<span class="input-group-append">
|
||||
<button type="submit" class="search-button btn btn-primary">
|
||||
<i class="fas fa-search"></i> {{ labelPrefix + 'search.button' | translate }}
|
||||
</button>
|
||||
</span>
|
||||
<button type="submit" class="search-button btn btn-primary">
|
||||
<i class="fas fa-search"></i> {{ labelPrefix + 'search.button' | translate }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button (click)="clearFormAndResetResult();"
|
||||
class="search-button btn btn-secondary">{{labelPrefix + 'button.see-all' | translate}}</button>
|
||||
class="search-button btn btn-secondary">{{labelPrefix + 'button.see-all' | translate}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-loading *ngIf="searching$ | async"></ds-loading>
|
||||
<ds-pagination
|
||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (searching$ | async) !== true"
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="epeople" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
|
||||
<th>{{labelPrefix + 'table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
|
||||
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}">
|
||||
<td>{{epersonDto.eperson.id}}</td>
|
||||
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
||||
<td>{{epersonDto.eperson.email}}</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button [routerLink]="getEditEPeoplePage(epersonDto.eperson.id)"
|
||||
@if (searching$ | async) {
|
||||
<ds-loading></ds-loading>
|
||||
}
|
||||
@if ((pageInfoState$ | async)?.totalElements > 0 && (searching$ | async) !== true) {
|
||||
<ds-pagination
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="epeople" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
|
||||
<th>{{labelPrefix + 'table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (epersonDto of (ePeopleDto$ | async)?.page; track epersonDto) {
|
||||
<tr
|
||||
[ngClass]="{'table-primary' : (activeEPerson$ | async) === epersonDto.eperson}">
|
||||
<td>{{epersonDto.eperson.id}}</td>
|
||||
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
||||
<td>{{epersonDto.eperson.email}}</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button [routerLink]="getEditEPeoplePage(epersonDto.eperson.id)"
|
||||
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
<button *ngIf="epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
|
||||
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
@if (epersonDto.ableToDelete) {
|
||||
<button (click)="deleteEPerson(epersonDto.eperson)"
|
||||
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
}
|
||||
|
||||
@if ((pageInfoState$ | async)?.totalElements === 0) {
|
||||
<div class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{labelPrefix + 'no-items' | translate}}
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(pageInfoState$ | async)?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{labelPrefix + 'no-items' | translate}}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -42,6 +42,7 @@ import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
|
||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||
import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock';
|
||||
@@ -151,7 +152,7 @@ describe('EPeopleRegistryComponent', () => {
|
||||
paginationService = new PaginationServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule.withRoutes([]),
|
||||
TranslateModule.forRoot(), EPeopleRegistryComponent],
|
||||
TranslateModule.forRoot(), EPeopleRegistryComponent, BtnDisabledDirective],
|
||||
providers: [
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgClass,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
@@ -72,13 +70,11 @@ import { EPersonFormComponent } from './eperson-form/eperson-form.component';
|
||||
TranslateModule,
|
||||
RouterModule,
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
EPersonFormComponent,
|
||||
ReactiveFormsModule,
|
||||
ThemedLoadingComponent,
|
||||
PaginationComponent,
|
||||
NgClass,
|
||||
NgForOf,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
@@ -100,6 +96,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -165,6 +163,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
initialisePage() {
|
||||
this.searching$.next(true);
|
||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
||||
this.activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||
this.subs.push(this.ePeople$.pipe(
|
||||
switchMap((epeople: PaginatedList<EPerson>) => {
|
||||
if (epeople.pageInfo.totalElements > 0) {
|
||||
@@ -232,23 +231,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
|
||||
*/
|
||||
|
@@ -2,95 +2,111 @@
|
||||
<div class="group-form row">
|
||||
<div class="col-12">
|
||||
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async; then editHeader; else createHeader"></div>
|
||||
|
||||
<ng-template #createHeader>
|
||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #editHeader>
|
||||
@if (activeEPerson$ | async) {
|
||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.edit' | translate}}</h1>
|
||||
</ng-template>
|
||||
} @else {
|
||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
|
||||
}
|
||||
|
||||
|
||||
|
||||
<ds-form [formId]="formId"
|
||||
[formModel]="formModel"
|
||||
[formGroup]="formGroup"
|
||||
[formLayout]="formLayout"
|
||||
[displayCancel]="false"
|
||||
[submitLabel]="submitLabel"
|
||||
(submitForm)="onSubmit()">
|
||||
[formModel]="formModel"
|
||||
[formGroup]="formGroup"
|
||||
[formLayout]="formLayout"
|
||||
[displayCancel]="false"
|
||||
[submitLabel]="submitLabel"
|
||||
(submitForm)="onSubmit()">
|
||||
<div before class="btn-group">
|
||||
<button (click)="onCancel()" type="button" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="displayResetPassword" between class="btn-group">
|
||||
<button class="btn btn-primary" [disabled]="(canReset$ | async) !== true" type="button" (click)="resetPassword()">
|
||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||
@if (displayResetPassword) {
|
||||
<div between class="btn-group">
|
||||
<button class="btn btn-primary" [dsBtnDisabled]="(canReset$ | async) !== true" type="button" (click)="resetPassword()">
|
||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (canImpersonate$ | async) {
|
||||
<div between class="btn-group ms-1">
|
||||
@if (!isImpersonated) {
|
||||
<button class="btn btn-primary" type="button" (click)="impersonate()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
||||
</button>
|
||||
}
|
||||
@if (isImpersonated) {
|
||||
<button class="btn btn-primary" type="button" (click)="stopImpersonating()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (canDelete$ | async) {
|
||||
<button after class="btn btn-danger delete-button" type="button" (click)="delete()">
|
||||
<i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="canImpersonate$ | async" between class="btn-group">
|
||||
<button *ngIf="!isImpersonated" class="btn btn-primary" type="button" (click)="impersonate()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
||||
</button>
|
||||
<button *ngIf="isImpersonated" class="btn btn-primary" type="button" (click)="stopImpersonating()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<button *ngIf="canDelete$ | async" after class="btn btn-danger delete-button" type="button" (click)="delete()">
|
||||
<i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
||||
</button>
|
||||
}
|
||||
</ds-form>
|
||||
|
||||
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading>
|
||||
@if (!formGroup) {
|
||||
<ds-loading [showMessage]="false"></ds-loading>
|
||||
}
|
||||
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
|
||||
|
||||
<ds-loading [showMessage]="false" *ngIf="groups$ | async | dsHasNoValue"></ds-loading>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(groups$ | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="(groups$ | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="groups" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (groups$ | async)?.payload?.page">
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="groupsDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">
|
||||
{{ dsoNameService.getName(group) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">{{ dsoNameService.getName(undefined) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(groups$ | async)?.payload?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
<div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div>
|
||||
<div>
|
||||
<button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]"
|
||||
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
|
||||
</div>
|
||||
@if (activeEPerson$ | async) {
|
||||
<div>
|
||||
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
|
||||
@if (groups$ | async | dsHasNoValue) {
|
||||
<ds-loading [showMessage]="false"></ds-loading>
|
||||
}
|
||||
@if ((groups$ | async)?.payload?.totalElements > 0) {
|
||||
<ds-pagination
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="(groups$ | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
<div class="table-responsive">
|
||||
<table id="groups" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (group of (groups$ | async)?.payload?.page; track group) {
|
||||
<tr>
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="groupsDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">
|
||||
{{ dsoNameService.getName(group) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{{ dsoNameService.getName((group.object | async)?.payload) }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
}
|
||||
@if ((groups$ | async)?.payload?.totalElements === 0) {
|
||||
<div class="alert alert-info w-100 mb-2" role="alert">
|
||||
<div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div>
|
||||
<div>
|
||||
<button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]"
|
||||
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
@@ -19,12 +19,10 @@ import {
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
RouterModule,
|
||||
} from '@angular/router';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
TranslateLoader,
|
||||
TranslateModule,
|
||||
} from '@ngx-translate/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
@@ -45,11 +43,11 @@ import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||
import { FormComponent } from '../../../shared/form/form.component';
|
||||
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
||||
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 { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
@@ -92,9 +90,6 @@ describe('EPersonFormComponent', () => {
|
||||
ePersonDataServiceStub = {
|
||||
activeEPerson: null,
|
||||
allEpeople: mockEPeople,
|
||||
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople));
|
||||
},
|
||||
getActiveEPerson(): Observable<EPerson> {
|
||||
return observableOf(this.activeEPerson);
|
||||
},
|
||||
@@ -227,13 +222,9 @@ describe('EPersonFormComponent', () => {
|
||||
route = new ActivatedRouteStub();
|
||||
router = new RouterStub();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock,
|
||||
},
|
||||
}),
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BtnDisabledDirective, BrowserModule,
|
||||
RouterModule.forRoot([]),
|
||||
TranslateModule.forRoot(),
|
||||
EPersonFormComponent,
|
||||
HasNoValuePipe,
|
||||
],
|
||||
@@ -251,7 +242,7 @@ describe('EPersonFormComponent', () => {
|
||||
{ provide: Router, useValue: router },
|
||||
EPeopleRegistryComponent,
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
.overrideComponent(EPersonFormComponent, {
|
||||
remove: { imports: [ ThemedLoadingComponent, PaginationComponent,FormComponent] },
|
||||
@@ -274,37 +265,13 @@ describe('EPersonFormComponent', () => {
|
||||
});
|
||||
|
||||
describe('check form validation', () => {
|
||||
let firstName;
|
||||
let lastName;
|
||||
let email;
|
||||
let canLogIn;
|
||||
let requireCertificate;
|
||||
let canLogIn: boolean;
|
||||
let requireCertificate: boolean;
|
||||
|
||||
let expected;
|
||||
beforeEach(() => {
|
||||
firstName = 'testName';
|
||||
lastName = 'testLastName';
|
||||
email = 'testEmail@test.com';
|
||||
canLogIn = 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');
|
||||
component.canLogIn.value = canLogIn;
|
||||
component.requireCertificate.value = requireCertificate;
|
||||
@@ -378,15 +345,13 @@ describe('EPersonFormComponent', () => {
|
||||
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('when submitting the form', () => {
|
||||
let firstName;
|
||||
let lastName;
|
||||
let email;
|
||||
let canLogIn;
|
||||
let canLogIn: boolean;
|
||||
let requireCertificate;
|
||||
|
||||
let expected;
|
||||
@@ -415,6 +380,7 @@ describe('EPersonFormComponent', () => {
|
||||
requireCertificate: requireCertificate,
|
||||
});
|
||||
spyOn(component.submitForm, 'emit');
|
||||
component.ngOnInit();
|
||||
component.firstName.value = firstName;
|
||||
component.lastName.value = lastName;
|
||||
component.email.value = email;
|
||||
@@ -454,9 +420,17 @@ describe('EPersonFormComponent', () => {
|
||||
email: email,
|
||||
canLogIn: canLogIn,
|
||||
requireCertificate: requireCertificate,
|
||||
_links: undefined,
|
||||
_links: {
|
||||
groups: {
|
||||
href: '',
|
||||
},
|
||||
self: {
|
||||
href: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
|
||||
component.ngOnInit();
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -504,22 +478,19 @@ describe('EPersonFormComponent', () => {
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
|
||||
let ePersonId;
|
||||
let eperson: EPerson;
|
||||
let modalService;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authService, 'impersonate').and.callThrough();
|
||||
ePersonId = 'testEPersonId';
|
||||
eperson = EPersonMock;
|
||||
component.epersonInitial = eperson;
|
||||
component.canDelete$ = observableOf(true);
|
||||
spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson));
|
||||
modalService = (component as any).modalService;
|
||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
});
|
||||
|
||||
it('the delete button should be visible if the ePerson can be deleted', () => {
|
||||
@@ -546,7 +517,8 @@ describe('EPersonFormComponent', () => {
|
||||
// ePersonDataServiceStub.activeEPerson = eperson;
|
||||
spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204));
|
||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||
expect(deleteButton.nativeElement.disabled).toBe(false);
|
||||
expect(deleteButton.nativeElement.getAttribute('aria-disabled')).toBeNull();
|
||||
expect(deleteButton.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||
deleteButton.triggerEventHandler('click', null);
|
||||
fixture.detectChanges();
|
||||
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgClass,
|
||||
NgFor,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
@@ -65,6 +63,7 @@ import {
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { Registration } from '../../../core/shared/registration.model';
|
||||
import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||
@@ -83,8 +82,6 @@ import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||
templateUrl: './eperson-form.component.html',
|
||||
imports: [
|
||||
FormComponent,
|
||||
NgIf,
|
||||
NgFor,
|
||||
AsyncPipe,
|
||||
TranslateModule,
|
||||
NgClass,
|
||||
@@ -92,6 +89,7 @@ import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||
PaginationComponent,
|
||||
RouterLink,
|
||||
HasNoValuePipe,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
@@ -189,6 +187,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
canImpersonate$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The current {@link EPerson}
|
||||
*/
|
||||
activeEPerson$: Observable<EPerson>;
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
*/
|
||||
@@ -254,7 +257,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
protected route: ActivatedRoute,
|
||||
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;
|
||||
if (hasValue(eperson)) {
|
||||
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
||||
@@ -262,9 +269,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
this.submitLabel = 'form.submit';
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.initialisePage();
|
||||
}
|
||||
|
||||
@@ -272,130 +276,121 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
* This method will initialise the page
|
||||
*/
|
||||
initialisePage() {
|
||||
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();
|
||||
});
|
||||
}
|
||||
if (this.route.snapshot.params.id) {
|
||||
this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData<EPerson>) => {
|
||||
this.epersonService.editEPerson(ePersonRD.payload);
|
||||
}));
|
||||
|
||||
const activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||
|
||||
this.groups$ = 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$ = 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.firstName = new DynamicInputModel({
|
||||
id: 'firstName',
|
||||
label: this.translateService.instant(`${this.messagePrefix}.firstName`),
|
||||
name: 'firstName',
|
||||
validators: {
|
||||
required: null,
|
||||
},
|
||||
required: 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 +409,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
* Emit the updated/created eperson using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe(
|
||||
this.activeEPerson$.pipe(take(1)).subscribe(
|
||||
(ePerson: EPerson) => {
|
||||
const values = {
|
||||
metadata: {
|
||||
@@ -533,7 +528,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.
|
||||
*/
|
||||
delete(): void {
|
||||
this.epersonService.getActiveEPerson().pipe(
|
||||
this.activeEPerson$.pipe(
|
||||
take(1),
|
||||
switchMap((eperson: EPerson) => {
|
||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||
@@ -637,7 +632,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
* Update the list of groups by fetching it from the rest api or cache
|
||||
*/
|
||||
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);
|
||||
}));
|
||||
}
|
||||
|
@@ -2,13 +2,7 @@
|
||||
<div class="group-form row">
|
||||
<div class="col-12">
|
||||
|
||||
<div *ngIf="groupDataService.getActiveGroup() | async; then editHeader; else createHeader"></div>
|
||||
|
||||
<ng-template #createHeader>
|
||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #editHeader>
|
||||
@if (activeGroup$ | async) {
|
||||
<h1 class="border-bottom pb-2">
|
||||
<span
|
||||
*dsContextHelp="{
|
||||
@@ -17,44 +11,61 @@
|
||||
iconPlacement: 'right',
|
||||
tooltipPlacement: ['right', 'bottom']
|
||||
}"
|
||||
>
|
||||
>
|
||||
{{messagePrefix + '.head.edit' | translate}}
|
||||
</span>
|
||||
</h1>
|
||||
</ng-template>
|
||||
} @else {
|
||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
|
||||
}
|
||||
|
||||
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
|
||||
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
|
||||
<ds-alert *ngIf="(canEdit$ | async) !== true && (groupDataService.getActiveGroup() | async)" [type]="AlertTypeEnum.Warning"
|
||||
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName((getLinkedDSO(groupBeingEdited) | async)?.payload), comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })">
|
||||
</ds-alert>
|
||||
|
||||
|
||||
@if ((activeGroup$ | async); as groupBeingEdited) {
|
||||
@if (groupBeingEdited?.permanent) {
|
||||
<ds-alert [type]="AlertType.Warning"
|
||||
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
|
||||
}
|
||||
@if ((activeGroupLinkedDSO$ | async); as activeGroupLinkedDSO) {
|
||||
@if ((canEdit$ | async) !== true) {
|
||||
<ds-alert [type]="AlertType.Warning"
|
||||
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName(activeGroupLinkedDSO), comcol: activeGroupLinkedDSO.type, comcolEditRolesRoute: (linkedEditRolesRoute$ | async) })">
|
||||
</ds-alert>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<ds-form [formId]="formId"
|
||||
[formModel]="formModel"
|
||||
[formGroup]="formGroup"
|
||||
[formLayout]="formLayout"
|
||||
[displayCancel]="false"
|
||||
(submitForm)="onSubmit()">
|
||||
[formModel]="formModel"
|
||||
[formGroup]="formGroup"
|
||||
[formLayout]="formLayout"
|
||||
[displayCancel]="false"
|
||||
(submitForm)="onSubmit()">
|
||||
<div before class="btn-group">
|
||||
<button (click)="onCancel()" type="button"
|
||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||
</div>
|
||||
<div after *ngIf="(canEdit$ | async) && !groupBeingEdited?.permanent" class="btn-group">
|
||||
<button (click)="delete()" class="btn btn-danger delete-button" type="button">
|
||||
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</ds-form>
|
||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||
</div>
|
||||
@if ((canEdit$ | async) && !(activeGroup$ | async)?.permanent) {
|
||||
<div after class="btn-group">
|
||||
<button (click)="delete()" class="btn btn-danger delete-button" type="button">
|
||||
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</ds-form>
|
||||
|
||||
<div class="mb-5">
|
||||
<ds-members-list *ngIf="groupBeingEdited !== undefined"
|
||||
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
||||
@if ((activeGroup$ | async); as groupBeingEdited) {
|
||||
<div class="mb-5">
|
||||
@if (groupBeingEdited !== undefined) {
|
||||
<ds-members-list
|
||||
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
||||
}
|
||||
</div>
|
||||
@if (groupBeingEdited !== undefined) {
|
||||
<ds-subgroups-list
|
||||
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<ds-subgroups-list *ngIf="groupBeingEdited !== undefined"
|
||||
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
@@ -23,11 +23,7 @@ import {
|
||||
} from '@angular/router';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Store } from '@ngrx/store';
|
||||
import {
|
||||
TranslateLoader,
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import {
|
||||
Observable,
|
||||
@@ -61,15 +57,14 @@ import { FormComponent } from '../../../shared/form/form.component';
|
||||
import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
|
||||
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
||||
import { RouterMock } from '../../../shared/mocks/router.mock';
|
||||
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||
import {
|
||||
GroupMock,
|
||||
GroupMock2,
|
||||
} from '../../../shared/testing/group-mock';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock';
|
||||
import { GroupFormComponent } from './group-form.component';
|
||||
import { MembersListComponent } from './members-list/members-list.component';
|
||||
import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component';
|
||||
@@ -78,19 +73,19 @@ import { ValidateGroupExists } from './validators/group-exists.validator';
|
||||
describe('GroupFormComponent', () => {
|
||||
let component: GroupFormComponent;
|
||||
let fixture: ComponentFixture<GroupFormComponent>;
|
||||
let translateService: TranslateService;
|
||||
let builderService: FormBuilderService;
|
||||
let ePersonDataServiceStub: any;
|
||||
let groupsDataServiceStub: any;
|
||||
let dsoDataServiceStub: any;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let notificationService: NotificationsServiceStub;
|
||||
let router;
|
||||
let router: RouterMock;
|
||||
let route: ActivatedRouteStub;
|
||||
|
||||
let groups;
|
||||
let groupName;
|
||||
let groupDescription;
|
||||
let expected;
|
||||
let groups: Group[];
|
||||
let groupName: string;
|
||||
let groupDescription: string;
|
||||
let expected: Group;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
groups = [GroupMock, GroupMock2];
|
||||
@@ -105,6 +100,15 @@ describe('GroupFormComponent', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
object: createSuccessfulRemoteDataObject$(undefined),
|
||||
_links: {
|
||||
self: {
|
||||
href: 'group-selflink',
|
||||
},
|
||||
object: {
|
||||
href: 'group-objectlink',
|
||||
},
|
||||
},
|
||||
});
|
||||
ePersonDataServiceStub = {};
|
||||
groupsDataServiceStub = {
|
||||
@@ -141,7 +145,14 @@ describe('GroupFormComponent', () => {
|
||||
create(group: Group): Observable<RemoteData<Group>> {
|
||||
this.allGroups = [...this.allGroups, group];
|
||||
this.createdGroup = Object.assign({}, group, {
|
||||
_links: { self: { href: 'group-selflink' } },
|
||||
_links: {
|
||||
self: {
|
||||
href: 'group-selflink',
|
||||
},
|
||||
object: {
|
||||
href: 'group-objectlink',
|
||||
},
|
||||
},
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(this.createdGroup);
|
||||
},
|
||||
@@ -223,17 +234,15 @@ describe('GroupFormComponent', () => {
|
||||
return typeof value === 'object' && value !== null;
|
||||
},
|
||||
});
|
||||
translateService = getMockTranslateService();
|
||||
router = new RouterMock();
|
||||
route = new ActivatedRouteStub();
|
||||
notificationService = new NotificationsServiceStub();
|
||||
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock,
|
||||
},
|
||||
}), GroupFormComponent],
|
||||
TranslateModule.forRoot(),
|
||||
GroupFormComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
@@ -249,14 +258,11 @@ describe('GroupFormComponent', () => {
|
||||
{ provide: Store, useValue: {} },
|
||||
{ provide: RemoteDataBuildService, useValue: {} },
|
||||
{ provide: HALEndpointService, useValue: {} },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) },
|
||||
},
|
||||
{ provide: ActivatedRoute, useValue: route },
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
.overrideComponent(GroupFormComponent, {
|
||||
remove: { imports: [
|
||||
@@ -279,8 +285,8 @@ describe('GroupFormComponent', () => {
|
||||
describe('when submitting the form', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(component.submitForm, 'emit');
|
||||
component.groupName.value = groupName;
|
||||
component.groupDescription.value = groupDescription;
|
||||
component.groupName.setValue(groupName);
|
||||
component.groupDescription.setValue(groupDescription);
|
||||
});
|
||||
describe('without active Group', () => {
|
||||
beforeEach(() => {
|
||||
@@ -288,14 +294,22 @@ describe('GroupFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a new group using the correct values', (async () => {
|
||||
await fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
it('should emit a new group using the correct values', (() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||
name: groupName,
|
||||
metadata: {
|
||||
'dc.description': [
|
||||
{
|
||||
value: groupDescription,
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
}));
|
||||
});
|
||||
|
||||
describe('with active Group', () => {
|
||||
let expected2;
|
||||
let expected2: Group;
|
||||
beforeEach(() => {
|
||||
expected2 = Object.assign(new Group(), {
|
||||
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, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2));
|
||||
component.groupName.value = 'newGroupName';
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
component.ngOnInit();
|
||||
});
|
||||
|
||||
it('should edit with name and description operations', () => {
|
||||
component.groupName.setValue('newGroupName');
|
||||
component.onSubmit();
|
||||
const operations = [{
|
||||
op: 'add',
|
||||
path: '/metadata/dc.description',
|
||||
@@ -328,9 +351,8 @@ describe('GroupFormComponent', () => {
|
||||
});
|
||||
|
||||
it('should edit with description operations', () => {
|
||||
component.groupName.value = null;
|
||||
component.groupName.setValue(null);
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
const operations = [{
|
||||
op: 'add',
|
||||
path: '/metadata/dc.description',
|
||||
@@ -340,9 +362,9 @@ describe('GroupFormComponent', () => {
|
||||
});
|
||||
|
||||
it('should edit with name operations', () => {
|
||||
component.groupDescription.value = null;
|
||||
component.groupName.setValue('newGroupName');
|
||||
component.groupDescription.setValue(null);
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
const operations = [{
|
||||
op: 'replace',
|
||||
path: '/name',
|
||||
@@ -351,12 +373,13 @@ describe('GroupFormComponent', () => {
|
||||
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||
});
|
||||
|
||||
it('should emit the existing group using the correct new values', (async () => {
|
||||
await fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
||||
});
|
||||
}));
|
||||
it('should emit the existing group using the correct new values', () => {
|
||||
component.onSubmit();
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
||||
});
|
||||
|
||||
it('should emit success notification', () => {
|
||||
component.onSubmit();
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -371,11 +394,8 @@ describe('GroupFormComponent', () => {
|
||||
|
||||
|
||||
describe('check form validation', () => {
|
||||
let groupCommunity;
|
||||
|
||||
beforeEach(() => {
|
||||
groupName = 'testName';
|
||||
groupCommunity = 'testgroupCommunity';
|
||||
groupDescription = 'testgroupDescription';
|
||||
|
||||
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(dsoDataServiceStub, 'findByHref').and.returnValue(observableOf(expected));
|
||||
|
||||
fixture.detectChanges();
|
||||
component.initialisePage();
|
||||
@@ -438,21 +467,20 @@ describe('GroupFormComponent', () => {
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
let deleteButton;
|
||||
let deleteButton: HTMLButtonElement;
|
||||
|
||||
beforeEach(() => {
|
||||
component.initialisePage();
|
||||
|
||||
component.canEdit$ = observableOf(true);
|
||||
component.groupBeingEdited = {
|
||||
beforeEach(async () => {
|
||||
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
||||
component.activeGroup$ = observableOf({
|
||||
id: 'active-group',
|
||||
permanent: false,
|
||||
} as Group;
|
||||
} as Group);
|
||||
component.canEdit$ = observableOf(true);
|
||||
|
||||
component.initialisePage();
|
||||
|
||||
fixture.detectChanges();
|
||||
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', () => {
|
||||
|
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
@@ -11,7 +8,10 @@ import {
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import {
|
||||
AbstractControl,
|
||||
UntypedFormGroup,
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
@@ -31,13 +31,10 @@ import { Operation } from 'fast-json-patch';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
catchError,
|
||||
debounceTime,
|
||||
filter,
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
@@ -53,7 +50,6 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { Group } from '../../../core/eperson/models/group.model';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
@@ -61,9 +57,9 @@ import { Community } from '../../../core/shared/community.model';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import {
|
||||
getAllCompletedRemoteData,
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getRemoteDataPayload,
|
||||
} from '../../../core/shared/operators';
|
||||
import { AlertComponent } from '../../../shared/alert/alert.component';
|
||||
@@ -93,7 +89,6 @@ import { ValidateGroupExists } from './validators/group-exists.validator';
|
||||
imports: [
|
||||
FormComponent,
|
||||
AlertComponent,
|
||||
NgIf,
|
||||
AsyncPipe,
|
||||
TranslateModule,
|
||||
ContextHelpDirective,
|
||||
@@ -117,9 +112,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Dynamic models for the inputs of form
|
||||
*/
|
||||
groupName: DynamicInputModel;
|
||||
groupCommunity: DynamicInputModel;
|
||||
groupDescription: DynamicTextAreaModel;
|
||||
groupName: AbstractControl;
|
||||
groupCommunity: AbstractControl;
|
||||
groupDescription: AbstractControl;
|
||||
|
||||
/**
|
||||
* A list of all dynamic input models
|
||||
@@ -162,21 +157,30 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
canEdit$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {AlertType}
|
||||
* The current {@link Group}
|
||||
*/
|
||||
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
|
||||
@@ -186,126 +190,121 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
public groupDataService: GroupDataService,
|
||||
private ePersonDataService: EPersonDataService,
|
||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private route: ActivatedRoute,
|
||||
protected dSpaceObjectDataService: DSpaceObjectDataService,
|
||||
protected formBuilderService: FormBuilderService,
|
||||
protected translateService: TranslateService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private modalService: NgbModal,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected modalService: NgbModal,
|
||||
public requestService: RequestService,
|
||||
protected changeDetectorRef: ChangeDetectorRef,
|
||||
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();
|
||||
}
|
||||
|
||||
initialisePage() {
|
||||
this.subs.push(this.route.params.subscribe((params) => {
|
||||
if (params.groupId !== 'newGroup') {
|
||||
this.setActiveGroup(params.groupId);
|
||||
}
|
||||
}));
|
||||
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
||||
hasValueOperator(),
|
||||
switchMap((group: Group) => {
|
||||
return observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined),
|
||||
this.hasLinkedDSO(group),
|
||||
]).pipe(
|
||||
map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO),
|
||||
);
|
||||
const groupNameModel = new DynamicInputModel({
|
||||
id: 'groupName',
|
||||
label: this.translateService.instant(`${this.messagePrefix}.groupName`),
|
||||
name: 'groupName',
|
||||
validators: {
|
||||
required: null,
|
||||
},
|
||||
required: true,
|
||||
});
|
||||
const groupCommunityModel = new DynamicInputModel({
|
||||
id: 'groupCommunity',
|
||||
label: this.translateService.instant(`${this.messagePrefix}.groupCommunity`),
|
||||
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 +323,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
* Emit the updated/created eperson using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe(
|
||||
(group: Group) => {
|
||||
const values = {
|
||||
this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
|
||||
if (group === null) {
|
||||
this.createNewGroup({
|
||||
name: this.groupName.value,
|
||||
metadata: {
|
||||
'dc.description': [
|
||||
@@ -335,14 +334,11 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
if (group === null) {
|
||||
this.createNewGroup(values);
|
||||
} else {
|
||||
this.editGroup(group);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.editGroup(group);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -448,7 +444,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
* @param groupSelfLink SelfLink of group to set as active
|
||||
*/
|
||||
setActiveGroupWithLink(groupSelfLink: string) {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
this.activeGroup$.pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup === null) {
|
||||
this.groupDataService.cancelEditGroup();
|
||||
this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object'))
|
||||
@@ -467,7 +463,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.
|
||||
*/
|
||||
delete() {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((group: Group) => {
|
||||
this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
|
||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||
modalRef.componentInstance.name = this.dsoNameService.getName(group);
|
||||
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header';
|
||||
@@ -511,52 +507,38 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if group has a linked object (community or collection linked to a workflow group)
|
||||
* @param group
|
||||
* Get the active {@link Group}'s linked object if it has one ({@link Community} or {@link Collection} linked to a
|
||||
* workflow group)
|
||||
*/
|
||||
hasLinkedDSO(group: Group): Observable<boolean> {
|
||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
||||
return this.getLinkedDSO(group).pipe(
|
||||
map((rd: RemoteData<DSpaceObject>) => {
|
||||
return hasValue(rd) && hasValue(rd.payload);
|
||||
}),
|
||||
catchError(() => observableOf(false)),
|
||||
);
|
||||
}
|
||||
getActiveGroupLinkedDSO(): Observable<DSpaceObject> {
|
||||
return this.activeGroup$.pipe(
|
||||
hasValueOperator(),
|
||||
switchMap((group: Group) => {
|
||||
if (group.object === undefined) {
|
||||
return this.dSpaceObjectDataService.findByHref(group._links.object.href);
|
||||
}
|
||||
return group.object;
|
||||
}),
|
||||
getAllCompletedRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get group's linked object if it has one (community or collection linked to a workflow group)
|
||||
* @param group
|
||||
* Get the route to the edit roles tab of the active {@link Group}'s linked object (community or collection linked
|
||||
* to a workflow group) if it has one
|
||||
*/
|
||||
getLinkedDSO(group: Group): Observable<RemoteData<DSpaceObject>> {
|
||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
||||
if (group.object === undefined) {
|
||||
return this.dSpaceObjectDataService.findByHref(group._links.object.href);
|
||||
}
|
||||
return group.object;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
getLinkedEditRolesRoute(): Observable<string> {
|
||||
return this.activeGroupLinkedDSO$.pipe(
|
||||
hasValueOperator(),
|
||||
map((dso: DSpaceObject) => {
|
||||
switch ((dso as any).type) {
|
||||
case Community.type.value:
|
||||
return getCommunityEditRolesRoute(dso.id);
|
||||
case Collection.type.value:
|
||||
return getCollectionEditRolesRoute(dso.id);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -3,63 +3,70 @@
|
||||
|
||||
<h3>{{messagePrefix + '.headMembers' | translate}}</h3>
|
||||
|
||||
<ds-pagination *ngIf="(ePeopleMembersOfGroup | async)?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="(ePeopleMembersOfGroup | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
@if ((ePeopleMembersOfGroup | async)?.totalElements > 0) {
|
||||
<ds-pagination
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="(ePeopleMembersOfGroup | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
|
||||
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (epersonDTO of (ePeopleMembersOfGroup | async)?.page; track epersonDTO) {
|
||||
<tr>
|
||||
<td class="align-middle">{{epersonDTO.eperson.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a [routerLink]="getEPersonEditRoute(epersonDTO.eperson.id)">
|
||||
{{ dsoNameService.getName(epersonDTO.eperson) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{{messagePrefix + '.table.email' | translate}}: {{ epersonDTO.eperson.email ? epersonDTO.eperson.email : '-' }}<br/>
|
||||
{{messagePrefix + '.table.netid' | translate}}: {{ epersonDTO.eperson.netid ? epersonDTO.eperson.netid : '-' }}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
@if (epersonDTO.ableToDelete) {
|
||||
<button (click)="deleteMemberFromGroup(epersonDTO.eperson)"
|
||||
[dsBtnDisabled]="actionConfig.remove.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
|
||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||
</button>
|
||||
}
|
||||
@if (!epersonDTO.ableToDelete) {
|
||||
<button
|
||||
(click)="addMemberToGroup(epersonDTO.eperson)"
|
||||
[dsBtnDisabled]="actionConfig.add.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
|
||||
<i [ngClass]="actionConfig.add.icon"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
|
||||
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let epersonDTO of (ePeopleMembersOfGroup | async)?.page">
|
||||
<td class="align-middle">{{epersonDTO.eperson.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a [routerLink]="getEPersonEditRoute(epersonDTO.eperson.id)">
|
||||
{{ dsoNameService.getName(epersonDTO.eperson) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{{messagePrefix + '.table.email' | translate}}: {{ epersonDTO.eperson.email ? epersonDTO.eperson.email : '-' }}<br/>
|
||||
{{messagePrefix + '.table.netid' | translate}}: {{ epersonDTO.eperson.netid ? epersonDTO.eperson.netid : '-' }}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="deleteMemberFromGroup(epersonDTO.eperson)"
|
||||
*ngIf="epersonDTO.ableToDelete"
|
||||
[disabled]="actionConfig.remove.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
|
||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||
</button>
|
||||
<button *ngIf="!epersonDTO.ableToDelete"
|
||||
(click)="addMemberToGroup(epersonDTO.eperson)"
|
||||
[disabled]="actionConfig.add.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
|
||||
<i [ngClass]="actionConfig.add.icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@if ((ePeopleMembersOfGroup | async) === undefined || (ePeopleMembersOfGroup | async)?.totalElements === 0) {
|
||||
<div class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-members-yet' | translate}}
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(ePeopleMembersOfGroup | async) === undefined || (ePeopleMembersOfGroup | async)?.totalElements === 0" class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-members-yet' | translate}}
|
||||
</div>
|
||||
}
|
||||
|
||||
<h3 id="search" class="border-bottom pb-2">
|
||||
<span
|
||||
@@ -69,77 +76,81 @@
|
||||
iconPlacement: 'right',
|
||||
tooltipPlacement: ['top', 'right', 'bottom']
|
||||
}"
|
||||
>
|
||||
>
|
||||
{{messagePrefix + '.search.head' | translate}}
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||
<div class="flex-grow-1 mr-3">
|
||||
<div class="form-group input-group mr-3">
|
||||
<div class="flex-grow-1 me-3">
|
||||
<div class="form-group input-group me-3">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" aria-label="Search input">
|
||||
class="form-control" aria-label="Search input">
|
||||
<span class="input-group-append">
|
||||
<button type="submit" class="search-button btn btn-primary">
|
||||
<i class="fas fa-search"></i> {{ messagePrefix + '.search.button' | translate }}</button>
|
||||
</span>
|
||||
<button type="submit" class="search-button btn btn-primary">
|
||||
<i class="fas fa-search"></i> {{ messagePrefix + '.search.button' | translate }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button (click)="clearFormAndResetResult();"
|
||||
class="btn btn-secondary">{{messagePrefix + '.button.see-all' | translate}}</button>
|
||||
</div>
|
||||
</form>
|
||||
<div>
|
||||
<button (click)="clearFormAndResetResult();"
|
||||
class="btn btn-secondary">{{messagePrefix + '.button.see-all' | translate}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-pagination *ngIf="(ePeopleSearch | async)?.totalElements > 0"
|
||||
[paginationOptions]="configSearch"
|
||||
[collectionSize]="(ePeopleSearch | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
@if ((ePeopleSearch | async)?.totalElements > 0) {
|
||||
<ds-pagination
|
||||
[paginationOptions]="configSearch"
|
||||
[collectionSize]="(ePeopleSearch | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="epersonsSearch" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
|
||||
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (eperson of (ePeopleSearch | async)?.page; track eperson) {
|
||||
<tr>
|
||||
<td class="align-middle">{{eperson.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a [routerLink]="getEPersonEditRoute(eperson.id)">
|
||||
{{ dsoNameService.getName(eperson) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}<br/>
|
||||
{{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="addMemberToGroup(eperson)"
|
||||
[dsBtnDisabled]="actionConfig.add.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
|
||||
<i [ngClass]="actionConfig.add.icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="epersonsSearch" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
|
||||
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let eperson of (ePeopleSearch | async)?.page">
|
||||
<td class="align-middle">{{eperson.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a [routerLink]="getEPersonEditRoute(eperson.id)">
|
||||
{{ dsoNameService.getName(eperson) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}<br/>
|
||||
{{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="addMemberToGroup(eperson)"
|
||||
[disabled]="actionConfig.add.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
|
||||
<i [ngClass]="actionConfig.add.icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if ((ePeopleSearch | async)?.totalElements === 0 && searchDone) {
|
||||
<div
|
||||
class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-items' | translate}}
|
||||
</div>
|
||||
}
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(ePeopleSearch | async)?.totalElements === 0 && searchDone"
|
||||
class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgClass,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
@@ -54,6 +52,7 @@ import {
|
||||
getFirstCompletedRemoteData,
|
||||
getRemoteDataPayload,
|
||||
} from '../../../../core/shared/operators';
|
||||
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
|
||||
import { ContextHelpDirective } from '../../../../shared/context-help.directive';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
||||
@@ -108,11 +107,10 @@ export interface EPersonListActionConfig {
|
||||
ContextHelpDirective,
|
||||
ReactiveFormsModule,
|
||||
PaginationComponent,
|
||||
NgIf,
|
||||
AsyncPipe,
|
||||
RouterLink,
|
||||
NgClass,
|
||||
NgForOf,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -3,51 +3,56 @@
|
||||
|
||||
<h4>{{messagePrefix + '.headSubgroups' | translate}}</h4>
|
||||
|
||||
<ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="(subGroups$ | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page">
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
|
||||
{{ dsoNameService.getName(group) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload)}}</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="deleteSubgroupFromGroup(group)"
|
||||
@if ((subGroups$ | async)?.payload?.totalElements > 0) {
|
||||
<ds-pagination
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="(subGroups$ | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (group of (subGroups$ | async)?.payload?.page; track group) {
|
||||
<tr>
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
|
||||
{{ dsoNameService.getName(group) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload)}}</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="deleteSubgroupFromGroup(group)"
|
||||
class="btn btn-outline-danger btn-sm deleteButton"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
}
|
||||
|
||||
<div *ngIf="(subGroups$ | async)?.payload?.totalElements === 0" class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-subgroups-yet' | translate}}
|
||||
</div>
|
||||
@if ((subGroups$ | async)?.payload?.totalElements === 0) {
|
||||
<div class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-subgroups-yet' | translate}}
|
||||
</div>
|
||||
}
|
||||
|
||||
<h4 id="search" class="border-bottom pb-2">
|
||||
<span *dsContextHelp="{
|
||||
@@ -56,75 +61,80 @@
|
||||
iconPlacement: 'right',
|
||||
tooltipPlacement: ['top', 'right', 'bottom']
|
||||
}"
|
||||
>
|
||||
>
|
||||
{{messagePrefix + '.search.head' | translate}}
|
||||
</span>
|
||||
|
||||
</h4>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||
<div class="flex-grow-1 mr-3">
|
||||
<div class="form-group input-group mr-3">
|
||||
<div class="flex-grow-1 me-3">
|
||||
<div class="mb-3 input-group me-3">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" aria-label="Search input">
|
||||
class="form-control" aria-label="Search input">
|
||||
<span class="input-group-append">
|
||||
<button type="submit" class="search-button btn btn-primary">
|
||||
<i class="fas fa-search"></i> {{ messagePrefix + '.search.button' | translate }}
|
||||
</button>
|
||||
<button type="submit" class="search-button btn btn-primary">
|
||||
<i class="fas fa-search"></i> {{ messagePrefix + '.search.button' | translate }}
|
||||
</button>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button (click)="clearFormAndResetResult();" class="btn btn-secondary float-right">
|
||||
<button (click)="clearFormAndResetResult();" class="btn btn-secondary float-end">
|
||||
{{messagePrefix + '.button.see-all' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-pagination *ngIf="(searchResults$ | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="configSearch"
|
||||
[collectionSize]="(searchResults$ | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="groupsSearch" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (searchResults$ | async)?.payload?.page">
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
|
||||
{{ dsoNameService.getName(group) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload) }}</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="addSubgroupToGroup(group)"
|
||||
@if ((searchResults$ | async)?.payload?.totalElements > 0) {
|
||||
<ds-pagination
|
||||
[paginationOptions]="configSearch"
|
||||
[collectionSize]="(searchResults$ | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="groupsSearch" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (group of (searchResults$ | async)?.payload?.page; track group) {
|
||||
<tr>
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
|
||||
{{ dsoNameService.getName(group) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload) }}</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="addSubgroupToGroup(group)"
|
||||
class="btn btn-outline-primary btn-sm addButton"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(group) } }}">
|
||||
<i class="fas fa-plus fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
<i class="fas fa-plus fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
}
|
||||
|
||||
<div *ngIf="(searchResults$ | async)?.payload?.totalElements === 0 && searchDone" class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-items' | translate}}
|
||||
</div>
|
||||
@if ((searchResults$ | async)?.payload?.totalElements === 0 && searchDone) {
|
||||
<div class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-items' | translate}}
|
||||
</div>
|
||||
}
|
||||
|
||||
</ng-container>
|
||||
|
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
@@ -65,12 +61,10 @@ enum SubKey {
|
||||
imports: [
|
||||
RouterLink,
|
||||
AsyncPipe,
|
||||
NgForOf,
|
||||
ContextHelpDirective,
|
||||
TranslateModule,
|
||||
ReactiveFormsModule,
|
||||
PaginationComponent,
|
||||
NgIf,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -4,21 +4,21 @@
|
||||
<div class="d-flex justify-content-between border-bottom mb-3">
|
||||
<h1 id="header" class="pb-2">{{messagePrefix + 'head' | translate}}</h1>
|
||||
<div>
|
||||
<button class="mr-auto btn btn-success"
|
||||
[routerLink]="'create'">
|
||||
<button class="me-auto btn btn-success"
|
||||
[routerLink]="'create'">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline ml-1">{{messagePrefix + 'button.add' | translate}}</span>
|
||||
<span class="d-none d-sm-inline ms-1">{{messagePrefix + 'button.add' | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 id="search" class="border-bottom pb-2">{{messagePrefix + 'search.head' | translate}}</h2>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||
<div class="flex-grow-1 mr-3">
|
||||
<div class="form-group input-group">
|
||||
<div class="flex-grow-1 me-3">
|
||||
<div class="mb-3 input-group">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" [attr.aria-label]="messagePrefix + 'search.placeholder' | translate"
|
||||
[placeholder]="(messagePrefix + 'search.placeholder' | translate)" >
|
||||
class="form-control" [attr.aria-label]="messagePrefix + 'search.placeholder' | translate"
|
||||
[placeholder]="(messagePrefix + 'search.placeholder' | translate)" >
|
||||
<span class="input-group-append">
|
||||
<button type="submit" class="search-button btn btn-primary">
|
||||
<i class="fas fa-search"></i> {{ messagePrefix + 'search.button' | translate }}
|
||||
@@ -33,66 +33,78 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-loading *ngIf="loading$ | async"></ds-loading>
|
||||
<ds-pagination
|
||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (loading$ | async) !== true"
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
@if (loading$ | async) {
|
||||
<ds-loading></ds-loading>
|
||||
}
|
||||
@if ((pageInfoState$ | async)?.totalElements > 0 && (loading$ | async) !== true) {
|
||||
<ds-pagination
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="groups" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + 'table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + 'table.name' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + 'table.collectionOrCommunity' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + 'table.members' | translate}}</th>
|
||||
<th>{{messagePrefix + 'table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (groupDto of (groupsDto$ | async)?.page; track groupDto) {
|
||||
<tr>
|
||||
<td>{{groupDto.group.id}}</td>
|
||||
<td>{{ dsoNameService.getName(groupDto.group) }}</td>
|
||||
<td>{{ dsoNameService.getName((groupDto.group.object | async)?.payload) }}</td>
|
||||
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
@switch (groupDto.ableToEdit) {
|
||||
@case (true) {
|
||||
<button
|
||||
[routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
|
||||
class="btn btn-outline-primary btn-sm btn-edit"
|
||||
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: dsoNameService.getName(groupDto.group) } }}"
|
||||
>
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
}
|
||||
@case (false) {
|
||||
<button
|
||||
[dsBtnDisabled]="true"
|
||||
class="btn btn-outline-primary btn-sm btn-edit"
|
||||
placement="left"
|
||||
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"
|
||||
>
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@if (!groupDto.group?.permanent && groupDto.ableToDelete) {
|
||||
<button
|
||||
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm btn-delete"
|
||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: dsoNameService.getName(groupDto.group) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="groups" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + 'table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + 'table.name' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + 'table.collectionOrCommunity' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + 'table.members' | translate}}</th>
|
||||
<th>{{messagePrefix + 'table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let groupDto of (groupsDto$ | async)?.page">
|
||||
<td>{{groupDto.group.id}}</td>
|
||||
<td>{{ dsoNameService.getName(groupDto.group) }}</td>
|
||||
<td>{{ dsoNameService.getName((groupDto.group.object | async)?.payload) }}</td>
|
||||
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<ng-container [ngSwitch]="groupDto.ableToEdit">
|
||||
<button *ngSwitchCase="true"
|
||||
[routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
|
||||
class="btn btn-outline-primary btn-sm btn-edit"
|
||||
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: dsoNameService.getName(groupDto.group) } }}"
|
||||
>
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
<button *ngSwitchCase="false"
|
||||
[disabled]="true"
|
||||
class="btn btn-outline-primary btn-sm btn-edit"
|
||||
placement="left"
|
||||
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"
|
||||
>
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
</ng-container>
|
||||
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
|
||||
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm btn-delete"
|
||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: dsoNameService.getName(groupDto.group) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@if ((pageInfoState$ | async)?.totalElements === 0) {
|
||||
<div class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{messagePrefix + 'no-items' | translate}}
|
||||
</div>
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(pageInfoState$ | async)?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{messagePrefix + 'no-items' | translate}}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -50,6 +50,7 @@ import { RouteService } from '../../core/services/route.service';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { NoContent } from '../../core/shared/NoContent.model';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import {
|
||||
DSONameServiceMock,
|
||||
UNDEFINED_NAME,
|
||||
@@ -208,6 +209,7 @@ describe('GroupsRegistryComponent', () => {
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot(),
|
||||
GroupsRegistryComponent,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
providers: [GroupsRegistryComponent,
|
||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||
@@ -278,7 +280,8 @@ describe('GroupsRegistryComponent', () => {
|
||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||
expect(editButtonsFound.length).toEqual(2);
|
||||
editButtonsFound.forEach((editButtonFound) => {
|
||||
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
||||
expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBeNull();
|
||||
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -312,7 +315,8 @@ describe('GroupsRegistryComponent', () => {
|
||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||
expect(editButtonsFound.length).toEqual(2);
|
||||
editButtonsFound.forEach((editButtonFound) => {
|
||||
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
||||
expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBeNull();
|
||||
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -331,7 +335,8 @@ describe('GroupsRegistryComponent', () => {
|
||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||
expect(editButtonsFound.length).toEqual(2);
|
||||
editButtonsFound.forEach((editButtonFound) => {
|
||||
expect(editButtonFound.nativeElement.disabled).toBeTrue();
|
||||
expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeTrue();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
NgSwitch,
|
||||
NgSwitchCase,
|
||||
} from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
OnDestroy,
|
||||
@@ -62,6 +56,7 @@ import {
|
||||
getRemoteDataPayload,
|
||||
} from '../../core/shared/operators';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
@@ -78,12 +73,9 @@ import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
RouterLink,
|
||||
ReactiveFormsModule,
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
PaginationComponent,
|
||||
NgSwitch,
|
||||
NgSwitchCase,
|
||||
NgbTooltipModule,
|
||||
NgForOf,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -1,14 +1,16 @@
|
||||
<div class="container">
|
||||
<h1 id="header">{{'admin.batch-import.page.header' | translate}}</h1>
|
||||
<p>{{'admin.batch-import.page.help' | translate}}</p>
|
||||
<p *ngIf="dso">
|
||||
selected collection: <b>{{getDspaceObjectName()}}</b>
|
||||
<a href="javascript:void(0)" (click)="removeDspaceObject()">{{'admin.batch-import.page.remove' | translate}}</a>
|
||||
</p>
|
||||
@if (dso) {
|
||||
<p>
|
||||
selected collection: <b>{{getDspaceObjectName()}}</b>
|
||||
<a href="javascript:void(0)" (click)="removeDspaceObject()">{{'admin.batch-import.page.remove' | translate}}</a>
|
||||
</p>
|
||||
}
|
||||
<p>
|
||||
<button class="btn btn-primary" (click)="this.selectCollection();">{{'admin.metadata-import.page.button.select-collection' | translate}}</button>
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
|
||||
<label class="form-check-label" for="validateOnly">
|
||||
@@ -21,32 +23,35 @@
|
||||
</div>
|
||||
|
||||
<ui-switch color="#ebebeb"
|
||||
[checkedLabel]="'admin.metadata-import.page.toggle.upload' | translate"
|
||||
[uncheckedLabel]="'admin.metadata-import.page.toggle.url' | translate"
|
||||
[checked]="isUpload"
|
||||
(change)="toggleUpload()" ></ui-switch>
|
||||
<small class="form-text text-muted">
|
||||
[checkedLabel]="'admin.metadata-import.page.toggle.upload' | translate"
|
||||
[uncheckedLabel]="'admin.metadata-import.page.toggle.url' | translate"
|
||||
[checked]="isUpload"
|
||||
(change)="toggleUpload()" ></ui-switch>
|
||||
<small class="form-text text-muted d-block">
|
||||
{{'admin.batch-import.page.toggle.help' | translate}}
|
||||
</small>
|
||||
|
||||
|
||||
<ds-file-dropzone-no-uploader
|
||||
*ngIf="isUpload"
|
||||
data-test="file-dropzone"
|
||||
(onFileAdded)="setFile($event)"
|
||||
[dropMessageLabel]="'admin.batch-import.page.dropMsg'"
|
||||
[dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'">
|
||||
</ds-file-dropzone-no-uploader>
|
||||
@if (isUpload) {
|
||||
<ds-file-dropzone-no-uploader
|
||||
data-test="file-dropzone"
|
||||
(onFileAdded)="setFile($event)"
|
||||
[dropMessageLabel]="'admin.batch-import.page.dropMsg'"
|
||||
[dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'">
|
||||
</ds-file-dropzone-no-uploader>
|
||||
}
|
||||
|
||||
<div class="form-group mt-2" *ngIf="!isUpload">
|
||||
<input class="form-control" type="text" placeholder="{{'admin.metadata-import.page.urlMsg' | translate}}"
|
||||
data-test="file-url-input" [(ngModel)]="fileURL">
|
||||
</div>
|
||||
@if (!isUpload) {
|
||||
<div class="mb-3 mt-2">
|
||||
<input class="form-control" type="text" placeholder="{{'admin.metadata-import.page.urlMsg' | translate}}"
|
||||
data-test="file-url-input" [(ngModel)]="fileURL">
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="space-children-mr">
|
||||
<button class="btn btn-secondary" id="backButton"
|
||||
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
|
||||
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
|
||||
<button class="btn btn-primary" id="proceedButton"
|
||||
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
|
||||
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
Location,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import { Location } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
@@ -36,7 +33,6 @@ import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzo
|
||||
selector: 'ds-batch-import-page',
|
||||
templateUrl: './batch-import-page.component.html',
|
||||
imports: [
|
||||
NgIf,
|
||||
TranslateModule,
|
||||
FormsModule,
|
||||
UiSwitchModule,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<h1 id="header">{{'admin.metadata-import.page.header' | translate}}</h1>
|
||||
<p>{{'admin.metadata-import.page.help' | translate}}</p>
|
||||
<div class="form-group">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
|
||||
<label class="form-check-label" for="validateOnly">
|
||||
|
@@ -4,66 +4,74 @@
|
||||
<h1 class="flex-grow-1">{{ isNewService ? ('ldn-create-service.title' | translate) : ('ldn-edit-registered-service.title' | translate) }}</h1>
|
||||
</div>
|
||||
<!-- In the toggle section -->
|
||||
<div class="toggle-switch-container" *ngIf="!isNewService">
|
||||
<label class="status-label font-weight-bold" for="enabled">{{ 'ldn-service-status' | translate }}</label>
|
||||
<div>
|
||||
<input formControlName="enabled" hidden id="enabled" name="enabled" type="checkbox">
|
||||
<div (click)="toggleEnabled()" [class.checked]="formModel.get('enabled').value" class="toggle-switch">
|
||||
<div class="slider"></div>
|
||||
@if (!isNewService) {
|
||||
<div class="toggle-switch-container">
|
||||
<label class="status-label font-weight-bold" for="enabled">{{ 'ldn-service-status' | translate }}</label>
|
||||
<div>
|
||||
<input formControlName="enabled" hidden id="enabled" name="enabled" type="checkbox">
|
||||
<div (click)="toggleEnabled()" [class.checked]="formModel.get('enabled').value" class="toggle-switch">
|
||||
<div class="slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- In the Name section -->
|
||||
<div class="mb-5">
|
||||
<label for="name" class="font-weight-bold">{{ 'ldn-new-service.form.label.name' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('name').invalid && formModel.get('name').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.name' | translate" class="form-control"
|
||||
formControlName="name"
|
||||
id="name"
|
||||
name="name"
|
||||
type="text">
|
||||
<div *ngIf="formModel.get('name').invalid && formModel.get('name').touched" class="error-text">
|
||||
{{ 'ldn-new-service.form.error.name' | translate }}
|
||||
</div>
|
||||
[placeholder]="'ldn-new-service.form.placeholder.name' | translate" class="form-control"
|
||||
formControlName="name"
|
||||
id="name"
|
||||
name="name"
|
||||
type="text">
|
||||
@if (formModel.get('name').invalid && formModel.get('name').touched) {
|
||||
<div class="error-text">
|
||||
{{ 'ldn-new-service.form.error.name' | translate }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- In the description section -->
|
||||
<div class="mb-5 mt-5 d-flex flex-column">
|
||||
<label for="description" class="font-weight-bold">{{ 'ldn-new-service.form.label.description' | translate }}</label>
|
||||
<textarea [placeholder]="'ldn-new-service.form.placeholder.description' | translate"
|
||||
class="form-control" formControlName="description" id="description" name="description"></textarea>
|
||||
class="form-control" formControlName="description" id="description" name="description"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-5 mt-5">
|
||||
<!-- In the url section -->
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex flex-column w-50 mr-2">
|
||||
<div class="d-flex flex-column w-50 me-2">
|
||||
<label for="url" class="font-weight-bold">{{ 'ldn-new-service.form.label.url' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('url').invalid && formModel.get('url').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.url' | translate" class="form-control"
|
||||
formControlName="url"
|
||||
id="url"
|
||||
name="url"
|
||||
type="text">
|
||||
<div *ngIf="formModel.get('url').invalid && formModel.get('url').touched" class="error-text">
|
||||
{{ 'ldn-new-service.form.error.url' | translate }}
|
||||
</div>
|
||||
[placeholder]="'ldn-new-service.form.placeholder.url' | translate" class="form-control"
|
||||
formControlName="url"
|
||||
id="url"
|
||||
name="url"
|
||||
type="text">
|
||||
@if (formModel.get('url').invalid && formModel.get('url').touched) {
|
||||
<div class="error-text">
|
||||
{{ 'ldn-new-service.form.error.url' | translate }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column w-50">
|
||||
<label for="score" class="font-weight-bold">{{ 'ldn-new-service.form.label.score' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('score').invalid && formModel.get('score').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.score' | translate" formControlName="score"
|
||||
id="score"
|
||||
name="score"
|
||||
min="0"
|
||||
max="1"
|
||||
step=".01"
|
||||
class="form-control"
|
||||
type="number">
|
||||
<div *ngIf="formModel.get('score').invalid && formModel.get('score').touched" class="error-text">
|
||||
{{ 'ldn-new-service.form.error.score' | translate }}
|
||||
</div>
|
||||
[placeholder]="'ldn-new-service.form.placeholder.score' | translate" formControlName="score"
|
||||
id="score"
|
||||
name="score"
|
||||
min="0"
|
||||
max="1"
|
||||
step=".01"
|
||||
class="form-control"
|
||||
type="number">
|
||||
@if (formModel.get('score').invalid && formModel.get('score').touched) {
|
||||
<div class="error-text">
|
||||
{{ 'ldn-new-service.form.error.score' | translate }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,21 +81,23 @@
|
||||
<label for="lowerIp" class="font-weight-bold">{{ 'ldn-new-service.form.label.ip-range' | translate }}</label>
|
||||
<div class="d-flex">
|
||||
<input [class.invalid-field]="formModel.get('lowerIp').invalid && formModel.get('lowerIp').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.lowerIp' | translate" class="form-control mr-2"
|
||||
formControlName="lowerIp"
|
||||
id="lowerIp"
|
||||
name="lowerIp"
|
||||
type="text">
|
||||
[placeholder]="'ldn-new-service.form.placeholder.lowerIp' | translate" class="form-control me-2"
|
||||
formControlName="lowerIp"
|
||||
id="lowerIp"
|
||||
name="lowerIp"
|
||||
type="text">
|
||||
<input [class.invalid-field]="formModel.get('upperIp').invalid && formModel.get('upperIp').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.upperIp' | translate" class="form-control"
|
||||
formControlName="upperIp"
|
||||
id="upperIp"
|
||||
name="upperIp"
|
||||
type="text">
|
||||
</div>
|
||||
<div *ngIf="(formModel.get('lowerIp').invalid && formModel.get('lowerIp').touched) || (formModel.get('upperIp').invalid && formModel.get('upperIp').touched)" class="error-text">
|
||||
{{ 'ldn-new-service.form.error.ipRange' | translate }}
|
||||
[placeholder]="'ldn-new-service.form.placeholder.upperIp' | translate" class="form-control"
|
||||
formControlName="upperIp"
|
||||
id="upperIp"
|
||||
name="upperIp"
|
||||
type="text">
|
||||
</div>
|
||||
@if ((formModel.get('lowerIp').invalid && formModel.get('lowerIp').touched) || (formModel.get('upperIp').invalid && formModel.get('upperIp').touched)) {
|
||||
<div class="error-text">
|
||||
{{ 'ldn-new-service.form.error.ipRange' | translate }}
|
||||
</div>
|
||||
}
|
||||
<div class="text-muted">
|
||||
{{ 'ldn-new-service.form.hint.ipRange' | translate }}
|
||||
</div>
|
||||
@@ -97,223 +107,258 @@
|
||||
<div class="mb-5 mt-5">
|
||||
<label for="ldnUrl" class="font-weight-bold">{{ 'ldn-new-service.form.label.ldnUrl' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.ldnUrl' | translate" class="form-control"
|
||||
formControlName="ldnUrl"
|
||||
id="ldnUrl"
|
||||
name="ldnUrl"
|
||||
type="text">
|
||||
<div *ngIf="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched" >
|
||||
<div *ngIf="formModel.get('ldnUrl').errors['required']" class="error-text">
|
||||
{{ 'ldn-new-service.form.error.ldnurl' | translate }}
|
||||
[placeholder]="'ldn-new-service.form.placeholder.ldnUrl' | translate" class="form-control"
|
||||
formControlName="ldnUrl"
|
||||
id="ldnUrl"
|
||||
name="ldnUrl"
|
||||
type="text">
|
||||
@if (formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched) {
|
||||
<div >
|
||||
@if (formModel.get('ldnUrl').errors['required']) {
|
||||
<div class="error-text">
|
||||
{{ 'ldn-new-service.form.error.ldnurl' | translate }}
|
||||
</div>
|
||||
}
|
||||
@if (formModel.get('ldnUrl').errors['ldnUrlAlreadyAssociated']) {
|
||||
<div class="error-text">
|
||||
{{ 'ldn-new-service.form.error.ldnurl.ldnUrlAlreadyAssociated' | translate }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div *ngIf="formModel.get('ldnUrl').errors['ldnUrlAlreadyAssociated']" class="error-text">
|
||||
{{ 'ldn-new-service.form.error.ldnurl.ldnUrlAlreadyAssociated' | translate }}
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- In the usesActorEmailId section -->
|
||||
<div class="mb-5 mt-5">
|
||||
<label class="status-label font-weight-bold" for="usesActorEmailId">{{ 'ldn-service-usesActorEmailId' | translate }}</label>
|
||||
<div>
|
||||
<input formControlName="usesActorEmailId" hidden id="usesActorEmailId"
|
||||
name="usesActorEmailId" type="checkbox">
|
||||
<div (click)="toggleUsesActorEmailId()"
|
||||
[class.checked]="formModel.get('usesActorEmailId').value" class="toggle-switch">
|
||||
<div class="slider"></div>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
{{ 'ldn-service-usesActorEmailId-description' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- In the Inbound Patterns Labels section -->
|
||||
<div class="row mb-1 mt-5" *ngIf="areControlsInitialized">
|
||||
<div class="col">
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
|
||||
</div>
|
||||
<ng-container *ngIf="formModel.get('notifyServiceInboundPatterns')['controls'][0]?.value?.pattern">
|
||||
@if (areControlsInitialized) {
|
||||
<div class="row mb-1 mt-5">
|
||||
<div class="col">
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
|
||||
@if (formModel.get('notifyServiceInboundPatterns')['controls'][0]?.value?.pattern) {
|
||||
<div class="col">
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
|
||||
</div>
|
||||
}
|
||||
<div class="col-sm-2">
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="col-sm-2">
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- In the Inbound Patterns section -->
|
||||
<div *ngIf="areControlsInitialized">
|
||||
<div *ngFor="let patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; let i = index"
|
||||
[class.marked-for-deletion]="markedForDeletionInboundPattern.includes(i)"
|
||||
formGroupName="notifyServiceInboundPatterns">
|
||||
|
||||
<ng-container [formGroupName]="i">
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-center">
|
||||
<div class="col">
|
||||
<div #inboundPatternDropdown="ngbDropdown" class="w-80" display="dynamic"
|
||||
id="additionalInboundPattern{{i}}"
|
||||
ngbDropdown placement="top-start">
|
||||
<div class="position-relative right-addon" role="combobox" aria-expanded="false" aria-controls="inboundPatternDropdownButton">
|
||||
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
|
||||
ngbDropdownToggle></i>
|
||||
<input
|
||||
(click)="inboundPatternDropdown.open();"
|
||||
[readonly]="true"
|
||||
[value]="selectedInboundPatterns"
|
||||
class="form-control w-80 scrollable-dropdown-input"
|
||||
formControlName="patternLabel"
|
||||
id="inboundPatternDropdownButton"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
[attr.aria-label]="'ldn-service-input-inbound-pattern-dropdown' | translate"
|
||||
/>
|
||||
<div aria-labelledby="inboundPatternDropdownButton"
|
||||
class="dropdown-menu dropdown-menu-top w-100 "
|
||||
ngbDropdownMenu>
|
||||
<div class="scrollable-menu" role="listbox">
|
||||
<button (click)="selectInboundPattern(pattern, i); $event.stopPropagation()"
|
||||
*ngFor="let pattern of inboundPatterns; let internalIndex = index"
|
||||
@if (areControlsInitialized) {
|
||||
<div>
|
||||
@for (patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; track patternGroup; let i = $index) {
|
||||
<div
|
||||
[class.marked-for-deletion]="markedForDeletionInboundPattern.includes(i)"
|
||||
formGroupName="notifyServiceInboundPatterns">
|
||||
<ng-container [formGroupName]="i">
|
||||
<div class="row mb-1 align-items-center">
|
||||
<div class="col">
|
||||
<div #inboundPatternDropdown="ngbDropdown" class="w-80" display="dynamic"
|
||||
id="additionalInboundPattern{{i}}"
|
||||
ngbDropdown placement="top-start">
|
||||
<div class="position-relative right-addon" role="combobox" aria-expanded="false" aria-controls="inboundPatternDropdownButton">
|
||||
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
|
||||
ngbDropdownToggle></i>
|
||||
<input
|
||||
(click)="inboundPatternDropdown.open();"
|
||||
[readonly]="true"
|
||||
[value]="selectedInboundPatterns"
|
||||
class="form-control w-80 scrollable-dropdown-input"
|
||||
formControlName="patternLabel"
|
||||
id="inboundPatternDropdownButton"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
[attr.aria-label]="'ldn-service-input-inbound-pattern-dropdown' | translate"
|
||||
/>
|
||||
<div aria-labelledby="inboundPatternDropdownButton"
|
||||
class="dropdown-menu dropdown-menu-top w-100 "
|
||||
ngbDropdownMenu>
|
||||
<div class="scrollable-menu" role="listbox">
|
||||
@for (pattern of inboundPatterns; track pattern; let internalIndex = $index) {
|
||||
<button (click)="selectInboundPattern(pattern, i); $event.stopPropagation()"
|
||||
[title]="'ldn-service.form.pattern.' + pattern + '.description' | translate"
|
||||
class="dropdown-item collection-item text-truncate w-100"
|
||||
ngbDropdownItem
|
||||
type="button">
|
||||
<div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<ng-container
|
||||
*ngIf="formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern">
|
||||
<div #inboundItemfilterDropdown="ngbDropdown" class="w-100" id="constraint{{i}}" ngbDropdown
|
||||
placement="top-start">
|
||||
<div class="position-relative right-addon" aria-expanded="false" aria-controls="inboundItemfilterDropdown" role="combobox">
|
||||
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
|
||||
ngbDropdownToggle></i>
|
||||
<input
|
||||
[readonly]="true"
|
||||
class="form-control d-none w-100 scrollable-dropdown-input"
|
||||
formControlName="constraint"
|
||||
id="inboundItemfilterDropdown"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
[attr.aria-label]="'ldn-service-input-inbound-item-filter-dropdown' | translate"
|
||||
/>
|
||||
<input
|
||||
(click)="inboundItemfilterDropdown.open();"
|
||||
[readonly]="true"
|
||||
class="form-control w-100 scrollable-dropdown-input"
|
||||
formControlName="constraintFormatted"
|
||||
id="inboundItemfilterDropdownPrettified"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
[attr.aria-label]="'ldn-service-input-inbound-item-filter-dropdown' | translate"
|
||||
/>
|
||||
<div aria-labelledby="inboundItemfilterDropdownButton"
|
||||
class="dropdown-menu scrollable-dropdown-menu w-100 "
|
||||
ngbDropdownMenu>
|
||||
<div class="scrollable-menu" role="listbox">
|
||||
<button (click)="selectInboundItemFilter('', i); $event.stopPropagation()"
|
||||
class="dropdown-item collection-item text-truncate w-100" ngbDropdownItem type="button">
|
||||
<span> {{'ldn-service.control-constaint-select-none' | translate}} </span>
|
||||
</button>
|
||||
<button (click)="selectInboundItemFilter(constraint.id, i); $event.stopPropagation()"
|
||||
*ngFor="let constraint of (itemFiltersRD$ | async)?.payload?.page; let internalIndex = index"
|
||||
class="dropdown-item collection-item text-truncate w-100"
|
||||
ngbDropdownItem
|
||||
type="button">
|
||||
<div>{{ constraint.id + '.label' | translate }}</div>
|
||||
</button>
|
||||
<div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div
|
||||
[style.visibility]="formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern ? 'visible' : 'hidden'"
|
||||
class="col-sm-1">
|
||||
<input formControlName="automatic" hidden id="automatic{{i}}" name="automatic{{i}}"
|
||||
type="checkbox">
|
||||
<div (click)="toggleAutomatic(i)"
|
||||
[class.checked]="formModel.get('notifyServiceInboundPatterns.' + i + '.automatic').value"
|
||||
class="toggle-switch">
|
||||
<div class="slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="btn-group">
|
||||
<button (click)="markForInboundPatternDeletion(i)" class="btn btn-outline-dark trash-button"
|
||||
[title]="'ldn-service-button-mark-inbound-deletion' | translate"
|
||||
type="button">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
|
||||
|
||||
<button (click)="unmarkForInboundPatternDeletion(i)"
|
||||
*ngIf="markedForDeletionInboundPattern.includes(i)"
|
||||
<div class="col">
|
||||
@if (formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern) {
|
||||
<div #inboundItemfilterDropdown="ngbDropdown" class="w-100" id="constraint{{i}}" ngbDropdown
|
||||
placement="top-start">
|
||||
<div class="position-relative right-addon" aria-expanded="false" aria-controls="inboundItemfilterDropdown" role="combobox">
|
||||
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
|
||||
ngbDropdownToggle></i>
|
||||
<input
|
||||
[readonly]="true"
|
||||
class="form-control d-none w-100 scrollable-dropdown-input"
|
||||
formControlName="constraint"
|
||||
id="inboundItemfilterDropdown"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
[attr.aria-label]="'ldn-service-input-inbound-item-filter-dropdown' | translate"
|
||||
/>
|
||||
<input
|
||||
(click)="inboundItemfilterDropdown.open();"
|
||||
[readonly]="true"
|
||||
class="form-control w-100 scrollable-dropdown-input"
|
||||
formControlName="constraintFormatted"
|
||||
id="inboundItemfilterDropdownPrettified"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
[attr.aria-label]="'ldn-service-input-inbound-item-filter-dropdown' | translate"
|
||||
/>
|
||||
<div aria-labelledby="inboundItemfilterDropdownButton"
|
||||
class="dropdown-menu scrollable-dropdown-menu w-100 "
|
||||
ngbDropdownMenu>
|
||||
<div class="scrollable-menu" role="listbox">
|
||||
<button (click)="selectInboundItemFilter('', i); $event.stopPropagation()"
|
||||
class="dropdown-item collection-item text-truncate w-100" ngbDropdownItem type="button">
|
||||
<span> {{'ldn-service.control-constaint-select-none' | translate}} </span>
|
||||
</button>
|
||||
@for (constraint of (itemFiltersRD$ | async)?.payload?.page; track constraint; let internalIndex = $index) {
|
||||
<button (click)="selectInboundItemFilter(constraint.id, i); $event.stopPropagation()"
|
||||
class="dropdown-item collection-item text-truncate w-100"
|
||||
ngbDropdownItem
|
||||
type="button">
|
||||
<div>{{ constraint.id + '.label' | translate }}</div>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
[style.visibility]="formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern ? 'visible' : 'hidden'"
|
||||
class="col-sm-1">
|
||||
<input formControlName="automatic" hidden id="automatic{{i}}" name="automatic{{i}}"
|
||||
type="checkbox">
|
||||
<div (click)="toggleAutomatic(i)"
|
||||
[class.checked]="formModel.get('notifyServiceInboundPatterns.' + i + '.automatic').value"
|
||||
class="toggle-switch">
|
||||
<div class="slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<div class="btn-group">
|
||||
<button (click)="markForInboundPatternDeletion(i)" class="btn btn-outline-dark trash-button"
|
||||
[title]="'ldn-service-button-mark-inbound-deletion' | translate"
|
||||
type="button">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
@if (markedForDeletionInboundPattern.includes(i)) {
|
||||
<button (click)="unmarkForInboundPatternDeletion(i)"
|
||||
[title]="'ldn-service-button-unmark-inbound-deletion' | translate"
|
||||
class="btn btn-warning "
|
||||
type="button">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<span (click)="addInboundPattern()"
|
||||
class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span>
|
||||
class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span>
|
||||
<hr>
|
||||
<div class="form-group row">
|
||||
<div class="col text-right space-children-mr">
|
||||
<ng-content select="[before]"></ng-content>
|
||||
<button (click)="resetFormAndLeave()" class="btn btn-outline-secondary" type="button">
|
||||
<span> {{ 'submission.general.back.submit' | translate }}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
<span><i class="fas fa-save"></i> {{ 'ldn-new-service.form.label.submit' | translate }}</span>
|
||||
</button>
|
||||
<div class="form-group row">
|
||||
<div class="col text-right space-children-mr">
|
||||
<ng-content select="[before]"></ng-content>
|
||||
<button (click)="resetFormAndLeave()" class="btn btn-outline-secondary" type="button">
|
||||
<span> {{ 'submission.general.back.submit' | translate }}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
<span><i class="fas fa-save"></i> {{ 'ldn-new-service.form.label.submit' | translate }}</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<ng-template #confirmModal>
|
||||
</form>
|
||||
</div>
|
||||
<ng-template #confirmModal>
|
||||
<div class="modal-header">
|
||||
<h4 *ngIf="!isNewService">{{'service.overview.edit.modal' | translate }}</h4>
|
||||
<h4 *ngIf="isNewService">{{'service.overview.create.modal' | translate }}</h4>
|
||||
@if (!isNewService) {
|
||||
<h4>{{'service.overview.edit.modal' | translate }}</h4>
|
||||
}
|
||||
@if (isNewService) {
|
||||
<h4>{{'service.overview.create.modal' | translate }}</h4>
|
||||
}
|
||||
<button (click)="closeModal()" aria-label="Close"
|
||||
class="close" type="button">
|
||||
class="close" type="button">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div *ngIf="!isNewService">
|
||||
{{ 'service.overview.edit.body' | translate }}
|
||||
</div>
|
||||
<span *ngIf="isNewService">
|
||||
{{ 'service.overview.create.body' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div *ngIf="!isNewService">
|
||||
<button (click)="closeModal()" class="btn btn-outline-secondary mr-2"
|
||||
id="delete-confirm-edit">{{ 'service.detail.return' | translate }}
|
||||
</button>
|
||||
<button *ngIf="!isNewService" (click)="patchService()"
|
||||
class="btn btn-primary">{{ 'service.detail.update' | translate }}
|
||||
</button>
|
||||
@if (!isNewService) {
|
||||
<div>
|
||||
{{ 'service.overview.edit.body' | translate }}
|
||||
</div>
|
||||
<div *ngIf="isNewService">
|
||||
<button (click)="closeModal()" class="btn btn-outline-secondary mr-2 "
|
||||
id="delete-confirm-new">{{ 'service.refuse.create' | translate }}
|
||||
}
|
||||
@if (isNewService) {
|
||||
<span>
|
||||
{{ 'service.overview.create.body' | translate }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@if (!isNewService) {
|
||||
<div>
|
||||
<button (click)="closeModal()" class="btn btn-outline-secondary me-2"
|
||||
id="delete-confirm-edit">{{ 'service.detail.return' | translate }}
|
||||
</button>
|
||||
@if (!isNewService) {
|
||||
<button (click)="patchService()"
|
||||
class="btn btn-primary">{{ 'service.detail.update' | translate }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (isNewService) {
|
||||
<div>
|
||||
<button (click)="closeModal()" class="btn btn-outline-secondary me-2 "
|
||||
id="delete-confirm-new">{{ 'service.refuse.create' | translate }}
|
||||
</button>
|
||||
<button (click)="createService()"
|
||||
class="btn btn-primary">{{ 'service.confirm.create' | translate }}
|
||||
class="btn btn-primary">{{ 'service.confirm.create' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
|
||||
|
@@ -5,11 +5,7 @@ import {
|
||||
transition,
|
||||
trigger,
|
||||
} from '@angular/animations';
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
@@ -77,9 +73,7 @@ import { notifyPatterns } from '../ldn-services-patterns/ldn-service-coar-patter
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
NgIf,
|
||||
NgbDropdownModule,
|
||||
NgForOf,
|
||||
AsyncPipe,
|
||||
],
|
||||
})
|
||||
@@ -131,6 +125,7 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
|
||||
score: ['', [Validators.required, Validators.pattern('^0*(\.[0-9]+)?$|^1(\.0+)?$')]], inboundPattern: [''],
|
||||
constraintPattern: [''],
|
||||
enabled: [''],
|
||||
usesActorEmailId: [''],
|
||||
type: LDN_SERVICE.value,
|
||||
});
|
||||
}
|
||||
@@ -184,7 +179,8 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
|
||||
return rest;
|
||||
});
|
||||
|
||||
const values = { ...this.formModel.value, enabled: true };
|
||||
const values = { ...this.formModel.value, enabled: true,
|
||||
usesActorEmailId: this.formModel.get('usesActorEmailId').value };
|
||||
|
||||
const ldnServiceData = this.ldnServicesService.create(values);
|
||||
|
||||
@@ -243,6 +239,7 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
|
||||
ldnUrl: this.ldnService.ldnUrl,
|
||||
type: this.ldnService.type,
|
||||
enabled: this.ldnService.enabled,
|
||||
usesActorEmailId: this.ldnService.usesActorEmailId,
|
||||
lowerIp: this.ldnService.lowerIp,
|
||||
upperIp: this.ldnService.upperIp,
|
||||
});
|
||||
@@ -390,6 +387,32 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the usesActorEmailId field of the LDN service by sending a patch request
|
||||
*/
|
||||
toggleUsesActorEmailId() {
|
||||
const newStatus = !this.formModel.get('usesActorEmailId').value;
|
||||
if (!this.isNewService) {
|
||||
const patchOperation: Operation = {
|
||||
op: 'replace',
|
||||
path: '/usesActorEmailId',
|
||||
value: newStatus,
|
||||
};
|
||||
|
||||
this.ldnServicesService.patch(this.ldnService, [patchOperation]).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
).subscribe(
|
||||
() => {
|
||||
this.formModel.get('usesActorEmailId').setValue(newStatus);
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this.formModel.get('usesActorEmailId').setValue(newStatus);
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the modal
|
||||
*/
|
||||
|
@@ -12,6 +12,7 @@ import { LdnService } from '../ldn-services-model/ldn-services.model';
|
||||
export const mockLdnService: LdnService = {
|
||||
uuid: '1',
|
||||
enabled: false,
|
||||
usesActorEmailId: false,
|
||||
score: 0,
|
||||
id: 1,
|
||||
lowerIp: '192.0.2.146',
|
||||
@@ -49,6 +50,7 @@ export const mockLdnServiceRD$ = createSuccessfulRemoteDataObject$(mockLdnServic
|
||||
export const mockLdnServices: LdnService[] = [{
|
||||
uuid: '1',
|
||||
enabled: false,
|
||||
usesActorEmailId: false,
|
||||
score: 0,
|
||||
id: 1,
|
||||
lowerIp: '192.0.2.146',
|
||||
@@ -81,6 +83,7 @@ export const mockLdnServices: LdnService[] = [{
|
||||
}, {
|
||||
uuid: '2',
|
||||
enabled: false,
|
||||
usesActorEmailId: false,
|
||||
score: 0,
|
||||
id: 2,
|
||||
lowerIp: '192.0.2.146',
|
||||
|
@@ -4,63 +4,67 @@
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button class="btn btn-success" routerLink="/admin/ldn/services/new"><i
|
||||
class="fas fa-plus pr-2"></i>{{ 'process.overview.new' | translate }}</button>
|
||||
class="fas fa-plus pe-2"></i>{{ 'process.overview.new' | translate }}</button>
|
||||
</div>
|
||||
<ds-pagination *ngIf="(ldnServicesRD$ | async)?.payload?.totalElements > 0"
|
||||
[collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
[paginationOptions]="pageConfig">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ 'service.overview.table.name' | translate }}</th>
|
||||
<th scope="col">{{ 'service.overview.table.description' | translate }}</th>
|
||||
<th scope="col">{{ 'service.overview.table.status' | translate }}</th>
|
||||
<th scope="col">{{ 'service.overview.table.actions' | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let ldnService of (ldnServicesRD$ | async)?.payload?.page">
|
||||
<td class="col-3">{{ ldnService.name }}</td>
|
||||
<td>
|
||||
<ds-truncatable [id]="ldnService.id">
|
||||
<ds-truncatable-part [id]="ldnService.id" [minLines]="2">
|
||||
<div>
|
||||
{{ ldnService.description }}
|
||||
</div>
|
||||
</ds-truncatable-part>
|
||||
</ds-truncatable>
|
||||
</td>
|
||||
<td>
|
||||
<span (click)="toggleStatus(ldnService, ldnServicesService)"
|
||||
[ngClass]="{ 'status-enabled': ldnService.enabled, 'status-disabled': !ldnService.enabled }"
|
||||
[title]="ldnService.enabled ? ('ldn-service.overview.table.clickToDisable' | translate) : ('ldn-service.overview.table.clickToEnable' | translate)"
|
||||
class="status-indicator">
|
||||
{{ ldnService.enabled ? ('ldn-service.overview.table.enabled' | translate) : ('ldn-service.overview.table.disabled' | translate) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<button
|
||||
(click)="selectServiceToDelete(ldnService.id)"
|
||||
[attr.aria-label]="'ldn-service-overview-select-delete' | translate"
|
||||
class="btn btn-outline-danger">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<button [routerLink]="['/admin/ldn/services/edit/', ldnService.id]"
|
||||
@if ((ldnServicesRD$ | async)?.payload?.totalElements > 0) {
|
||||
<ds-pagination
|
||||
[collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
[paginationOptions]="pageConfig">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ 'service.overview.table.name' | translate }}</th>
|
||||
<th scope="col">{{ 'service.overview.table.description' | translate }}</th>
|
||||
<th scope="col">{{ 'service.overview.table.status' | translate }}</th>
|
||||
<th scope="col">{{ 'service.overview.table.actions' | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (ldnService of (ldnServicesRD$ | async)?.payload?.page; track ldnService) {
|
||||
<tr>
|
||||
<td class="col-3">{{ ldnService.name }}</td>
|
||||
<td>
|
||||
<ds-truncatable [id]="ldnService.id">
|
||||
<ds-truncatable-part [id]="ldnService.id" [minLines]="2">
|
||||
<div>
|
||||
{{ ldnService.description }}
|
||||
</div>
|
||||
</ds-truncatable-part>
|
||||
</ds-truncatable>
|
||||
</td>
|
||||
<td>
|
||||
<span (click)="toggleStatus(ldnService, ldnServicesService)"
|
||||
[ngClass]="{ 'status-enabled': ldnService.enabled, 'status-disabled': !ldnService.enabled }"
|
||||
[title]="ldnService.enabled ? ('ldn-service.overview.table.clickToDisable' | translate) : ('ldn-service.overview.table.clickToEnable' | translate)"
|
||||
class="status-indicator">
|
||||
{{ ldnService.enabled ? ('ldn-service.overview.table.enabled' | translate) : ('ldn-service.overview.table.disabled' | translate) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<button
|
||||
(click)="selectServiceToDelete(ldnService.id)"
|
||||
[attr.aria-label]="'ldn-service-overview-select-delete' | translate"
|
||||
class="btn btn-outline-danger">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<button [routerLink]="['/admin/ldn/services/edit/', ldnService.id]"
|
||||
[attr.aria-label]="'ldn-service-overview-select-edit' | translate"
|
||||
class="btn btn-outline-dark">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-template #deleteModal>
|
||||
@@ -72,8 +76,8 @@
|
||||
<h4>{{'service.overview.delete.header' | translate }}</h4>
|
||||
</div>
|
||||
<button (click)="closeModal()" aria-label="Close"
|
||||
[attr.aria-label]="'ldn-service-overview-close-modal' | translate"
|
||||
class="close" type="button">
|
||||
[attr.aria-label]="'ldn-service-overview-close-modal' | translate"
|
||||
class="close" type="button">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -84,12 +88,12 @@
|
||||
</div>
|
||||
<div class="mt-4 text-right">
|
||||
<button (click)="closeModal()"
|
||||
[attr.aria-label]="'ldn-service-overview-close-modal' | translate"
|
||||
class="btn btn-outline-secondary mr-2">{{ 'service.detail.delete.cancel' | translate }}</button>
|
||||
[attr.aria-label]="'ldn-service-overview-close-modal' | translate"
|
||||
class="btn btn-outline-secondary me-2">{{ 'service.detail.delete.cancel' | translate }}</button>
|
||||
<button (click)="deleteSelected(this.selectedServiceId.toString(), ldnServicesService)"
|
||||
class="btn btn-danger"
|
||||
[attr.aria-label]="'ldn-service-overview-select-delete' | translate"
|
||||
id="delete-confirm">{{ 'service.overview.delete' | translate }}
|
||||
class="btn btn-danger"
|
||||
[attr.aria-label]="'ldn-service-overview-select-delete' | translate"
|
||||
id="delete-confirm">{{ 'service.overview.delete' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgClass,
|
||||
NgFor,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -54,8 +52,6 @@ import { LdnService } from '../ldn-services-model/ldn-services.model';
|
||||
styleUrls: ['./ldn-services-directory.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
imports: [
|
||||
NgIf,
|
||||
NgFor,
|
||||
TranslateModule,
|
||||
AsyncPipe,
|
||||
PaginationComponent,
|
||||
|
@@ -52,6 +52,9 @@ export class LdnService extends CacheableObject {
|
||||
@autoserialize
|
||||
enabled: boolean;
|
||||
|
||||
@autoserialize
|
||||
usesActorEmailId: boolean;
|
||||
|
||||
@autoserialize
|
||||
ldnUrl: string;
|
||||
|
||||
|
@@ -1 +1 @@
|
||||
<ds-publication-claim [source]="'openaire'"></ds-publication-claim>
|
||||
<ds-suggestion-sources></ds-suggestion-sources>
|
||||
|
@@ -6,8 +6,9 @@ import {
|
||||
waitForAsync,
|
||||
} from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
|
||||
import { PublicationClaimComponent } from '../../../notifications/suggestion-targets/publication-claim/publication-claim.component';
|
||||
import { SuggestionSourcesComponent } from '../../../notifications/suggestions/sources/suggestion-sources.component';
|
||||
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page.component';
|
||||
|
||||
describe('AdminNotificationsPublicationClaimPageComponent', () => {
|
||||
@@ -20,17 +21,10 @@ describe('AdminNotificationsPublicationClaimPageComponent', () => {
|
||||
CommonModule,
|
||||
TranslateModule.forRoot(),
|
||||
AdminNotificationsPublicationClaimPageComponent,
|
||||
],
|
||||
providers: [
|
||||
AdminNotificationsPublicationClaimPageComponent,
|
||||
MockComponent(SuggestionSourcesComponent),
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).overrideComponent(AdminNotificationsPublicationClaimPageComponent, {
|
||||
remove: {
|
||||
imports: [PublicationClaimComponent],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
|
@@ -1,14 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { PublicationClaimComponent } from '../../../notifications/suggestion-targets/publication-claim/publication-claim.component';
|
||||
import { SuggestionSourcesComponent } from '../../../notifications/suggestions/sources/suggestion-sources.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notifications-publication-claim-page',
|
||||
templateUrl: './admin-notifications-publication-claim-page.component.html',
|
||||
styleUrls: ['./admin-notifications-publication-claim-page.component.scss'],
|
||||
imports: [
|
||||
PublicationClaimComponent,
|
||||
],
|
||||
imports: [ SuggestionSourcesComponent ],
|
||||
standalone: true,
|
||||
})
|
||||
export class AdminNotificationsPublicationClaimPageComponent {
|
||||
|
@@ -2,7 +2,8 @@ import { Route } from '@angular/router';
|
||||
|
||||
import { authenticatedGuard } from '../../core/auth/authenticated.guard';
|
||||
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { qualityAssuranceBreadcrumbResolver } from '../../core/breadcrumbs/quality-assurance-breadcrumb.resolver';
|
||||
import { sourcesBreadcrumbResolver } from '../../core/breadcrumbs/sources-breadcrumb.resolver';
|
||||
import { PublicationClaimComponent } from '../../notifications/suggestions/targets/publication-claim/publication-claim.component';
|
||||
import { AdminNotificationsPublicationClaimPageResolver } from '../../quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service';
|
||||
import { QualityAssuranceEventsPageComponent } from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.component';
|
||||
import { qualityAssuranceEventsPageResolver } from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver';
|
||||
@@ -33,13 +34,28 @@ export const ROUTES: Route[] = [
|
||||
showBreadcrumbsFluid: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
canActivate: [ authenticatedGuard ],
|
||||
path: `${PUBLICATION_CLAIMS_PATH}/:sourceId`,
|
||||
pathMatch: 'full',
|
||||
component: PublicationClaimComponent,
|
||||
resolve: {
|
||||
breadcrumb: sourcesBreadcrumbResolver,
|
||||
openaireQualityAssuranceEventsParams: AdminNotificationsPublicationClaimPageResolver,
|
||||
},
|
||||
data: {
|
||||
title: 'admin.notifications.publicationclaim.page.title',
|
||||
breadcrumbKey: 'admin.notifications.publicationclaim',
|
||||
showBreadcrumbsFluid: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
canActivate: [authenticatedGuard],
|
||||
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
|
||||
component: QualityAssuranceTopicsPageComponent,
|
||||
pathMatch: 'full',
|
||||
resolve: {
|
||||
breadcrumb: qualityAssuranceBreadcrumbResolver,
|
||||
breadcrumb: sourcesBreadcrumbResolver,
|
||||
openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver,
|
||||
},
|
||||
data: {
|
||||
@@ -85,7 +101,7 @@ export const ROUTES: Route[] = [
|
||||
component: QualityAssuranceEventsPageComponent,
|
||||
pathMatch: 'full',
|
||||
resolve: {
|
||||
breadcrumb: qualityAssuranceBreadcrumbResolver,
|
||||
breadcrumb: sourcesBreadcrumbResolver,
|
||||
openaireQualityAssuranceEventsParams: qualityAssuranceEventsPageResolver,
|
||||
},
|
||||
data: {
|
||||
|
@@ -13,11 +13,13 @@
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="'outbound'" [queryParams]="{view: 'table'}">{{'admin.notify.dashboard.outbound-logs' | translate}}</a>
|
||||
</ul>
|
||||
<div class="mt-2">
|
||||
<ds-admin-notify-metrics *ngIf="(notifyMetricsRows$ | async)?.length" [boxesConfig]="notifyMetricsRows$ | async"></ds-admin-notify-metrics>
|
||||
</ul>
|
||||
<div class="mt-2">
|
||||
@if ((notifyMetricsRows$ | async)?.length) {
|
||||
<ds-admin-notify-metrics [boxesConfig]="notifyMetricsRows$ | async"></ds-admin-notify-metrics>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
Inject,
|
||||
@@ -46,7 +43,6 @@ import {
|
||||
imports: [
|
||||
AdminNotifyMetricsComponent,
|
||||
RouterLink,
|
||||
NgIf,
|
||||
TranslateModule,
|
||||
AsyncPipe,
|
||||
],
|
||||
|
@@ -5,12 +5,14 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<div *ngFor="let key of notifyMessageKeys">
|
||||
@for (key of notifyMessageKeys; track key) {
|
||||
<div>
|
||||
<div class="row mb-4">
|
||||
<div class="font-weight-bold col">{{ key + '.notify-detail-modal' | translate}}</div>
|
||||
<div class="col text-right">{{'notify-detail-modal.' + notifyMessage[key] | translate: {default: notifyMessage[key] ?? "n/a" } }}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button class="btn-primary" (click)="toggleCoarMessage()">
|
||||
@@ -18,5 +20,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<pre @fadeIn [innerHTML]="notifyMessage.message" class="bg-secondary text-white mt-2 p-2" *ngIf="isCoarMessageVisible"></pre>
|
||||
@if (isCoarMessageVisible) {
|
||||
<pre @fadeIn [innerHTML]="notifyMessage.message" class="bg-secondary text-white mt-2 p-2"></pre>
|
||||
}
|
||||
</div>
|
||||
|
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
NgForOf,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -26,9 +23,7 @@ import { AdminNotifyMessage } from '../models/admin-notify-message.model';
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgForOf,
|
||||
TranslateModule,
|
||||
NgIf,
|
||||
],
|
||||
})
|
||||
/**
|
||||
|
@@ -3,10 +3,12 @@
|
||||
<div class="col-12 col-md-3 text-left h4">{{((isInbound$ | async) ? 'admin.notify.dashboard.inbound' : 'admin.notify.dashboard.outbound') | translate}}</div>
|
||||
<div class="col-md-9">
|
||||
<div class="h4">
|
||||
<button (click)="resetDefaultConfiguration()" *ngIf="(selectedSearchConfig$ | async) !== defaultConfiguration" class="badge badge-primary mr-1 mb-1">
|
||||
{{ 'admin-notify-logs.' + (selectedSearchConfig$ | async) | translate}}
|
||||
<span> ×</span>
|
||||
</button>
|
||||
@if ((selectedSearchConfig$ | async) !== defaultConfiguration) {
|
||||
<button (click)="resetDefaultConfiguration()" class="badge bg-primary me-1 mb-1">
|
||||
{{ 'admin-notify-logs.' + (selectedSearchConfig$ | async) | translate}}
|
||||
<span> ×</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<ds-search-labels [inPlaceSearch]="true"></ds-search-labels>
|
||||
</div>
|
||||
|
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
Inject,
|
||||
@@ -39,7 +36,6 @@ import { ThemedSearchComponent } from '../../../../shared/search/themed-search.c
|
||||
ThemedSearchComponent,
|
||||
AsyncPipe,
|
||||
TranslateModule,
|
||||
NgIf,
|
||||
],
|
||||
})
|
||||
|
||||
|
@@ -1,9 +1,13 @@
|
||||
<div class="mb-5" *ngFor="let row of boxesConfig">
|
||||
<div class="mb-2">{{ row.title | translate }}</div>
|
||||
<div class="row justify-content-between">
|
||||
<div class="col-sm" *ngFor="let box of row.boxes">
|
||||
<ds-notification-box (selectedBoxConfig)="navigateToSelectedSearchConfig($event)" [boxConfig]="box"></ds-notification-box>
|
||||
@for (row of boxesConfig; track row) {
|
||||
<div class="mb-5">
|
||||
<div class="mb-2">{{ row.title | translate }}</div>
|
||||
<div class="row justify-content-between">
|
||||
@for (box of row.boxes; track box) {
|
||||
<div class="col-sm">
|
||||
<ds-notification-box (selectedBoxConfig)="navigateToSelectedSearchConfig($event)" [boxConfig]="box"></ds-notification-box>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { NgForOf } from '@angular/common';
|
||||
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
@@ -17,7 +17,6 @@ import { AdminNotifyMetricsRow } from './admin-notify-metrics.model';
|
||||
imports: [
|
||||
NotificationBoxComponent,
|
||||
TranslateModule,
|
||||
NgForOf,
|
||||
],
|
||||
})
|
||||
/**
|
||||
|
@@ -11,41 +11,57 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let message of (messagesSubject$ | async)">
|
||||
<td class="text-nowrap">
|
||||
<div *ngIf="message.queueLastStartTime">{{ message.queueLastStartTime | date:"YYYY/MM/d hh:mm:ss" }}</div>
|
||||
<div *ngIf="!message.queueLastStartTime">n/a</div>
|
||||
</td>
|
||||
<td>
|
||||
<ds-truncatable [id]="message.id">
|
||||
<ds-truncatable-part [id]="message.id" [minLines]="2">
|
||||
<a *ngIf="message.relatedItem" [routerLink]="'/items/' + (message.context || message.object)">{{ message.relatedItem }}</a>
|
||||
</ds-truncatable-part>
|
||||
</ds-truncatable>
|
||||
<div *ngIf="!message.relatedItem">n/a</div>
|
||||
</td>
|
||||
<td>
|
||||
<div *ngIf="message.ldnService">{{ message.ldnService }}</div>
|
||||
<div *ngIf="!message.ldnService">n/a</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ message.activityStreamType }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-nowrap">{{ 'notify-detail-modal.' + message.queueStatusLabel | translate }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<button class="btn mb-2 btn-dark" (click)="openDetailModal(message)">{{ 'notify-message-result.detail' | translate }}</button>
|
||||
<button *ngIf="message.queueStatusLabel !== reprocessStatus && validStatusesForReprocess.includes(message.queueStatusLabel)"
|
||||
(click)="reprocessMessage(message)"
|
||||
class="btn btn-warning"
|
||||
>
|
||||
{{ 'notify-message-result.reprocess' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@for (message of (messagesSubject$ | async); track message) {
|
||||
<tr>
|
||||
<td class="text-nowrap">
|
||||
@if (message.queueLastStartTime) {
|
||||
<div>{{ message.queueLastStartTime | date:"YYYY/MM/d hh:mm:ss" }}</div>
|
||||
}
|
||||
@if (!message.queueLastStartTime) {
|
||||
<div>n/a</div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<ds-truncatable [id]="message.id">
|
||||
<ds-truncatable-part [id]="message.id" [minLines]="2">
|
||||
@if (message.relatedItem) {
|
||||
<a [routerLink]="'/items/' + (message.context || message.object)">{{ message.relatedItem }}</a>
|
||||
}
|
||||
</ds-truncatable-part>
|
||||
</ds-truncatable>
|
||||
@if (!message.relatedItem) {
|
||||
<div>n/a</div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (message.ldnService) {
|
||||
<div>{{ message.ldnService }}</div>
|
||||
}
|
||||
@if (!message.ldnService) {
|
||||
<div>n/a</div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ message.activityStreamType }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-nowrap">{{ 'notify-detail-modal.' + message.queueStatusLabel | translate }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<button class="btn mb-2 btn-dark" (click)="openDetailModal(message)">{{ 'notify-message-result.detail' | translate }}</button>
|
||||
@if (message.queueStatusLabel !== reprocessStatus && validStatusesForReprocess.includes(message.queueStatusLabel)) {
|
||||
<button
|
||||
(click)="reprocessMessage(message)"
|
||||
class="btn btn-warning"
|
||||
>
|
||||
{{ 'notify-message-result.reprocess' | translate }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
DatePipe,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
@@ -42,8 +40,6 @@ import { AdminNotifyMessagesService } from '../services/admin-notify-messages.se
|
||||
standalone: true,
|
||||
imports: [
|
||||
TranslateModule,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
DatePipe,
|
||||
AsyncPipe,
|
||||
TruncatableComponent,
|
||||
|
@@ -8,51 +8,62 @@
|
||||
<p id="create-new" class="mb-2"><a [routerLink]="'add'" class="btn btn-success">{{'admin.registries.bitstream-formats.create.new' | translate}}</a></p>
|
||||
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(bitstreamFormats | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="pageConfig"
|
||||
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
||||
[hideGear]="false"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="formats" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><span class="sr-only">{{'admin.registries.bitstream-formats.table.selected' | translate}}</span></th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.id' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
||||
<td>
|
||||
<label class="mb-0">
|
||||
<input type="checkbox"
|
||||
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
|
||||
[checked]="isSelected(bitstreamFormat) | async"
|
||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||
>
|
||||
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
|
||||
</label>
|
||||
</td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@if ((bitstreamFormats$ | async)?.payload?.totalElements > 0) {
|
||||
<ds-pagination
|
||||
[paginationOptions]="pageConfig"
|
||||
[collectionSize]="(bitstreamFormats$ | async)?.payload?.totalElements"
|
||||
[hideGear]="false"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="formats" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><span class="sr-only">{{'admin.registries.bitstream-formats.table.selected' | translate}}</span></th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.id' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (bitstreamFormat of (bitstreamFormats$ | async)?.payload?.page; track bitstreamFormat) {
|
||||
<tr>
|
||||
<td>
|
||||
<label class="form-label mb-0">
|
||||
<input type="checkbox"
|
||||
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
|
||||
[checked]="(selectedBitstreamFormatIDs$ | async)?.includes(bitstreamFormat.id)"
|
||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||
>
|
||||
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
|
||||
</label>
|
||||
</td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} @if (bitstreamFormat.internal) {
|
||||
<span>({{'admin.registries.bitstream-formats.table.internal' | translate}})</span>
|
||||
}</a></td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
}
|
||||
@if ((bitstreamFormats$ | async)?.payload?.totalElements === 0) {
|
||||
<div class="alert alert-info" role="alert">
|
||||
{{'admin.registries.bitstream-formats.no-items' | translate}}
|
||||
</div>
|
||||
</ds-pagination>
|
||||
<div *ngIf="(bitstreamFormats | async)?.payload?.totalElements === 0" class="alert alert-info" role="alert">
|
||||
{{'admin.registries.bitstream-formats.no-items' | translate}}
|
||||
</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" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button>
|
||||
@if ((bitstreamFormats$ | async)?.payload?.page?.length > 0) {
|
||||
<button class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button>
|
||||
}
|
||||
@if ((bitstreamFormats$ | async)?.payload?.page?.length > 0) {
|
||||
<button type="submit" class="btn btn-danger float-end" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -8,10 +8,7 @@ import { By } from '@angular/platform-browser';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
cold,
|
||||
hot,
|
||||
} from 'jasmine-marbles';
|
||||
import { hot } from 'jasmine-marbles';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||
@@ -191,17 +188,17 @@ describe('BitstreamFormatsComponent', () => {
|
||||
beforeEach(waitForAsync(initAsync));
|
||||
beforeEach(initBeforeEach);
|
||||
it('should return an observable of true if the provided bitstream is in the list returned by the service', () => {
|
||||
const result = comp.isSelected(bitstreamFormat1);
|
||||
|
||||
expect(result).toBeObservable(cold('b', { b: true }));
|
||||
comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => {
|
||||
expect(selectedBitstreamFormatIDs).toContain(bitstreamFormat1.id);
|
||||
});
|
||||
});
|
||||
it('should return an observable of false if the provided bitstream is not in the list returned by the service', () => {
|
||||
const format = new BitstreamFormat();
|
||||
format.uuid = 'new';
|
||||
|
||||
const result = comp.isSelected(format);
|
||||
|
||||
expect(result).toBeObservable(cold('b', { b: false }));
|
||||
comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => {
|
||||
expect(selectedBitstreamFormatIDs).not.toContain(format.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
OnDestroy,
|
||||
@@ -13,10 +9,7 @@ import {
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
mergeMap,
|
||||
@@ -44,12 +37,10 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
|
||||
selector: 'ds-bitstream-formats',
|
||||
templateUrl: './bitstream-formats.component.html',
|
||||
imports: [
|
||||
NgIf,
|
||||
AsyncPipe,
|
||||
RouterLink,
|
||||
TranslateModule,
|
||||
PaginationComponent,
|
||||
NgForOf,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
@@ -58,7 +49,12 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* 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
|
||||
@@ -125,14 +121,11 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given bitstream format is selected in the list (checkbox)
|
||||
* @param bitstreamFormat
|
||||
* Returns the list of all the bitstream formats that are selected in the list (checkbox)
|
||||
*/
|
||||
isSelected(bitstreamFormat: BitstreamFormat): Observable<boolean> {
|
||||
selectedBitstreamFormatIDs(): Observable<string[]> {
|
||||
return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
|
||||
map((bitstreamFormats: BitstreamFormat[]) => {
|
||||
return bitstreamFormats.find((selectedFormat) => selectedFormat.id === bitstreamFormat.id) != null;
|
||||
}),
|
||||
map((bitstreamFormats: BitstreamFormat[]) => bitstreamFormats.map((selectedFormat) => selectedFormat.id)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -156,27 +149,23 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
||||
const prefix = 'admin.registries.bitstream-formats.delete';
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
|
||||
const messages = observableCombineLatest(
|
||||
this.translateService.get(`${prefix}.${suffix}.head`),
|
||||
this.translateService.get(`${prefix}.${suffix}.amount`, { amount: amount }),
|
||||
);
|
||||
messages.subscribe(([head, content]) => {
|
||||
const head: string = this.translateService.instant(`${prefix}.${suffix}.head`);
|
||||
const content: string = this.translateService.instant(`${prefix}.${suffix}.amount`, { amount: amount });
|
||||
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content);
|
||||
} else {
|
||||
this.notificationsService.error(head, content);
|
||||
}
|
||||
});
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content);
|
||||
} else {
|
||||
this.notificationsService.error(head, content);
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
return this.bitstreamFormatService.findAll(findListOptions);
|
||||
}),
|
||||
);
|
||||
this.selectedBitstreamFormatIDs$ = this.selectedBitstreamFormatIDs();
|
||||
}
|
||||
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
<ds-form *ngIf="formModel"
|
||||
[formId]="'comcol-form-id'"
|
||||
[formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form>
|
||||
@if (formModel) {
|
||||
<ds-form
|
||||
[formId]="'comcol-form-id'"
|
||||
[formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form>
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import { NgIf } from '@angular/common';
|
||||
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -35,7 +35,6 @@ import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-p
|
||||
templateUrl: './format-form.component.html',
|
||||
imports: [
|
||||
FormComponent,
|
||||
NgIf,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
@@ -64,7 +63,7 @@ export class FormatFormComponent implements OnInit {
|
||||
*/
|
||||
arrayElementLayout: DynamicFormControlLayout = {
|
||||
grid: {
|
||||
group: 'form-row',
|
||||
group: 'row',
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -1,60 +1,65 @@
|
||||
<div class="container">
|
||||
<div class="metadata-registry row">
|
||||
<div class="col-12">
|
||||
<div class="metadata-registry row">
|
||||
<div class="col-12">
|
||||
|
||||
<h1 id="header" class="border-bottom pb-2">{{'admin.registries.metadata.head' | translate}}</h1>
|
||||
<h1 id="header" class="border-bottom pb-2">{{'admin.registries.metadata.head' | translate}}</h1>
|
||||
|
||||
<p id="description" class="pb-2">{{'admin.registries.metadata.description' | translate}}</p>
|
||||
<p id="description" class="pb-2">{{'admin.registries.metadata.description' | translate}}</p>
|
||||
|
||||
<ds-metadata-schema-form (submitForm)="forceUpdateSchemas()"></ds-metadata-schema-form>
|
||||
<ds-metadata-schema-form (submitForm)="forceUpdateSchemas()"></ds-metadata-schema-form>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-schemas" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><span class="sr-only">{{'admin.registries.metadata.schemas.table.selected' | translate}}</span></th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.id' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.namespace' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.name' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
||||
[ngClass]="{'table-primary' : isActive(schema) | async}">
|
||||
<td>
|
||||
<label class="mb-0">
|
||||
<input type="checkbox"
|
||||
[checked]="isSelected(schema) | async"
|
||||
(change)="selectMetadataSchema(schema, $event)"
|
||||
>
|
||||
<span class="sr-only">{{((isSelected(schema) | async) ? 'admin.registries.metadata.schemas.deselect' : 'admin.registries.metadata.schemas.select') | translate}}</span>
|
||||
</label>
|
||||
</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.namespace}}</a></td>
|
||||
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.prefix}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(metadataSchemas | async)?.payload?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{'admin.registries.metadata.schemas.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button *ngIf="(metadataSchemas | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteSchemas()">{{'admin.registries.metadata.schemas.table.delete' | translate}}</button>
|
||||
</div>
|
||||
@if ((metadataSchemas | async)?.payload?.totalElements > 0) {
|
||||
<ds-pagination
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-schemas" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><span class="sr-only">{{'admin.registries.metadata.schemas.table.selected' | translate}}</span></th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.id' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.namespace' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.name' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (schema of (metadataSchemas | async)?.payload?.page; track schema) {
|
||||
<tr
|
||||
[ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}">
|
||||
<td>
|
||||
<label class="form-label mb-0">
|
||||
<input type="checkbox"
|
||||
[checked]="(selectedMetadataSchemaIDs$ | async)?.includes(schema.id)"
|
||||
(change)="selectMetadataSchema(schema, $event)"
|
||||
>
|
||||
<span class="sr-only">{{(((selectedMetadataSchemaIDs$ | async)?.includes(schema.id)) ? 'admin.registries.metadata.schemas.deselect' : 'admin.registries.metadata.schemas.select') | translate}}</span>
|
||||
</label>
|
||||
</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.namespace}}</a></td>
|
||||
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.prefix}}</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
}
|
||||
|
||||
@if ((metadataSchemas | async)?.payload?.totalElements === 0) {
|
||||
<div class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{'admin.registries.metadata.schemas.no-items' | translate}}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
@if ((metadataSchemas | async)?.payload?.page?.length > 0) {
|
||||
<button type="submit" class="btn btn-danger float-end" (click)="deleteSchemas()">{{'admin.registries.metadata.schemas.table.delete' | translate}}</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -17,9 +17,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
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 { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
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 { NotificationsServiceStub } from '../../../shared/testing/notifications-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 { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||
import { MetadataRegistryComponent } from './metadata-registry.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', () => {
|
||||
let comp: MetadataRegistryComponent;
|
||||
let fixture: ComponentFixture<MetadataRegistryComponent>;
|
||||
let registryService: RegistryService;
|
||||
let paginationService;
|
||||
const mockSchemasList = [
|
||||
|
||||
let paginationService: PaginationServiceStub;
|
||||
let registryService: RegistryServiceStub;
|
||||
|
||||
const mockSchemasList: MetadataSchema[] = [
|
||||
{
|
||||
id: 1,
|
||||
_links: {
|
||||
@@ -67,25 +69,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
prefix: 'mock',
|
||||
namespace: 'http://dspace.org/mockschema',
|
||||
},
|
||||
];
|
||||
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();
|
||||
] as MetadataSchema[];
|
||||
|
||||
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||
@@ -109,6 +93,10 @@ describe('MetadataRegistryComponent', () => {
|
||||
);
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
paginationService = new PaginationServiceStub();
|
||||
registryService = new RegistryServiceStub();
|
||||
spyOn(registryService, 'getMetadataSchemas').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)));
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -120,7 +108,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
EnumKeysPipe,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: RegistryService, useValue: registryService },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{
|
||||
@@ -190,7 +178,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
}));
|
||||
|
||||
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');
|
||||
row.click();
|
||||
fixture.detectChanges();
|
||||
@@ -205,7 +193,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
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();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -1,25 +1,21 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgClass,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
Router,
|
||||
RouterLink,
|
||||
} from '@angular/router';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import {
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
Subscription,
|
||||
zip,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
@@ -36,7 +32,6 @@ import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { RegistryService } from '../../../core/registry/registry.service';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||
@@ -52,8 +47,6 @@ import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-sch
|
||||
TranslateModule,
|
||||
AsyncPipe,
|
||||
PaginationComponent,
|
||||
NgIf,
|
||||
NgForOf,
|
||||
NgClass,
|
||||
RouterLink,
|
||||
],
|
||||
@@ -63,13 +56,23 @@ import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-sch
|
||||
* A component used for managing all existing metadata schemas within the repository.
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -79,15 +82,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);
|
||||
|
||||
constructor(private registryService: RegistryService,
|
||||
private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
private paginationService: PaginationService,
|
||||
private translateService: TranslateService) {
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
protected registryService: RegistryService,
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -116,30 +129,13 @@ export class MetadataRegistryComponent implements OnDestroy {
|
||||
* @param schema
|
||||
*/
|
||||
editSchema(schema: MetadataSchema) {
|
||||
this.getActiveSchema().pipe(take(1)).subscribe((activeSchema) => {
|
||||
this.subscriptions.push(this.activeMetadataSchema$.pipe(take(1)).subscribe((activeSchema: MetadataSchema) => {
|
||||
if (schema === activeSchema) {
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
} else {
|
||||
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 +149,25 @@ export class MetadataRegistryComponent implements OnDestroy {
|
||||
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
|
||||
*/
|
||||
deleteSchemas() {
|
||||
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
||||
(schemas) => {
|
||||
const tasks$ = [];
|
||||
for (const schema of schemas) {
|
||||
if (hasValue(schema.id)) {
|
||||
tasks$.push(this.registryService.deleteMetadataSchema(schema.id).pipe(getFirstCompletedRemoteData()));
|
||||
}
|
||||
}
|
||||
zip(...tasks$).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||
if (successResponses.length > 0) {
|
||||
this.showNotification(true, successResponses.length);
|
||||
}
|
||||
if (failedResponses.length > 0) {
|
||||
this.showNotification(false, failedResponses.length);
|
||||
}
|
||||
this.registryService.deselectAllMetadataSchema();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
});
|
||||
},
|
||||
);
|
||||
this.subscriptions.push(this.selectedMetadataSchemaIDs$.pipe(
|
||||
take(1),
|
||||
switchMap((schemaIDs: number[]) => zip(schemaIDs.map((schemaID: number) => this.registryService.deleteMetadataSchema(schemaID).pipe(getFirstCompletedRemoteData())))),
|
||||
).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||
const successResponses: RemoteData<NoContent>[] = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||
const failedResponses: RemoteData<NoContent>[] = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||
if (successResponses.length > 0) {
|
||||
this.showNotification(true, successResponses.length);
|
||||
}
|
||||
if (failedResponses.length > 0) {
|
||||
this.showNotification(false, failedResponses.length);
|
||||
}
|
||||
this.registryService.deselectAllMetadataSchema();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,20 +178,20 @@ export class MetadataRegistryComponent implements OnDestroy {
|
||||
showNotification(success: boolean, amount: number) {
|
||||
const prefix = 'admin.registries.schema.notification';
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
const messages = observableCombineLatest(
|
||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
||||
this.translateService.get(`${prefix}.deleted.${suffix}`, { amount: amount }),
|
||||
);
|
||||
messages.subscribe(([head, content]) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content);
|
||||
} else {
|
||||
this.notificationsService.error(head, content);
|
||||
}
|
||||
});
|
||||
|
||||
const head: string = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`);
|
||||
const content: string = this.translateService.instant(`${prefix}.deleted.${suffix}`, { amount: amount });
|
||||
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content);
|
||||
} else {
|
||||
this.notificationsService.error(head, content);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,18 +1,16 @@
|
||||
<div *ngIf="registryService.getActiveMetadataSchema() | async; then editheader; else createHeader"></div>
|
||||
@if (activeMetadataSchema$ | async) {
|
||||
<h2>{{messagePrefix + '.edit' | translate}}</h2>
|
||||
} @else {
|
||||
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
||||
}
|
||||
|
||||
<ng-template #createHeader>
|
||||
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #editheader>
|
||||
<h2>{{messagePrefix + '.edit' | translate}}</h2>
|
||||
</ng-template>
|
||||
|
||||
<ds-form [formId]="formId"
|
||||
[formModel]="formModel"
|
||||
[formGroup]="formGroup"
|
||||
[formLayout]="formLayout"
|
||||
(cancel)="onCancel()"
|
||||
(submitForm)="onSubmit()">
|
||||
[formModel]="formModel"
|
||||
[formGroup]="formGroup"
|
||||
[formLayout]="formLayout"
|
||||
(cancel)="onCancel()"
|
||||
(submitForm)="onSubmit()">
|
||||
|
||||
</ds-form>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
inject,
|
||||
@@ -16,42 +16,26 @@ import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { FormComponent } from '../../../../shared/form/form.component';
|
||||
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 { MetadataSchemaFormComponent } from './metadata-schema-form.component';
|
||||
|
||||
describe('MetadataSchemaFormComponent', () => {
|
||||
let component: MetadataSchemaFormComponent;
|
||||
let fixture: ComponentFixture<MetadataSchemaFormComponent>;
|
||||
let registryService: RegistryService;
|
||||
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
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 */
|
||||
let registryService: RegistryServiceStub;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
registryService = new RegistryServiceStub();
|
||||
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataSchemaFormComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: RegistryService, useValue: registryService },
|
||||
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
.overrideComponent(MetadataSchemaFormComponent, {
|
||||
remove: {
|
||||
@@ -88,7 +72,7 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
|
||||
describe('without an active schema', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(undefined));
|
||||
component.activeMetadataSchema$ = observableOf(undefined);
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -107,7 +91,7 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
} as MetadataSchema);
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId));
|
||||
component.activeMetadataSchema$ = observableOf(expectedWithId);
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -21,13 +18,13 @@ import {
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import {
|
||||
combineLatest,
|
||||
Observable,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||
@@ -39,7 +36,6 @@ import { FormComponent } from '../../../../shared/form/form.component';
|
||||
selector: 'ds-metadata-schema-form',
|
||||
templateUrl: './metadata-schema-form.component.html',
|
||||
imports: [
|
||||
NgIf,
|
||||
AsyncPipe,
|
||||
TranslateModule,
|
||||
FormComponent,
|
||||
@@ -102,64 +98,71 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
@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() {
|
||||
combineLatest([
|
||||
this.translateService.get(`${this.messagePrefix}.name`),
|
||||
this.translateService.get(`${this.messagePrefix}.namespace`),
|
||||
]).subscribe(([name, namespace]) => {
|
||||
this.name = new DynamicInputModel({
|
||||
id: 'name',
|
||||
label: name,
|
||||
name: 'name',
|
||||
validators: {
|
||||
required: null,
|
||||
pattern: '^[^. ,]*$',
|
||||
maxLength: 32,
|
||||
},
|
||||
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.name = new DynamicInputModel({
|
||||
id: 'name',
|
||||
label: this.translateService.instant(`${this.messagePrefix}.name`),
|
||||
name: 'name',
|
||||
validators: {
|
||||
required: null,
|
||||
pattern: '^[^. ,]*$',
|
||||
maxLength: 32,
|
||||
},
|
||||
required: true,
|
||||
errorMessages: {
|
||||
pattern: 'error.validation.metadata.name.invalid-pattern',
|
||||
maxLength: 'error.validation.metadata.name.max-length',
|
||||
},
|
||||
});
|
||||
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 +179,29 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
* Emit the updated/created schema using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit(): void {
|
||||
this.registryService
|
||||
.getActiveMetadataSchema()
|
||||
.pipe(
|
||||
take(1),
|
||||
switchMap((schema: MetadataSchema) => {
|
||||
const metadataValues = {
|
||||
prefix: this.name.value,
|
||||
namespace: this.namespace.value,
|
||||
};
|
||||
|
||||
let createOrUpdate$: Observable<MetadataSchema>;
|
||||
|
||||
if (schema == null) {
|
||||
createOrUpdate$ =
|
||||
this.registryService.createOrUpdateMetadataSchema(
|
||||
Object.assign(new MetadataSchema(), metadataValues),
|
||||
);
|
||||
} else {
|
||||
const updatedSchema = Object.assign(
|
||||
new MetadataSchema(),
|
||||
schema,
|
||||
{
|
||||
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();
|
||||
});
|
||||
this.activeMetadataSchema$.pipe(
|
||||
take(1),
|
||||
switchMap((schema: MetadataSchema) => {
|
||||
const metadataValues = {
|
||||
prefix: this.name.value,
|
||||
namespace: this.namespace.value,
|
||||
};
|
||||
if (schema == null) {
|
||||
return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), metadataValues));
|
||||
} else {
|
||||
return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
||||
namespace: metadataValues.namespace,
|
||||
}));
|
||||
}
|
||||
}),
|
||||
switchMap((updatedOrCreatedSchema: MetadataSchema) => this.registryService.clearMetadataSchemaRequests().pipe(
|
||||
map(() => updatedOrCreatedSchema),
|
||||
)),
|
||||
).subscribe((updatedOrCreatedSchema: MetadataSchema) => {
|
||||
this.submitForm.emit(updatedOrCreatedSchema);
|
||||
this.clearFields();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,5 +217,6 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.onCancel();
|
||||
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
|
||||
}
|
||||
}
|
||||
|
@@ -1,18 +1,16 @@
|
||||
<div *ngIf="registryService.getActiveMetadataField() | async; then editheader; else createHeader"></div>
|
||||
@if (registryService.getActiveMetadataField() | async) {
|
||||
<h2>{{messagePrefix + '.edit' | translate}}</h2>
|
||||
} @else {
|
||||
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
||||
}
|
||||
|
||||
<ng-template #createHeader>
|
||||
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #editheader>
|
||||
<h2>{{messagePrefix + '.edit' | translate}}</h2>
|
||||
</ng-template>
|
||||
|
||||
<ds-form [formId]="formId"
|
||||
[formModel]="formModel"
|
||||
[formLayout]="formLayout"
|
||||
[formGroup]="formGroup"
|
||||
(cancel)="onCancel()"
|
||||
(submit)="onSubmit()">
|
||||
[formModel]="formModel"
|
||||
[formLayout]="formLayout"
|
||||
[formGroup]="formGroup"
|
||||
(cancel)="onCancel()"
|
||||
(submit)="onSubmit()">
|
||||
|
||||
</ds-form>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
inject,
|
||||
@@ -17,13 +17,15 @@ import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { FormComponent } from '../../../../shared/form/form.component';
|
||||
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 { MetadataFieldFormComponent } from './metadata-field-form.component';
|
||||
|
||||
describe('MetadataFieldFormComponent', () => {
|
||||
let component: MetadataFieldFormComponent;
|
||||
let fixture: ComponentFixture<MetadataFieldFormComponent>;
|
||||
let registryService: RegistryService;
|
||||
|
||||
let registryService: RegistryServiceStub;
|
||||
|
||||
const metadataSchema = Object.assign(new MetadataSchema(), {
|
||||
id: 1,
|
||||
@@ -31,37 +33,16 @@ describe('MetadataFieldFormComponent', () => {
|
||||
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(() => {
|
||||
registryService = new RegistryServiceStub();
|
||||
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataFieldFormComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: RegistryService, useValue: registryService },
|
||||
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
.overrideComponent(MetadataFieldFormComponent, {
|
||||
remove: { imports: [FormComponent] },
|
||||
|
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -35,7 +32,6 @@ import { FormComponent } from '../../../../shared/form/form.component';
|
||||
selector: 'ds-metadata-field-form',
|
||||
templateUrl: './metadata-field-form.component.html',
|
||||
imports: [
|
||||
NgIf,
|
||||
FormComponent,
|
||||
TranslateModule,
|
||||
AsyncPipe,
|
||||
|
@@ -8,52 +8,59 @@
|
||||
|
||||
<ds-metadata-field-form
|
||||
[metadataSchema]="schema"
|
||||
(submitForm)="forceUpdateFields()"></ds-metadata-field-form>
|
||||
(submitForm)="forceUpdateFields()"></ds-metadata-field-form>
|
||||
|
||||
<h2>{{'admin.registries.schema.fields.head' | translate}}</h2>
|
||||
|
||||
<ng-container *ngVar="(metadataFields$ | async)?.payload as fields">
|
||||
<ds-pagination
|
||||
*ngIf="fields?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="fields?.totalElements"
|
||||
[hideGear]="false"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-fields" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><span class="sr-only">{{'admin.registries.schema.fields.table.selected' | translate}}</span></th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.id' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let field of fields?.page"
|
||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
||||
<td *ngVar="(isSelected(field) | async) as selected">
|
||||
<input type="checkbox"
|
||||
[attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate"
|
||||
[checked]="selected"
|
||||
(change)="selectMetadataField(field, $event)">
|
||||
</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}{{field.qualifier ? '.' + field.qualifier : ''}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
@if (fields?.totalElements > 0) {
|
||||
<ds-pagination
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="fields?.totalElements"
|
||||
[hideGear]="false"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-fields" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><span class="sr-only">{{'admin.registries.schema.fields.table.selected' | translate}}</span></th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.id' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (field of fields?.page; track field) {
|
||||
<tr
|
||||
[ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}">
|
||||
<td *ngVar="(selectedMetadataFieldIDs$ | async)?.includes(field.id) as selected">
|
||||
<input type="checkbox"
|
||||
[attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate"
|
||||
[checked]="selected"
|
||||
(change)="selectMetadataField(field, $event)">
|
||||
</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}{{field.qualifier ? '.' + field.qualifier : ''}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
}
|
||||
|
||||
<div *ngIf="fields?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{'admin.registries.schema.fields.no-items' | translate}}
|
||||
</div>
|
||||
@if (fields?.totalElements === 0) {
|
||||
<div class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{'admin.registries.schema.fields.no-items' | translate}}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<button [routerLink]="['/admin/registries/metadata']" class="btn btn-primary">{{'admin.registries.schema.return' | translate}}</button>
|
||||
<button *ngIf="fields?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
|
||||
@if (fields?.page?.length > 0) {
|
||||
<button type="submit" class="btn btn-danger float-end" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
inject,
|
||||
@@ -7,16 +7,12 @@ import {
|
||||
waitForAsync,
|
||||
} from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
||||
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 { NotificationsServiceStub } from '../../../shared/testing/notifications-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 { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||
@@ -45,8 +41,12 @@ import { MetadataSchemaComponent } from './metadata-schema.component';
|
||||
describe('MetadataSchemaComponent', () => {
|
||||
let comp: MetadataSchemaComponent;
|
||||
let fixture: ComponentFixture<MetadataSchemaComponent>;
|
||||
let registryService: RegistryService;
|
||||
const mockSchemasList = [
|
||||
|
||||
let registryService: RegistryServiceStub;
|
||||
let activatedRoute: ActivatedRouteStub;
|
||||
let paginationService: PaginationServiceStub;
|
||||
|
||||
const mockSchemasList: MetadataSchema[] = [
|
||||
{
|
||||
id: 1,
|
||||
_links: {
|
||||
@@ -67,8 +67,8 @@ describe('MetadataSchemaComponent', () => {
|
||||
prefix: 'mock',
|
||||
namespace: 'http://dspace.org/mockschema',
|
||||
},
|
||||
];
|
||||
const mockFieldsList = [
|
||||
] as MetadataSchema[];
|
||||
const mockFieldsList: MetadataField[] = [
|
||||
{
|
||||
id: 1,
|
||||
_links: {
|
||||
@@ -117,33 +117,8 @@ describe('MetadataSchemaComponent', () => {
|
||||
scopeNote: null,
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]),
|
||||
},
|
||||
];
|
||||
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 */
|
||||
] as MetadataField[];
|
||||
const schemaNameParam = 'mock';
|
||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||
params: observableOf({
|
||||
schemaName: schemaNameParam,
|
||||
}),
|
||||
});
|
||||
|
||||
const paginationService = new PaginationServiceStub();
|
||||
|
||||
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||
@@ -162,6 +137,14 @@ describe('MetadataSchemaComponent', () => {
|
||||
|
||||
|
||||
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({
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -174,10 +157,9 @@ describe('MetadataSchemaComponent', () => {
|
||||
VarDirective,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: RegistryService, useValue: registryService },
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{
|
||||
provide: NotificationsService,
|
||||
@@ -187,7 +169,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
.overrideComponent(MetadataSchemaComponent, {
|
||||
remove: {
|
||||
@@ -242,7 +224,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
}));
|
||||
|
||||
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');
|
||||
row.click();
|
||||
fixture.detectChanges();
|
||||
@@ -257,7 +239,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
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();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgClass,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
@@ -20,9 +18,9 @@ import {
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
Subscription,
|
||||
zip,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
@@ -42,7 +40,6 @@ import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
} from '../../../core/shared/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||
@@ -60,8 +57,6 @@ import { MetadataFieldFormComponent } from './metadata-field-form/metadata-field
|
||||
MetadataFieldFormComponent,
|
||||
TranslateModule,
|
||||
PaginationComponent,
|
||||
NgIf,
|
||||
NgForOf,
|
||||
NgClass,
|
||||
RouterLink,
|
||||
],
|
||||
@@ -71,7 +66,7 @@ import { MetadataFieldFormComponent } from './metadata-field-form/metadata-field
|
||||
* A component used for managing all existing metadata fields within the current metadata schema.
|
||||
* 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
|
||||
*/
|
||||
@@ -96,26 +91,33 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
|
||||
constructor(private registryService: RegistryService,
|
||||
private route: ActivatedRoute,
|
||||
private notificationsService: NotificationsService,
|
||||
private paginationService: PaginationService,
|
||||
private translateService: TranslateService) {
|
||||
/**
|
||||
* The current {@link MetadataField} that is being edited
|
||||
*/
|
||||
activeField$: Observable<MetadataField>;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
this.route.params.subscribe((params) => {
|
||||
this.initialize(params);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component using the params within the url (schemaName)
|
||||
* @param params
|
||||
*/
|
||||
initialize(params) {
|
||||
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(this.route.snapshot.params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||
this.activeField$ = this.registryService.getActiveMetadataField();
|
||||
this.selectedMetadataFieldIDs$ = this.registryService.getSelectedMetadataFields().pipe(
|
||||
map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => metadataField.id)),
|
||||
);
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
@@ -148,30 +150,13 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
||||
* @param field
|
||||
*/
|
||||
editField(field: MetadataField) {
|
||||
this.getActiveField().pipe(take(1)).subscribe((activeField) => {
|
||||
this.subscriptions.push(this.activeField$.pipe(take(1)).subscribe((activeField) => {
|
||||
if (field === activeField) {
|
||||
this.registryService.cancelEditMetadataField();
|
||||
} else {
|
||||
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 +170,25 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
||||
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
|
||||
*/
|
||||
deleteFields() {
|
||||
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
|
||||
(fields) => {
|
||||
const tasks$ = [];
|
||||
for (const field of fields) {
|
||||
if (hasValue(field.id)) {
|
||||
tasks$.push(this.registryService.deleteMetadataField(field.id).pipe(getFirstCompletedRemoteData()));
|
||||
}
|
||||
}
|
||||
zip(...tasks$).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||
if (successResponses.length > 0) {
|
||||
this.showNotification(true, successResponses.length);
|
||||
}
|
||||
if (failedResponses.length > 0) {
|
||||
this.showNotification(false, failedResponses.length);
|
||||
}
|
||||
this.registryService.deselectAllMetadataField();
|
||||
this.registryService.cancelEditMetadataField();
|
||||
});
|
||||
},
|
||||
);
|
||||
this.subscriptions.push(this.selectedMetadataFieldIDs$.pipe(
|
||||
take(1),
|
||||
switchMap((fieldIDs) => zip(fieldIDs.map((fieldID) => this.registryService.deleteMetadataField(fieldID).pipe(getFirstCompletedRemoteData())))),
|
||||
).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||
if (successResponses.length > 0) {
|
||||
this.showNotification(true, successResponses.length);
|
||||
}
|
||||
if (failedResponses.length > 0) {
|
||||
this.showNotification(false, failedResponses.length);
|
||||
}
|
||||
this.registryService.deselectAllMetadataField();
|
||||
this.registryService.cancelEditMetadataField();
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,21 +199,19 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
||||
showNotification(success: boolean, amount: number) {
|
||||
const prefix = 'admin.registries.schema.notification';
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
const messages = observableCombineLatest([
|
||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
||||
this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount }),
|
||||
]);
|
||||
messages.subscribe(([head, content]) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content);
|
||||
} else {
|
||||
this.notificationsService.error(head, content);
|
||||
}
|
||||
});
|
||||
const head = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`);
|
||||
const content = this.translateService.instant(`${prefix}.field.deleted.${suffix}`, { amount: amount });
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content);
|
||||
} else {
|
||||
this.notificationsService.error(head, content);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
this.registryService.deselectAllMetadataField();
|
||||
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,64 +1,72 @@
|
||||
<div class="container">
|
||||
<div class="metadata-registry row">
|
||||
<div class="col-12">
|
||||
<div class="metadata-registry row">
|
||||
<div class="col-12">
|
||||
|
||||
<h1 id="header" class="border-bottom pb-2">{{ "admin.reports.collections.head" | translate }}</h1>
|
||||
<h1 id="header" class="border-bottom pb-2">{{ "admin.reports.collections.head" | translate }}</h1>
|
||||
|
||||
<div id="metadatadiv">
|
||||
<ngb-accordion [closeOthers]="true" activeIds="filters" #acc="ngbAccordion">
|
||||
<ngb-panel id="filters">
|
||||
<ng-template ngbPanelTitle>
|
||||
{{ "admin.reports.commons.filters" | translate }}
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<span class="col-3"></span>
|
||||
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{ "admin.reports.button.show-collections" | translate }}</button>
|
||||
</div>
|
||||
<ds-filters [filtersForm]="filtersFormGroup()"></ds-filters>
|
||||
<div class="row">
|
||||
<span class="col-3"></span>
|
||||
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{ "admin.reports.button.show-collections" | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
<ngb-panel id="collections">
|
||||
<ng-template ngbPanelTitle>
|
||||
{{ "admin.reports.collections.collections-report" | translate }}
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<table id="table" class="table table-striped">
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th rowspan="2">{{ "admin.reports.collections.community" | translate }}</th>
|
||||
<th rowspan="2">{{ "admin.reports.collections.collection" | translate }}</th>
|
||||
<th>{{ "admin.reports.collections.nb_items" | translate }}</th>
|
||||
<th>{{ "admin.reports.collections.match_all_selected_filters" | translate }}</th>
|
||||
<th *ngFor="let filter of results.summary.values | keyvalue">{{ ("admin.reports.commons.filters." + getGroup(filter.key) + "." + filter.key) | translate }}</th>
|
||||
</tr>
|
||||
<tr class="header">
|
||||
<th class="num">{{ results.summary.nbTotalItems }}</th>
|
||||
<th class="num">{{ results.summary.allFiltersValue }}</th>
|
||||
<th class="num" *ngFor="let filter of results.summary.values | keyvalue">{{ filter.value }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let coll of results.collections">
|
||||
<td><a href="/handle/{{ coll.communityHandle }}" rel="noopener noreferrer" target="_blank">{{ coll.communityLabel }}</a></td>
|
||||
<td><a href="/handle/{{ coll.handle }}" rel="noopener noreferrer" target="_blank">{{ coll.label }}</a></td>
|
||||
<td class="num">{{ coll.nbTotalItems }}</td>
|
||||
<td class="num">{{ coll.allFiltersValue }}</td>
|
||||
<td class="num" *ngFor="let filter of results.summary.values | keyvalue">{{ coll.values[filter.key] || 0 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
</div>
|
||||
<div id="metadatadiv">
|
||||
<ngb-accordion [closeOthers]="true" activeIds="filters" #acc="ngbAccordion">
|
||||
<ngb-panel id="filters">
|
||||
<ng-template ngbPanelTitle>
|
||||
{{ "admin.reports.commons.filters" | translate }}
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<span class="col-3"></span>
|
||||
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{ "admin.reports.button.show-collections" | translate }}</button>
|
||||
</div>
|
||||
<ds-filters [filtersForm]="filtersFormGroup()"></ds-filters>
|
||||
<div class="row">
|
||||
<span class="col-3"></span>
|
||||
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{ "admin.reports.button.show-collections" | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
<ngb-panel id="collections">
|
||||
<ng-template ngbPanelTitle>
|
||||
{{ "admin.reports.collections.collections-report" | translate }}
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<table id="table" class="table table-striped">
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th rowspan="2">{{ "admin.reports.collections.community" | translate }}</th>
|
||||
<th rowspan="2">{{ "admin.reports.collections.collection" | translate }}</th>
|
||||
<th>{{ "admin.reports.collections.nb_items" | translate }}</th>
|
||||
<th>{{ "admin.reports.collections.match_all_selected_filters" | translate }}</th>
|
||||
@for (filter of results.summary.values | keyvalue; track filter) {
|
||||
<th>{{ ("admin.reports.commons.filters." + getGroup(filter.key) + "." + filter.key) | translate }}</th>
|
||||
}
|
||||
</tr>
|
||||
<tr class="header">
|
||||
<th class="num">{{ results.summary.nbTotalItems }}</th>
|
||||
<th class="num">{{ results.summary.allFiltersValue }}</th>
|
||||
@for (filter of results.summary.values | keyvalue; track filter) {
|
||||
<th class="num">{{ filter.value }}</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (coll of results.collections; track coll) {
|
||||
<tr>
|
||||
<td><a href="/handle/{{ coll.communityHandle }}" rel="noopener noreferrer" target="_blank">{{ coll.communityLabel }}</a></td>
|
||||
<td><a href="/handle/{{ coll.handle }}" rel="noopener noreferrer" target="_blank">{{ coll.label }}</a></td>
|
||||
<td class="num">{{ coll.nbTotalItems }}</td>
|
||||
<td class="num">{{ coll.allFiltersValue }}</td>
|
||||
@for (filter of results.summary.values | keyvalue; track filter) {
|
||||
<td class="num">{{ coll.values[filter.key] || 0 }}</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import {
|
||||
provideHttpClient,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
@@ -39,22 +43,21 @@ describe('FiltersComponent', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NgbAccordionModule,
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
imports: [NgbAccordionModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock,
|
||||
},
|
||||
}),
|
||||
HttpClientTestingModule,
|
||||
FilteredCollectionsComponent,
|
||||
],
|
||||
FilteredCollectionsComponent],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
DspaceRestService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
});
|
||||
}));
|
||||
|
||||
|
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
KeyValuePipe,
|
||||
NgForOf,
|
||||
} from '@angular/common';
|
||||
import { KeyValuePipe } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
@@ -37,7 +34,6 @@ import { FilteredCollections } from './filtered-collections.model';
|
||||
NgbAccordionModule,
|
||||
FiltersComponent,
|
||||
KeyValuePipe,
|
||||
NgForOf,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user