Merge branch 'main' into fix-10053-b

This commit is contained in:
Agustina Martinez
2025-03-12 08:38:56 +00:00
committed by GitHub
1222 changed files with 34356 additions and 22540 deletions

View File

@@ -293,7 +293,9 @@
], ],
"rules": { "rules": {
// Custom DSpace Angular 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"
} }
}, },
{ {

View File

@@ -8,6 +8,7 @@ on: [push, pull_request]
permissions: permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
packages: read # to fetch private images from GitHub Container Registry (GHCR)
jobs: jobs:
tests: tests:
@@ -35,6 +36,9 @@ jobs:
NODE_OPTIONS: '--max-old-space-size=4096' NODE_OPTIONS: '--max-old-space-size=4096'
# Project name to use when running "docker compose" prior to e2e tests # Project name to use when running "docker compose" prior to e2e tests
COMPOSE_PROJECT_NAME: 'ci' COMPOSE_PROJECT_NAME: 'ci'
# Docker Registry to use for Docker compose scripts below.
# We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub.
DOCKER_REGISTRY: ghcr.io
strategy: strategy:
# Create a matrix of Node versions to test against (in parallel) # Create a matrix of Node versions to test against (in parallel)
matrix: matrix:
@@ -114,6 +118,14 @@ jobs:
path: 'coverage/dspace-angular/lcov.info' path: 'coverage/dspace-angular/lcov.info'
retention-days: 14 retention-days: 14
# Login to our Docker registry, so that we can access private Docker images using "docker compose" below.
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# Using "docker compose" start backend using CI configuration # Using "docker compose" start backend using CI configuration
# and load assetstore from a cached copy # and load assetstore from a cached copy
- name: Start DSpace REST Backend via Docker (for e2e tests) - name: Start DSpace REST Backend via Docker (for e2e tests)

View File

@@ -17,6 +17,7 @@ on:
permissions: permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
packages: write # to write images to GitHub Container Registry (GHCR)
jobs: jobs:
############################################################# #############################################################

View File

@@ -1,7 +1,7 @@
# This image will be published as dspace/dspace-angular # This image will be published as dspace/dspace-angular
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details # See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
FROM node:18-alpine FROM docker.io/node:18-alpine
# Ensure Python and other build tools are available # Ensure Python and other build tools are available
# These are needed to install some node modules, especially on linux/arm64 # These are needed to install some node modules, especially on linux/arm64
@@ -22,5 +22,5 @@ ENV NODE_OPTIONS="--max_old_space_size=4096"
# Listen / accept connections from all IP addresses. # Listen / accept connections from all IP addresses.
# NOTE: At this time it is only possible to run Docker container in Production mode # NOTE: At this time it is only possible to run Docker container in Production mode
# if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485 # if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485
ENV NODE_ENV development ENV NODE_ENV=development
CMD npm run serve -- --host 0.0.0.0 CMD npm run serve -- --host 0.0.0.0

View File

@@ -4,7 +4,7 @@
# Test build: # Test build:
# docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist . # docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist .
FROM node:18-alpine AS build FROM docker.io/node:18-alpine AS build
# Ensure Python and other build tools are available # Ensure Python and other build tools are available
# These are needed to install some node modules, especially on linux/arm64 # These are needed to install some node modules, especially on linux/arm64
@@ -26,6 +26,6 @@ COPY --chown=node:node docker/dspace-ui.json /app/dspace-ui.json
WORKDIR /app WORKDIR /app
USER node USER node
ENV NODE_ENV production ENV NODE_ENV=production
EXPOSE 4000 EXPOSE 4000
CMD pm2-runtime start dspace-ui.json --json CMD pm2-runtime start dspace-ui.json --json

View File

@@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
Quick start Quick start
----------- -----------
**Ensure you're running [Node](https://nodejs.org) `v16.x` or `v18.x`, [npm](https://www.npmjs.com/) >= `v5.x`** **Ensure you're running [Node](https://nodejs.org) `v18.x` or `v20.x`, [npm](https://www.npmjs.com/) >= `v10.x`**
```bash ```bash
# clone the repo # clone the repo
@@ -90,7 +90,7 @@ Requirements
------------ ------------
- [Node.js](https://nodejs.org) - [Node.js](https://nodejs.org)
- Ensure you're running node `v16.x` or `v18.x` - Ensure you're running node `v18.x` or `v20.x`
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS. If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.

View File

@@ -23,6 +23,31 @@ ssr:
# Determining which styles are critical is a relatively expensive operation; this option is # 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. # disabled (false) by default to boost server performance at the expense of loading smoothness.
inlineCriticalCss: false 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 # The REST API server settings
# NOTE: these settings define which (publicly available) REST API to use. They are usually # NOTE: these settings define which (publicly available) REST API to use. They are usually
@@ -33,6 +58,9 @@ rest:
port: 443 port: 443
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript # NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
nameSpace: /server 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 # Caching settings
cache: cache:
@@ -448,6 +476,12 @@ search:
enabled: false enabled: false
# List of filters to enable in "Advanced Search" dropdown # List of filters to enable in "Advanced Search" dropdown
filter: [ 'title', 'author', 'subject', 'entityType' ] 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 # Notify metrics

View File

@@ -9,9 +9,11 @@ describe('Admin Add New Modals', () => {
it('Add new Community modal should pass accessibility tests', () => { it('Add new Community modal should pass accessibility tests', () => {
// Pin the sidebar open // Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click(); cy.get('[data-test="sidebar-collapse-toggle"]').click();
// Click on entry of menu // Click on entry of menu
cy.get('[data-test="admin-menu-section-new-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-new-title"]').click(); cy.get('[data-test="admin-menu-section-new-title"]').click();
cy.get('a[data-test="menu.section.new_community"]').click(); cy.get('a[data-test="menu.section.new_community"]').click();
@@ -22,9 +24,11 @@ describe('Admin Add New Modals', () => {
it('Add new Collection modal should pass accessibility tests', () => { it('Add new Collection modal should pass accessibility tests', () => {
// Pin the sidebar open // Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click(); cy.get('[data-test="sidebar-collapse-toggle"]').click();
// Click on entry of menu // Click on entry of menu
cy.get('[data-test="admin-menu-section-new-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-new-title"]').click(); cy.get('[data-test="admin-menu-section-new-title"]').click();
cy.get('a[data-test="menu.section.new_collection"]').click(); cy.get('a[data-test="menu.section.new_collection"]').click();
@@ -35,9 +39,11 @@ describe('Admin Add New Modals', () => {
it('Add new Item modal should pass accessibility tests', () => { it('Add new Item modal should pass accessibility tests', () => {
// Pin the sidebar open // Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click(); cy.get('[data-test="sidebar-collapse-toggle"]').click();
// Click on entry of menu // Click on entry of menu
cy.get('[data-test="admin-menu-section-new-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-new-title"]').click(); cy.get('[data-test="admin-menu-section-new-title"]').click();
cy.get('a[data-test="menu.section.new_item"]').click(); cy.get('a[data-test="menu.section.new_item"]').click();

View File

@@ -9,10 +9,12 @@ describe('Admin Edit Modals', () => {
it('Edit Community modal should pass accessibility tests', () => { it('Edit Community modal should pass accessibility tests', () => {
// Pin the sidebar open // Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click(); cy.get('[data-test="sidebar-collapse-toggle"]').click();
// Click on entry of menu // Click on entry of menu
cy.get('#admin-menu-section-edit-title').click(); cy.get('[data-test="admin-menu-section-edit-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-edit-title"]').click();
cy.get('a[data-test="menu.section.edit_community"]').click(); cy.get('a[data-test="menu.section.edit_community"]').click();
@@ -22,10 +24,12 @@ describe('Admin Edit Modals', () => {
it('Edit Collection modal should pass accessibility tests', () => { it('Edit Collection modal should pass accessibility tests', () => {
// Pin the sidebar open // Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click(); cy.get('[data-test="sidebar-collapse-toggle"]').click();
// Click on entry of menu // Click on entry of menu
cy.get('#admin-menu-section-edit-title').click(); cy.get('[data-test="admin-menu-section-edit-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-edit-title"]').click();
cy.get('a[data-test="menu.section.edit_collection"]').click(); cy.get('a[data-test="menu.section.edit_collection"]').click();
@@ -35,10 +39,12 @@ describe('Admin Edit Modals', () => {
it('Edit Item modal should pass accessibility tests', () => { it('Edit Item modal should pass accessibility tests', () => {
// Pin the sidebar open // Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click(); cy.get('[data-test="sidebar-collapse-toggle"]').click();
// Click on entry of menu // Click on entry of menu
cy.get('#admin-menu-section-edit-title').click(); cy.get('[data-test="admin-menu-section-edit-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-edit-title"]').click();
cy.get('a[data-test="menu.section.edit_item"]').click(); cy.get('a[data-test="menu.section.edit_item"]').click();

View File

@@ -9,10 +9,12 @@ describe('Admin Export Modals', () => {
it('Export metadata modal should pass accessibility tests', () => { it('Export metadata modal should pass accessibility tests', () => {
// Pin the sidebar open // Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click(); cy.get('[data-test="sidebar-collapse-toggle"]').click();
// Click on entry of menu // Click on entry of menu
cy.get('#admin-menu-section-export-title').click(); cy.get('[data-test="admin-menu-section-export-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-export-title"]').click();
cy.get('a[data-test="menu.section.export_metadata"]').click(); cy.get('a[data-test="menu.section.export_metadata"]').click();
@@ -22,10 +24,12 @@ describe('Admin Export Modals', () => {
it('Export batch modal should pass accessibility tests', () => { it('Export batch modal should pass accessibility tests', () => {
// Pin the sidebar open // Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click(); cy.get('[data-test="sidebar-collapse-toggle"]').click();
// Click on entry of menu // Click on entry of menu
cy.get('#admin-menu-section-export-title').click(); cy.get('[data-test="admin-menu-section-export-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-export-title"]').click();
cy.get('a[data-test="menu.section.export_batch"]').click(); cy.get('a[data-test="menu.section.export_batch"]').click();

View File

@@ -12,6 +12,13 @@ describe('Community List Page', () => {
cy.get('[data-test="expand-button"]').click({ multiple: true }); cy.get('[data-test="expand-button"]').click({ multiple: true });
// Analyze <ds-community-list-page> for accessibility issues // 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 },
},
});
}); });
}); });

View File

@@ -9,18 +9,15 @@ beforeEach(() => {
// This page is restricted, so we will be shown the login form. Fill it out & submit. // This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
// We need to wait for the correction types allowed for the item to be loaded to be sure that each tab is fully loaded.
// This because the edit item page causes often tests to fails due to timeout.
cy.intercept('GET', 'server/api/config/correctiontypes/search/findByItem*').as('correctionTypes');
cy.wait('@correctionTypes');
}); });
describe('Edit Item > Edit Metadata tab', () => { describe('Edit Item > Edit Metadata tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="metadata"]').should('be.visible');
cy.get('a[data-test="metadata"]').click(); cy.get('a[data-test="metadata"]').click();
// Our selected tab should be active // Our selected tab should be both visible & active
cy.get('a[data-test="metadata"]').should('be.visible');
cy.get('a[data-test="metadata"]').should('have.class', 'active'); cy.get('a[data-test="metadata"]').should('have.class', 'active');
// <ds-edit-item-page> tag must be loaded // <ds-edit-item-page> tag must be loaded
@@ -39,9 +36,11 @@ describe('Edit Item > Edit Metadata tab', () => {
describe('Edit Item > Status tab', () => { describe('Edit Item > Status tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="status"]').should('be.visible');
cy.get('a[data-test="status"]').click(); cy.get('a[data-test="status"]').click();
// Our selected tab should be active // Our selected tab should be both visible & active
cy.get('a[data-test="status"]').should('be.visible');
cy.get('a[data-test="status"]').should('have.class', 'active'); cy.get('a[data-test="status"]').should('have.class', 'active');
// <ds-item-status> tag must be loaded // <ds-item-status> tag must be loaded
@@ -55,9 +54,11 @@ describe('Edit Item > Status tab', () => {
describe('Edit Item > Bitstreams tab', () => { describe('Edit Item > Bitstreams tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="bitstreams"]').should('be.visible');
cy.get('a[data-test="bitstreams"]').click(); cy.get('a[data-test="bitstreams"]').click();
// Our selected tab should be active // Our selected tab should be both visible & active
cy.get('a[data-test="bitstreams"]').should('be.visible');
cy.get('a[data-test="bitstreams"]').should('have.class', 'active'); cy.get('a[data-test="bitstreams"]').should('have.class', 'active');
// <ds-item-bitstreams> tag must be loaded // <ds-item-bitstreams> tag must be loaded
@@ -82,9 +83,11 @@ describe('Edit Item > Bitstreams tab', () => {
describe('Edit Item > Curate tab', () => { describe('Edit Item > Curate tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="curate"]').should('be.visible');
cy.get('a[data-test="curate"]').click(); cy.get('a[data-test="curate"]').click();
// Our selected tab should be active // Our selected tab should be both visible & active
cy.get('a[data-test="curate"]').should('be.visible');
cy.get('a[data-test="curate"]').should('have.class', 'active'); cy.get('a[data-test="curate"]').should('have.class', 'active');
// <ds-item-curate> tag must be loaded // <ds-item-curate> tag must be loaded
@@ -98,9 +101,11 @@ describe('Edit Item > Curate tab', () => {
describe('Edit Item > Relationships tab', () => { describe('Edit Item > Relationships tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="relationships"]').should('be.visible');
cy.get('a[data-test="relationships"]').click(); cy.get('a[data-test="relationships"]').click();
// Our selected tab should be active // Our selected tab should be both visible & active
cy.get('a[data-test="relationships"]').should('be.visible');
cy.get('a[data-test="relationships"]').should('have.class', 'active'); cy.get('a[data-test="relationships"]').should('have.class', 'active');
// <ds-item-relationships> tag must be loaded // <ds-item-relationships> tag must be loaded
@@ -114,9 +119,11 @@ describe('Edit Item > Relationships tab', () => {
describe('Edit Item > Version History tab', () => { describe('Edit Item > Version History tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="versionhistory"]').should('be.visible');
cy.get('a[data-test="versionhistory"]').click(); cy.get('a[data-test="versionhistory"]').click();
// Our selected tab should be active // Our selected tab should be both visible & active
cy.get('a[data-test="versionhistory"]').should('be.visible');
cy.get('a[data-test="versionhistory"]').should('have.class', 'active'); cy.get('a[data-test="versionhistory"]').should('have.class', 'active');
// <ds-item-version-history> tag must be loaded // <ds-item-version-history> tag must be loaded
@@ -130,9 +137,11 @@ describe('Edit Item > Version History tab', () => {
describe('Edit Item > Access Control tab', () => { describe('Edit Item > Access Control tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="access-control"]').should('be.visible');
cy.get('a[data-test="access-control"]').click(); cy.get('a[data-test="access-control"]').click();
// Our selected tab should be active // Our selected tab should be both visible & active
cy.get('a[data-test="access-control"]').should('be.visible');
cy.get('a[data-test="access-control"]').should('have.class', 'active'); cy.get('a[data-test="access-control"]').should('have.class', 'active');
// <ds-item-access-control> tag must be loaded // <ds-item-access-control> tag must be loaded
@@ -146,9 +155,11 @@ describe('Edit Item > Access Control tab', () => {
describe('Edit Item > Collection Mapper tab', () => { describe('Edit Item > Collection Mapper tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="mapper"]').should('be.visible');
cy.get('a[data-test="mapper"]').click(); cy.get('a[data-test="mapper"]').click();
// Our selected tab should be active // Our selected tab should be both visible & active
cy.get('a[data-test="mapper"]').should('be.visible');
cy.get('a[data-test="mapper"]').should('have.class', 'active'); cy.get('a[data-test="mapper"]').should('have.class', 'active');
// <ds-item-collection-mapper> tag must be loaded // <ds-item-collection-mapper> tag must be loaded

View File

@@ -217,7 +217,7 @@ describe('New Submission page', () => {
}); });
// Close popup window // 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 // 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 // Clicking it will display a confirmation, which we will confirm with another click

View File

@@ -21,7 +21,7 @@ networks:
external: true external: true
services: services:
dspace-cli: dspace-cli:
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}" image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}"
container_name: dspace-cli container_name: dspace-cli
environment: environment:
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. # Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.

View File

@@ -14,7 +14,7 @@
# # Therefore, it should be kept in sync with that file # # Therefore, it should be kept in sync with that file
services: services:
dspacedb: dspacedb:
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql" image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql"
environment: environment:
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace # This LOADSQL should be kept in sync with the URL in DSpace/DSpace
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data

View File

@@ -33,7 +33,7 @@ services:
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly. # This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
solr__D__statistics__P__autoCommit: 'false' solr__D__statistics__P__autoCommit: 'false'
LOGGING_CONFIG: /dspace/config/log4j2-container.xml LOGGING_CONFIG: /dspace/config/log4j2-container.xml
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
depends_on: depends_on:
- dspacedb - dspacedb
networks: networks:
@@ -60,7 +60,7 @@ services:
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data # NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
dspacedb: dspacedb:
container_name: dspacedb container_name: dspacedb
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql" image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql"
environment: environment:
# This LOADSQL should be kept in sync with the LOADSQL in # This LOADSQL should be kept in sync with the LOADSQL in
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
@@ -81,7 +81,7 @@ services:
# DSpace Solr container # DSpace Solr container
dspacesolr: dspacesolr:
container_name: dspacesolr container_name: dspacesolr
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
networks: networks:
- dspacenet - dspacenet
ports: ports:

View File

@@ -26,7 +26,7 @@ services:
DSPACE_REST_HOST: sandbox.dspace.org DSPACE_REST_HOST: sandbox.dspace.org
DSPACE_REST_PORT: 443 DSPACE_REST_PORT: 443
DSPACE_REST_NAMESPACE: /server DSPACE_REST_NAMESPACE: /server
image: dspace/dspace-angular:${DSPACE_VER:-latest}-dist image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-latest}-dist"
build: build:
context: .. context: ..
dockerfile: Dockerfile.dist dockerfile: Dockerfile.dist

View File

@@ -40,7 +40,7 @@ services:
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above. # from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
proxies__P__trusted__P__ipranges: '172.23.0' proxies__P__trusted__P__ipranges: '172.23.0'
LOGGING_CONFIG: /dspace/config/log4j2-container.xml LOGGING_CONFIG: /dspace/config/log4j2-container.xml
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
depends_on: depends_on:
- dspacedb - dspacedb
networks: networks:
@@ -68,7 +68,7 @@ services:
dspacedb: dspacedb:
container_name: dspacedb container_name: dspacedb
# Uses a custom Postgres image with pgcrypto installed # Uses a custom Postgres image with pgcrypto installed
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}" image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
environment: environment:
PGDATA: /pgdata PGDATA: /pgdata
POSTGRES_PASSWORD: dspace POSTGRES_PASSWORD: dspace
@@ -85,7 +85,7 @@ services:
# DSpace Solr container # DSpace Solr container
dspacesolr: dspacesolr:
container_name: dspacesolr container_name: dspacesolr
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
networks: networks:
- dspacenet - dspacenet
ports: ports:

View File

@@ -23,7 +23,7 @@ services:
DSPACE_REST_HOST: localhost DSPACE_REST_HOST: localhost
DSPACE_REST_PORT: 8080 DSPACE_REST_PORT: 8080
DSPACE_REST_NAMESPACE: /server DSPACE_REST_NAMESPACE: /server
image: dspace/dspace-angular:${DSPACE_VER:-latest} image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-latest}"
build: build:
context: .. context: ..
dockerfile: Dockerfile dockerfile: Dockerfile

View File

@@ -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/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.

View 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 &amp; 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>
```

View File

@@ -10,10 +10,13 @@ import {
bundle, bundle,
RuleExports, RuleExports,
} from '../../util/structure'; } from '../../util/structure';
import * as noDisabledAttributeOnButton from './no-disabled-attribute-on-button';
import * as themedComponentUsages from './themed-component-usages'; import * as themedComponentUsages from './themed-component-usages';
const index = [ const index = [
themedComponentUsages, themedComponentUsages,
noDisabledAttributeOnButton,
] as unknown as RuleExports[]; ] as unknown as RuleExports[];
export = { export = {

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

11928
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -57,106 +57,103 @@
"private": true, "private": true,
"overrides": { "overrides": {
"@kolkov/ngx-gallery": { "@kolkov/ngx-gallery": {
"@angular/animations": "^17.3.11", "@angular/animations": "^18.2.12",
"@angular/common": "^17.3.11", "@angular/common": "^18.2.12",
"@angular/core": "^17.3.11" "@angular/core": "^18.2.12"
}, },
"@ng-bootstrap/ng-bootstrap": { "@ng-bootstrap/ng-bootstrap": {
"@angular/common": "^17.3.11", "@angular/common": "^18.2.12",
"@angular/core": "^17.3.11", "@angular/core": "^18.2.12",
"@angular/forms": "^17.3.11", "@angular/forms": "^18.2.12",
"@angular/localize": "^17.3.11" "@angular/localize": "^18.2.12"
}, },
"@ng-dynamic-forms/core": { "@ng-dynamic-forms/core": {
"@angular/common": "^17.3.11", "@angular/common": "^18.2.12",
"@angular/core": "^17.3.11", "@angular/core": "^18.2.12",
"@angular/forms": "^17.3.11" "@angular/forms": "^18.2.12"
}, },
"@ng-dynamic-forms/ui-ng-bootstrap": { "@ng-dynamic-forms/ui-ng-bootstrap": {
"ngx-mask": "14.2.4" "ngx-mask": "14.2.4",
}, "@ng-bootstrap/ng-bootstrap": "^12.0.0",
"@ngtools/webpack": { "bootstrap": "^5.3"
"@angular/compiler-cli": "^17.3.11",
"typescript": "~5.4.5"
}, },
"@nicky-lenaers/ngx-scroll-to": { "@nicky-lenaers/ngx-scroll-to": {
"@angular/common": "^17.3.11", "@angular/common": "^18.2.12",
"@angular/core": "^17.3.11" "@angular/core": "^18.2.12"
}, },
"eslint-plugin-unused-imports": { "eslint-plugin-unused-imports": {
"@typescript-eslint/eslint-plugin": "^7.2.0" "@typescript-eslint/eslint-plugin": "^7.2.0"
}, },
"ng2-file-upload": {
"@angular/common": "^17.3.11",
"@angular/core": "^17.3.11"
},
"ngx-infinite-scroll": { "ngx-infinite-scroll": {
"@angular/common": "^17.3.11", "@angular/common": "^18.2.12",
"@angular/core": "^17.3.11" "@angular/core": "^18.2.12"
} },
"notistack": "3.0.1"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^17.3.12", "@angular/animations": "^18.2.12",
"@angular/cdk": "^17.3.10", "@angular/cdk": "^18.2.12",
"@angular/common": "^17.3.12", "@angular/common": "^18.2.12",
"@angular/compiler": "^17.3.12", "@angular/compiler": "^18.2.12",
"@angular/core": "^17.3.12", "@angular/core": "^18.2.12",
"@angular/forms": "^17.3.12", "@angular/forms": "^18.2.12",
"@angular/localize": "^17.3.12", "@angular/localize": "^18.2.12",
"@angular/platform-browser": "^17.3.12", "@angular/platform-browser": "^18.2.12",
"@angular/platform-browser-dynamic": "^17.3.12", "@angular/platform-browser-dynamic": "^18.2.12",
"@angular/platform-server": "^17.3.12", "@angular/platform-server": "^18.2.12",
"@angular/router": "^17.3.12", "@angular/router": "^18.2.12",
"@angular/ssr": "^17.3.11", "@angular/ssr": "^18.2.12",
"@babel/runtime": "7.26.0", "@babel/runtime": "7.26.0",
"@kolkov/ngx-gallery": "^2.0.1", "@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/core": "^16.0.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0", "@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
"@ngrx/effects": "^17.1.1", "@ngrx/effects": "^18.1.1",
"@ngrx/router-store": "^17.1.1", "@ngrx/operators": "^18.0.0",
"@ngrx/store": "^17.1.1", "@ngrx/router-store": "^18.1.1",
"@ngx-translate/core": "^14.0.0", "@ngrx/store": "^18.1.1",
"@ngx-translate/core": "^16.0.3",
"@nicky-lenaers/ngx-scroll-to": "^14.0.0", "@nicky-lenaers/ngx-scroll-to": "^14.0.0",
"angulartics2": "^12.2.0", "angulartics2": "^12.2.0",
"axios": "^1.7.4", "axios": "^1.7.9",
"bootstrap": "^4.6.1", "bootstrap": "^5.3",
"cerialize": "0.1.18", "cerialize": "0.1.18",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
"colors": "^1.4.0", "colors": "^1.4.0",
"compression": "^1.7.5", "compression": "^1.7.5",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"core-js": "^3.38.1", "core-js": "^3.40.0",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7", "date-fns-tz": "^1.3.7",
"deepmerge": "^4.3.1", "deepmerge": "^4.3.1",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^4.21.1", "express": "^4.21.2",
"express-rate-limit": "^5.1.3", "express-rate-limit": "^5.1.3",
"fast-json-patch": "^3.1.1", "fast-json-patch": "^3.1.1",
"filesize": "^6.1.0", "filesize": "^6.1.0",
"http-proxy-middleware": "^2.0.7", "http-proxy-middleware": "^2.0.7",
"http-terminator": "^3.2.0", "http-terminator": "^3.2.0",
"isbot": "^5.1.17", "isbot": "^5.1.22",
"js-cookie": "2.2.1", "js-cookie": "2.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"json5": "^2.2.3", "json5": "^2.2.3",
"jsonschema": "1.4.1", "jsonschema": "1.5.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lru-cache": "^7.14.1", "lru-cache": "^7.14.1",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"mirador": "^3.3.0", "mirador": "^3.4.3",
"mirador-dl-plugin": "^0.13.0", "mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.16.0", "mirador-share-plugin": "^0.16.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ng2-file-upload": "5.0.0", "ng2-file-upload": "7.0.1",
"ng2-nouislider": "^2.0.0", "ng2-nouislider": "^2.0.0",
"ngx-infinite-scroll": "^16.0.0", "ngx-infinite-scroll": "^18.0.0",
"ngx-pagination": "6.0.3", "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", "nouislider": "^15.7.1",
"orejime": "^2.3.0", "orejime": "^2.3.1",
"pem": "1.14.8", "pem": "1.14.8",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.0", "rxjs": "^7.8.0",
@@ -164,29 +161,29 @@
"zone.js": "~0.14.10" "zone.js": "~0.14.10"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "~17.0.2", "@angular-builders/custom-webpack": "~18.0.0",
"@angular-devkit/build-angular": "^17.3.11", "@angular-devkit/build-angular": "^18.2.12",
"@angular-eslint/builder": "^17.5.3", "@angular-eslint/builder": "^18.4.1",
"@angular-eslint/bundled-angular-compiler": "^17.5.3", "@angular-eslint/bundled-angular-compiler": "^18.4.1",
"@angular-eslint/eslint-plugin": "^17.5.3", "@angular-eslint/eslint-plugin": "^18.4.1",
"@angular-eslint/eslint-plugin-template": "^17.5.3", "@angular-eslint/eslint-plugin-template": "^18.4.1",
"@angular-eslint/schematics": "^17.5.3", "@angular-eslint/schematics": "^18.4.1",
"@angular-eslint/template-parser": "^17.5.3", "@angular-eslint/template-parser": "^18.4.1",
"@angular-eslint/utils": "^17.5.3", "@angular-eslint/utils": "^18.4.1",
"@angular/cli": "^17.3.11", "@angular/cli": "^18.2.12",
"@angular/compiler-cli": "^17.3.11", "@angular/compiler-cli": "^18.2.12",
"@angular/language-service": "^17.3.12", "@angular/language-service": "^18.2.12",
"@cypress/schematic": "^1.5.0", "@cypress/schematic": "^1.5.0",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.7.2",
"@ngrx/store-devtools": "^17.1.1", "@ngrx/store-devtools": "^18.1.1",
"@ngtools/webpack": "^16.2.16", "@ngtools/webpack": "^18.2.12",
"@types/deep-freeze": "0.1.5", "@types/deep-freeze": "0.1.5",
"@types/ejs": "^3.1.2", "@types/ejs": "^3.1.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/grecaptcha": "^3.0.9", "@types/grecaptcha": "^3.0.9",
"@types/jasmine": "~3.6.0", "@types/jasmine": "~3.6.0",
"@types/js-cookie": "2.2.6", "@types/js-cookie": "2.2.6",
"@types/lodash": "^4.17.13", "@types/lodash": "^4.17.15",
"@types/node": "^14.14.9", "@types/node": "^14.14.9",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.18.0",
@@ -196,8 +193,8 @@
"compression-webpack-plugin": "^9.2.0", "compression-webpack-plugin": "^9.2.0",
"copy-webpack-plugin": "^6.4.1", "copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cypress": "^13.16.0", "cypress": "^13.17.0",
"cypress-axe": "^1.5.0", "cypress-axe": "^1.6.0",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"eslint": "^8.39.0", "eslint": "^8.39.0",
"eslint-plugin-deprecation": "^1.4.1", "eslint-plugin-deprecation": "^1.4.1",
@@ -206,12 +203,12 @@
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-import-newlines": "^1.3.1", "eslint-plugin-import-newlines": "^1.3.1",
"eslint-plugin-jsdoc": "^45.0.0", "eslint-plugin-jsdoc": "^45.0.0",
"eslint-plugin-jsonc": "^2.6.0", "eslint-plugin-jsonc": "^2.19.1",
"eslint-plugin-lodash": "^7.4.0", "eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-rxjs": "^5.0.3", "eslint-plugin-rxjs": "^5.0.3",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^3.2.0", "eslint-plugin-unused-imports": "^3.2.0",
"express-static-gzip": "^2.1.8", "express-static-gzip": "^2.2.0",
"jasmine": "^3.8.0", "jasmine": "^3.8.0",
"jasmine-core": "^3.8.0", "jasmine-core": "^3.8.0",
"jasmine-marbles": "0.9.2", "jasmine-marbles": "0.9.2",
@@ -221,20 +218,20 @@
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "^1.5.0",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"ng-mocks": "^14.13.1", "ng-mocks": "^14.13.2",
"ngx-mask": "14.2.4", "ngx-mask": "14.2.4",
"nodemon": "^2.0.22", "nodemon": "^2.0.22",
"postcss": "^8.4", "postcss": "^8.5",
"postcss-import": "^14.0.0", "postcss-import": "^14.0.0",
"postcss-loader": "^4.0.3", "postcss-loader": "^4.0.3",
"postcss-preset-env": "^7.4.2", "postcss-preset-env": "^7.4.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "~1.80.6", "sass": "~1.84.0",
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",
"sass-resources-loader": "^2.2.5", "sass-resources-loader": "^2.2.5",
"ts-node": "^8.10.2", "ts-node": "^8.10.2",
"typescript": "~5.4.5", "typescript": "~5.4.5",
"webpack": "5.96.1", "webpack": "5.97.1",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1" "webpack-dev-server": "^4.15.1"
} }

View File

@@ -20,10 +20,10 @@ import 'reflect-metadata';
/* eslint-disable import/no-namespace */ /* eslint-disable import/no-namespace */
import * as morgan from 'morgan'; import * as morgan from 'morgan';
import * as express from 'express'; import express from 'express';
import * as ejs from 'ejs'; import * as ejs from 'ejs';
import * as compression from 'compression'; import * as compression from 'compression';
import * as expressStaticGzip from 'express-static-gzip'; import expressStaticGzip from 'express-static-gzip';
/* eslint-enable import/no-namespace */ /* eslint-enable import/no-namespace */
import axios from 'axios'; import axios from 'axios';
import LRU from 'lru-cache'; import LRU from 'lru-cache';
@@ -81,6 +81,9 @@ let anonymousCache: LRU<string, any>;
// extend environment with app config for server // extend environment with app config for server
extendEnvironmentWithAppConfig(environment, appConfig); 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. // The Express app is exported so that it can be used by serverless Functions.
export function app() { export function app() {
@@ -156,7 +159,7 @@ export function app() {
* Proxy the sitemaps * Proxy the sitemaps
*/ */
router.use('/sitemap**', createProxyMiddleware({ router.use('/sitemap**', createProxyMiddleware({
target: `${environment.rest.baseUrl}/sitemaps`, target: `${REST_BASE_URL}/sitemaps`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true, changeOrigin: true,
})); }));
@@ -165,7 +168,7 @@ export function app() {
* Proxy the linksets * Proxy the linksets
*/ */
router.use('/signposting**', createProxyMiddleware({ router.use('/signposting**', createProxyMiddleware({
target: `${environment.rest.baseUrl}`, target: `${REST_BASE_URL}`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true, changeOrigin: true,
})); }));
@@ -218,7 +221,7 @@ export function app() {
* The callback function to serve server side angular * The callback function to serve server side angular
*/ */
function ngApp(req, res, next) { function ngApp(req, res, next) {
if (environment.ssr.enabled) { if (environment.ssr.enabled && req.method === 'GET' && (req.path === '/' || environment.ssr.paths.some(pathPrefix => req.path.startsWith(pathPrefix)))) {
// Render the page to user via SSR (server side rendering) // Render the page to user via SSR (server side rendering)
serverSideRender(req, res, next); serverSideRender(req, res, next);
} else { } else {
@@ -266,6 +269,11 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) {
}) })
.then((html) => { .then((html) => {
if (hasValue(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) // save server side rendered page to cache (if any are enabled)
saveToCache(req, html); saveToCache(req, html);
if (sendToUser) { if (sendToUser) {
@@ -623,7 +631,7 @@ function start() {
* The callback function to serve health check requests * The callback function to serve health check requests
*/ */
function healthCheck(req, res) { function healthCheck(req, res) {
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`; const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
axios.get(baseUrl) axios.get(baseUrl)
.then((response) => { .then((response) => {
res.status(response.status).send(response.data); res.status(response.status).send(response.data);

View File

@@ -1,19 +1,13 @@
<ngb-accordion #acc="ngbAccordion" [activeIds]="'browse'"> <ngb-accordion #acc="ngbAccordion" [activeIds]="'browse'">
<ngb-panel [id]="'browse'"> <ngb-panel [id]="'browse'">
<ng-template ngbPanelHeader> <ng-template ngbPanelTitle>
<div class="w-100 d-flex gap-3 justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('browse')" <div class="w-100 d-flex gap-3 justify-content-between collapse-toggle" (click)="acc.toggle('browse')"
data-test="browse"> data-test="browse">
<button type="button" class="btn btn-link p-0" (click)="$event.preventDefault()" <button type="button" class="btn btn-link p-0" (click)="$event.preventDefault()"
[attr.aria-expanded]="acc.isExpanded('browse')" [attr.aria-expanded]="acc.isExpanded('browse')"
aria-controls="bulk-access-browse-panel-content"> aria-controls="bulk-access-browse-panel-content">
{{ 'admin.access-control.bulk-access-browse.header' | translate }} {{ 'admin.access-control.bulk-access-browse.header' | translate }}
</button> </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> </div>
</ng-template> </ng-template>
<ng-template ngbPanelContent> <ng-template ngbPanelContent>
@@ -22,7 +16,7 @@
<li [ngbNavItem]="'search'" role="presentation"> <li [ngbNavItem]="'search'" role="presentation">
<a ngbNavLink>{{'admin.access-control.bulk-access-browse.search.header' | translate}}</a> <a ngbNavLink>{{'admin.access-control.bulk-access-browse.search.header' | translate}}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<div class="mx-n3"> <div class="bulk-access-search">
<ds-search [configuration]="'administrativeBulkAccess'" <ds-search [configuration]="'administrativeBulkAccess'"
[selectable]="true" [selectable]="true"
[selectionConfig]="{ repeatable: true, listId: listId }" [selectionConfig]="{ repeatable: true, listId: listId }"
@@ -42,9 +36,11 @@
[showPaginator]="false" [showPaginator]="false"
(prev)="pagePrev()" (prev)="pagePrev()"
(next)="pageNext()"> (next)="pageNext()">
<ul *ngIf="(objectsSelected$|async)?.hasSucceeded" class="list-unstyled ml-4"> @if ((objectsSelected$|async)?.hasSucceeded) {
<li *ngFor='let object of (objectsSelected$|async)?.payload?.page | paginate: { itemsPerPage: (paginationOptions$ | async).pageSize, <ul class="list-unstyled ms-4">
currentPage: (paginationOptions$ | async).currentPage, totalItems: (objectsSelected$|async)?.payload?.page.length }; let i = index; let last = last ' @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" class="mt-4 mb-4 d-flex"
[attr.data-test]="'list-object' | dsBrowserOnly"> [attr.data-test]="'list-object' | dsBrowserOnly">
<ds-selectable-list-item-control [index]="i" <ds-selectable-list-item-control [index]="i"
@@ -56,7 +52,9 @@
[showThumbnails]="false" [showThumbnails]="false"
[viewMode]="'list'"></ds-listable-object-component-loader> [viewMode]="'list'"></ds-listable-object-component-loader>
</li> </li>
}
</ul> </ul>
}
</ds-pagination> </ds-pagination>
</ng-template> </ng-template>
</li> </li>

View File

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

View File

@@ -1,8 +1,4 @@
import { import { AsyncPipe } from '@angular/common';
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
Input, Input,
@@ -59,11 +55,9 @@ import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
AsyncPipe, AsyncPipe,
NgbAccordionModule, NgbAccordionModule,
TranslateModule, TranslateModule,
NgIf,
NgbNavModule, NgbNavModule,
ThemedSearchComponent, ThemedSearchComponent,
BrowserOnlyPipe, BrowserOnlyPipe,
NgForOf,
NgxPaginationModule, NgxPaginationModule,
SelectableListItemControlComponent, SelectableListItemControlComponent,
ListableObjectComponentLoaderComponent, ListableObjectComponentLoaderComponent,

View File

@@ -7,10 +7,10 @@
<hr> <hr>
<div class="d-flex justify-content-end"> <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 }} {{ 'access-control-cancel' | translate }}
</button> </button>
<button class="btn btn-primary" [disabled]="!canExport()" (click)="submit()"> <button class="btn btn-primary" [dsBtnDisabled]="!canExport()" (click)="submit()">
{{ 'access-control-execute' | translate }} {{ 'access-control-execute' | translate }}
</button> </button>
</div> </div>

View File

@@ -14,6 +14,7 @@ import {
} from 'rxjs/operators'; } from 'rxjs/operators';
import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; 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 { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { BulkAccessBrowseComponent } from './browse/bulk-access-browse.component'; import { BulkAccessBrowseComponent } from './browse/bulk-access-browse.component';
@@ -27,6 +28,7 @@ import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.com
TranslateModule, TranslateModule,
BulkAccessSettingsComponent, BulkAccessSettingsComponent,
BulkAccessBrowseComponent, BulkAccessBrowseComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -1,17 +1,11 @@
<ngb-accordion #acc="ngbAccordion" [activeIds]="'settings'"> <ngb-accordion #acc="ngbAccordion" [activeIds]="'settings'">
<ngb-panel [id]="'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"> <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')" <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"> aria-controls="bulk-access-settings-panel-content">
{{ 'admin.access-control.bulk-access-settings.header' | translate }} {{ 'admin.access-control.bulk-access-settings.header' | translate }}
</button> </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> </div>
</ng-template> </ng-template>
<ng-template ngbPanelContent> <ng-template ngbPanelContent>

View File

@@ -1,4 +1,4 @@
import { NgIf } from '@angular/common';
import { import {
Component, Component,
ViewChild, ViewChild,
@@ -16,7 +16,6 @@ import { AccessControlFormContainerComponent } from '../../../shared/access-cont
imports: [ imports: [
NgbAccordionModule, NgbAccordionModule,
TranslateModule, TranslateModule,
NgIf,
AccessControlFormContainerComponent, AccessControlFormContainerComponent,
], ],
standalone: true, standalone: true,

View File

@@ -5,10 +5,10 @@
<h1 id="header" class="pb-2">{{labelPrefix + 'head' | translate}}</h1> <h1 id="header" class="pb-2">{{labelPrefix + 'head' | translate}}</h1>
<div> <div>
<button class="mr-auto btn btn-success addEPerson-button" <button class="me-auto btn btn-success addEPerson-button"
[routerLink]="'create'"> [routerLink]="'create'">
<i class="fas fa-plus"></i> <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> </button>
</div> </div>
</div> </div>
@@ -18,13 +18,13 @@
</h2> </h2>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between"> <form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
<div> <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="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option> <option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
</select> </select>
</div> </div>
<div class="flex-grow-1 mr-3 ml-3"> <div class="flex-grow-1 me-3 ms-3">
<div class="form-group input-group"> <div class="mb-3 input-group">
<input type="text" name="query" id="query" formControlName="query" <input type="text" name="query" id="query" formControlName="query"
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate" class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
[placeholder]="(labelPrefix + 'search.placeholder' | translate)"> [placeholder]="(labelPrefix + 'search.placeholder' | translate)">
@@ -41,14 +41,15 @@
</div> </div>
</form> </form>
<ds-loading *ngIf="searching$ | async"></ds-loading> @if (searching$ | async) {
<ds-loading></ds-loading>
}
@if ((pageInfoState$ | async)?.totalElements > 0 && (searching$ | async) !== true) {
<ds-pagination <ds-pagination
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (searching$ | async) !== true"
[paginationOptions]="config" [paginationOptions]="config"
[collectionSize]="(pageInfoState$ | async)?.totalElements" [collectionSize]="(pageInfoState$ | async)?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">
<div class="table-responsive"> <div class="table-responsive">
<table id="epeople" class="table table-striped table-hover table-bordered"> <table id="epeople" class="table table-striped table-hover table-bordered">
<thead> <thead>
@@ -60,7 +61,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page" @for (epersonDto of (ePeopleDto$ | async)?.page; track epersonDto) {
<tr
[ngClass]="{'table-primary' : (activeEPerson$ | async) === epersonDto.eperson}"> [ngClass]="{'table-primary' : (activeEPerson$ | async) === epersonDto.eperson}">
<td>{{epersonDto.eperson.id}}</td> <td>{{epersonDto.eperson.id}}</td>
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td> <td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
@@ -72,23 +74,28 @@
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}"> title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
<button *ngIf="epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)" @if (epersonDto.ableToDelete) {
<button (click)="deleteEPerson(epersonDto.eperson)"
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton" class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}"> title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
}
</div> </div>
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
}
<div *ngIf="(pageInfoState$ | async)?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert"> @if ((pageInfoState$ | async)?.totalElements === 0) {
<div class="alert alert-info w-100 mb-2" role="alert">
{{labelPrefix + 'no-items' | translate}} {{labelPrefix + 'no-items' | translate}}
</div> </div>
}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -42,6 +42,7 @@ import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { PageInfo } from '../../core/shared/page-info.model'; 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 { FormBuilderService } from '../../shared/form/builder/form-builder.service';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock';
@@ -151,7 +152,7 @@ describe('EPeopleRegistryComponent', () => {
paginationService = new PaginationServiceStub(); paginationService = new PaginationServiceStub();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule.withRoutes([]), imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule.withRoutes([]),
TranslateModule.forRoot(), EPeopleRegistryComponent], TranslateModule.forRoot(), EPeopleRegistryComponent, BtnDisabledDirective],
providers: [ providers: [
{ provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },

View File

@@ -1,8 +1,6 @@
import { import {
AsyncPipe, AsyncPipe,
NgClass, NgClass,
NgForOf,
NgIf,
} from '@angular/common'; } from '@angular/common';
import { import {
Component, Component,
@@ -72,13 +70,11 @@ import { EPersonFormComponent } from './eperson-form/eperson-form.component';
TranslateModule, TranslateModule,
RouterModule, RouterModule,
AsyncPipe, AsyncPipe,
NgIf,
EPersonFormComponent, EPersonFormComponent,
ReactiveFormsModule, ReactiveFormsModule,
ThemedLoadingComponent, ThemedLoadingComponent,
PaginationComponent, PaginationComponent,
NgClass, NgClass,
NgForOf,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -2,15 +2,13 @@
<div class="group-form row"> <div class="group-form row">
<div class="col-12"> <div class="col-12">
<div *ngIf="activeEPerson$ | async; then editHeader; else createHeader"></div> @if (activeEPerson$ | async) {
<ng-template #createHeader>
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
</ng-template>
<ng-template #editHeader>
<h1 class="border-bottom pb-2">{{messagePrefix + '.edit' | translate}}</h1> <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" <ds-form [formId]="formId"
[formModel]="formModel" [formModel]="formModel"
@@ -24,39 +22,51 @@
<i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}} <i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}
</button> </button>
</div> </div>
<div *ngIf="displayResetPassword" between class="btn-group"> @if (displayResetPassword) {
<button class="btn btn-primary" [disabled]="(canReset$ | async) !== true" type="button" (click)="resetPassword()"> <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}} <i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
</button> </button>
</div> </div>
<div *ngIf="canImpersonate$ | async" between class="btn-group"> }
<button *ngIf="!isImpersonated" class="btn btn-primary" type="button" (click)="impersonate()"> @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}} <i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
</button> </button>
<button *ngIf="isImpersonated" class="btn btn-primary" type="button" (click)="stopImpersonating()"> }
@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}} <i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
</button> </button>
}
</div> </div>
<button *ngIf="canDelete$ | async" after class="btn btn-danger delete-button" type="button" (click)="delete()"> }
@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}} <i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
</button> </button>
}
</ds-form> </ds-form>
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading> @if (!formGroup) {
<ds-loading [showMessage]="false"></ds-loading>
}
<div *ngIf="activeEPerson$ | async"> @if (activeEPerson$ | async) {
<div>
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2> <h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
@if (groups$ | async | dsHasNoValue) {
<ds-loading [showMessage]="false" *ngIf="groups$ | async | dsHasNoValue"></ds-loading> <ds-loading [showMessage]="false"></ds-loading>
}
@if ((groups$ | async)?.payload?.totalElements > 0) {
<ds-pagination <ds-pagination
*ngIf="(groups$ | async)?.payload?.totalElements > 0"
[paginationOptions]="config" [paginationOptions]="config"
[collectionSize]="(groups$ | async)?.payload?.totalElements" [collectionSize]="(groups$ | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true" [hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)"> (pageChange)="onPageChange($event)">
<div class="table-responsive"> <div class="table-responsive">
<table id="groups" class="table table-striped table-hover table-bordered"> <table id="groups" class="table table-striped table-hover table-bordered">
<thead> <thead>
@@ -67,7 +77,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let group of (groups$ | async)?.payload?.page"> @for (group of (groups$ | async)?.payload?.page; track group) {
<tr>
<td class="align-middle">{{group.id}}</td> <td class="align-middle">{{group.id}}</td>
<td class="align-middle"> <td class="align-middle">
<a (click)="groupsDataService.startEditingNewGroup(group)" <a (click)="groupsDataService.startEditingNewGroup(group)"
@@ -79,20 +90,23 @@
{{ dsoNameService.getName((group.object | async)?.payload) }} {{ dsoNameService.getName((group.object | async)?.payload) }}
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
}
<div *ngIf="(groups$ | async)?.payload?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert"> @if ((groups$ | async)?.payload?.totalElements === 0) {
<div class="alert alert-info w-100 mb-2" role="alert">
<div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div> <div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div>
<div> <div>
<button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]" <button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]"
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button> class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
</div> </div>
</div> </div>
</div> }
</div>
}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -43,6 +43,7 @@ import { GroupDataService } from '../../../core/eperson/group-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../../core/eperson/models/eperson.model';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { PageInfo } from '../../../core/shared/page-info.model'; 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 { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { FormComponent } from '../../../shared/form/form.component'; import { FormComponent } from '../../../shared/form/form.component';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
@@ -221,7 +222,7 @@ describe('EPersonFormComponent', () => {
route = new ActivatedRouteStub(); route = new ActivatedRouteStub();
router = new RouterStub(); router = new RouterStub();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BtnDisabledDirective, BrowserModule,
RouterModule.forRoot([]), RouterModule.forRoot([]),
TranslateModule.forRoot(), TranslateModule.forRoot(),
EPersonFormComponent, EPersonFormComponent,
@@ -516,7 +517,8 @@ describe('EPersonFormComponent', () => {
// ePersonDataServiceStub.activeEPerson = eperson; // ePersonDataServiceStub.activeEPerson = eperson;
spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204)); spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204));
const deleteButton = fixture.debugElement.query(By.css('.delete-button')); 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); deleteButton.triggerEventHandler('click', null);
fixture.detectChanges(); fixture.detectChanges();
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson); expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);

View File

@@ -1,8 +1,6 @@
import { import {
AsyncPipe, AsyncPipe,
NgClass, NgClass,
NgFor,
NgIf,
} from '@angular/common'; } from '@angular/common';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
@@ -65,6 +63,7 @@ import {
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { Registration } from '../../../core/shared/registration.model'; import { Registration } from '../../../core/shared/registration.model';
import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component'; 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 { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; 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', templateUrl: './eperson-form.component.html',
imports: [ imports: [
FormComponent, FormComponent,
NgIf,
NgFor,
AsyncPipe, AsyncPipe,
TranslateModule, TranslateModule,
NgClass, NgClass,
@@ -92,6 +89,7 @@ import { ValidateEmailNotTaken } from './validators/email-taken.validator';
PaginationComponent, PaginationComponent,
RouterLink, RouterLink,
HasNoValuePipe, HasNoValuePipe,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })
@@ -343,7 +341,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, { this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, {
currentPage: 1, currentPage: 1,
elementsPerPage: this.config.pageSize, elementsPerPage: this.config.pageSize,
}); }, undefined, undefined, followLink('object'));
} }
this.formGroup.patchValue({ this.formGroup.patchValue({
firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '', firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '',

View File

@@ -2,13 +2,7 @@
<div class="group-form row"> <div class="group-form row">
<div class="col-12"> <div class="col-12">
<div *ngIf="activeGroup$ | async; then editHeader; else createHeader"></div> @if (activeGroup$ | async) {
<ng-template #createHeader>
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
</ng-template>
<ng-template #editHeader>
<h1 class="border-bottom pb-2"> <h1 class="border-bottom pb-2">
<span <span
*dsContextHelp="{ *dsContextHelp="{
@@ -21,17 +15,25 @@
{{messagePrefix + '.head.edit' | translate}} {{messagePrefix + '.head.edit' | translate}}
</span> </span>
</h1> </h1>
</ng-template> } @else {
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
}
<ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited">
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertType.Warning"
@if ((activeGroup$ | async); as groupBeingEdited) {
@if (groupBeingEdited?.permanent) {
<ds-alert [type]="AlertType.Warning"
[content]="messagePrefix + '.alert.permanent'"></ds-alert> [content]="messagePrefix + '.alert.permanent'"></ds-alert>
<ng-container *ngIf="(activeGroupLinkedDSO$ | async) as activeGroupLinkedDSO"> }
<ds-alert *ngIf="(canEdit$ | async) !== true" [type]="AlertType.Warning" @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) })"> [content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName(activeGroupLinkedDSO), comcol: activeGroupLinkedDSO.type, comcolEditRolesRoute: (linkedEditRolesRoute$ | async) })">
</ds-alert> </ds-alert>
</ng-container> }
</ng-container> }
}
<ds-form [formId]="formId" <ds-form [formId]="formId"
[formModel]="formModel" [formModel]="formModel"
@@ -43,21 +45,27 @@
<button (click)="onCancel()" type="button" <button (click)="onCancel()" type="button"
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button> class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
</div> </div>
<div after *ngIf="(canEdit$ | async) && !(activeGroup$ | async)?.permanent" class="btn-group"> @if ((canEdit$ | async) && !(activeGroup$ | async)?.permanent) {
<div after class="btn-group">
<button (click)="delete()" class="btn btn-danger delete-button" type="button"> <button (click)="delete()" class="btn btn-danger delete-button" type="button">
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}} <i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
</button> </button>
</div> </div>
}
</ds-form> </ds-form>
<ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited"> @if ((activeGroup$ | async); as groupBeingEdited) {
<div class="mb-5"> <div class="mb-5">
<ds-members-list *ngIf="groupBeingEdited !== undefined" @if (groupBeingEdited !== undefined) {
<ds-members-list
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list> [messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
}
</div> </div>
<ds-subgroups-list *ngIf="groupBeingEdited !== undefined" @if (groupBeingEdited !== undefined) {
<ds-subgroups-list
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list> [messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
</ng-container> }
}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,4 @@
import { import { AsyncPipe } from '@angular/common';
AsyncPipe,
NgIf,
} from '@angular/common';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
@@ -92,7 +89,6 @@ import { ValidateGroupExists } from './validators/group-exists.validator';
imports: [ imports: [
FormComponent, FormComponent,
AlertComponent, AlertComponent,
NgIf,
AsyncPipe, AsyncPipe,
TranslateModule, TranslateModule,
ContextHelpDirective, ContextHelpDirective,

View File

@@ -3,12 +3,12 @@
<h3>{{messagePrefix + '.headMembers' | translate}}</h3> <h3>{{messagePrefix + '.headMembers' | translate}}</h3>
<ds-pagination *ngIf="(ePeopleMembersOfGroup | async)?.totalElements > 0" @if ((ePeopleMembersOfGroup | async)?.totalElements > 0) {
<ds-pagination
[paginationOptions]="config" [paginationOptions]="config"
[collectionSize]="(ePeopleMembersOfGroup | async)?.totalElements" [collectionSize]="(ePeopleMembersOfGroup | async)?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">
<div class="table-responsive"> <div class="table-responsive">
<table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered"> <table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered">
<thead> <thead>
@@ -20,7 +20,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let epersonDTO of (ePeopleMembersOfGroup | async)?.page"> @for (epersonDTO of (ePeopleMembersOfGroup | async)?.page; track epersonDTO) {
<tr>
<td class="align-middle">{{epersonDTO.eperson.id}}</td> <td class="align-middle">{{epersonDTO.eperson.id}}</td>
<td class="align-middle"> <td class="align-middle">
<a [routerLink]="getEPersonEditRoute(epersonDTO.eperson.id)"> <a [routerLink]="getEPersonEditRoute(epersonDTO.eperson.id)">
@@ -33,33 +34,39 @@
</td> </td>
<td class="align-middle"> <td class="align-middle">
<div class="btn-group edit-field"> <div class="btn-group edit-field">
@if (epersonDTO.ableToDelete) {
<button (click)="deleteMemberFromGroup(epersonDTO.eperson)" <button (click)="deleteMemberFromGroup(epersonDTO.eperson)"
*ngIf="epersonDTO.ableToDelete" [dsBtnDisabled]="actionConfig.remove.disabled"
[disabled]="actionConfig.remove.disabled"
[ngClass]="['btn btn-sm', actionConfig.remove.css]" [ngClass]="['btn btn-sm', actionConfig.remove.css]"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}"> title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
<i [ngClass]="actionConfig.remove.icon"></i> <i [ngClass]="actionConfig.remove.icon"></i>
</button> </button>
<button *ngIf="!epersonDTO.ableToDelete" }
@if (!epersonDTO.ableToDelete) {
<button
(click)="addMemberToGroup(epersonDTO.eperson)" (click)="addMemberToGroup(epersonDTO.eperson)"
[disabled]="actionConfig.add.disabled" [dsBtnDisabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]" [ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}"> title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i> <i [ngClass]="actionConfig.add.icon"></i>
</button> </button>
}
</div> </div>
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
}
<div *ngIf="(ePeopleMembersOfGroup | async) === undefined || (ePeopleMembersOfGroup | async)?.totalElements === 0" class="alert alert-info w-100 mb-2" @if ((ePeopleMembersOfGroup | async) === undefined || (ePeopleMembersOfGroup | async)?.totalElements === 0) {
<div class="alert alert-info w-100 mb-2"
role="alert"> role="alert">
{{messagePrefix + '.no-members-yet' | translate}} {{messagePrefix + '.no-members-yet' | translate}}
</div> </div>
}
<h3 id="search" class="border-bottom pb-2"> <h3 id="search" class="border-bottom pb-2">
<span <span
@@ -75,8 +82,8 @@
</h3> </h3>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between"> <form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
<div class="flex-grow-1 mr-3"> <div class="flex-grow-1 me-3">
<div class="form-group input-group mr-3"> <div class="form-group input-group me-3">
<input type="text" name="query" id="query" formControlName="query" <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"> <span class="input-group-append">
@@ -91,12 +98,12 @@
</div> </div>
</form> </form>
<ds-pagination *ngIf="(ePeopleSearch | async)?.totalElements > 0" @if ((ePeopleSearch | async)?.totalElements > 0) {
<ds-pagination
[paginationOptions]="configSearch" [paginationOptions]="configSearch"
[collectionSize]="(ePeopleSearch | async)?.totalElements" [collectionSize]="(ePeopleSearch | async)?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">
<div class="table-responsive"> <div class="table-responsive">
<table id="epersonsSearch" class="table table-striped table-hover table-bordered"> <table id="epersonsSearch" class="table table-striped table-hover table-bordered">
<thead> <thead>
@@ -108,7 +115,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let eperson of (ePeopleSearch | async)?.page"> @for (eperson of (ePeopleSearch | async)?.page; track eperson) {
<tr>
<td class="align-middle">{{eperson.id}}</td> <td class="align-middle">{{eperson.id}}</td>
<td class="align-middle"> <td class="align-middle">
<a [routerLink]="getEPersonEditRoute(eperson.id)"> <a [routerLink]="getEPersonEditRoute(eperson.id)">
@@ -122,7 +130,7 @@
<td class="align-middle"> <td class="align-middle">
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<button (click)="addMemberToGroup(eperson)" <button (click)="addMemberToGroup(eperson)"
[disabled]="actionConfig.add.disabled" [dsBtnDisabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]" [ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}"> title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i> <i [ngClass]="actionConfig.add.icon"></i>
@@ -130,16 +138,19 @@
</div> </div>
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
}
<div *ngIf="(ePeopleSearch | async)?.totalElements === 0 && searchDone" @if ((ePeopleSearch | async)?.totalElements === 0 && searchDone) {
<div
class="alert alert-info w-100 mb-2" class="alert alert-info w-100 mb-2"
role="alert"> role="alert">
{{messagePrefix + '.no-items' | translate}} {{messagePrefix + '.no-items' | translate}}
</div> </div>
}
</ng-container> </ng-container>

View File

@@ -1,8 +1,6 @@
import { import {
AsyncPipe, AsyncPipe,
NgClass, NgClass,
NgForOf,
NgIf,
} from '@angular/common'; } from '@angular/common';
import { import {
Component, Component,
@@ -54,6 +52,7 @@ import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import { ContextHelpDirective } from '../../../../shared/context-help.directive'; import { ContextHelpDirective } from '../../../../shared/context-help.directive';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
@@ -108,11 +107,10 @@ export interface EPersonListActionConfig {
ContextHelpDirective, ContextHelpDirective,
ReactiveFormsModule, ReactiveFormsModule,
PaginationComponent, PaginationComponent,
NgIf,
AsyncPipe, AsyncPipe,
RouterLink, RouterLink,
NgClass, NgClass,
NgForOf, BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -3,12 +3,12 @@
<h4>{{messagePrefix + '.headSubgroups' | translate}}</h4> <h4>{{messagePrefix + '.headSubgroups' | translate}}</h4>
<ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0" @if ((subGroups$ | async)?.payload?.totalElements > 0) {
<ds-pagination
[paginationOptions]="config" [paginationOptions]="config"
[collectionSize]="(subGroups$ | async)?.payload?.totalElements" [collectionSize]="(subGroups$ | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">
<div class="table-responsive"> <div class="table-responsive">
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered"> <table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
<thead> <thead>
@@ -20,7 +20,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page"> @for (group of (subGroups$ | async)?.payload?.page; track group) {
<tr>
<td class="align-middle">{{group.id}}</td> <td class="align-middle">{{group.id}}</td>
<td class="align-middle"> <td class="align-middle">
<a (click)="groupDataService.startEditingNewGroup(group)" <a (click)="groupDataService.startEditingNewGroup(group)"
@@ -39,15 +40,19 @@
</div> </div>
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
}
<div *ngIf="(subGroups$ | async)?.payload?.totalElements === 0" class="alert alert-info w-100 mb-2" @if ((subGroups$ | async)?.payload?.totalElements === 0) {
<div class="alert alert-info w-100 mb-2"
role="alert"> role="alert">
{{messagePrefix + '.no-subgroups-yet' | translate}} {{messagePrefix + '.no-subgroups-yet' | translate}}
</div> </div>
}
<h4 id="search" class="border-bottom pb-2"> <h4 id="search" class="border-bottom pb-2">
<span *dsContextHelp="{ <span *dsContextHelp="{
@@ -62,8 +67,8 @@
</h4> </h4>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between"> <form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
<div class="flex-grow-1 mr-3"> <div class="flex-grow-1 me-3">
<div class="form-group input-group mr-3"> <div class="mb-3 input-group me-3">
<input type="text" name="query" id="query" formControlName="query" <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"> <span class="input-group-append">
@@ -75,18 +80,18 @@
</div> </div>
</div> </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}} {{messagePrefix + '.button.see-all' | translate}}
</button> </button>
</div> </div>
</form> </form>
<ds-pagination *ngIf="(searchResults$ | async)?.payload?.totalElements > 0" @if ((searchResults$ | async)?.payload?.totalElements > 0) {
<ds-pagination
[paginationOptions]="configSearch" [paginationOptions]="configSearch"
[collectionSize]="(searchResults$ | async)?.payload?.totalElements" [collectionSize]="(searchResults$ | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">
<div class="table-responsive"> <div class="table-responsive">
<table id="groupsSearch" class="table table-striped table-hover table-bordered"> <table id="groupsSearch" class="table table-striped table-hover table-bordered">
<thead> <thead>
@@ -98,7 +103,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let group of (searchResults$ | async)?.payload?.page"> @for (group of (searchResults$ | async)?.payload?.page; track group) {
<tr>
<td class="align-middle">{{group.id}}</td> <td class="align-middle">{{group.id}}</td>
<td class="align-middle"> <td class="align-middle">
<a (click)="groupDataService.startEditingNewGroup(group)" <a (click)="groupDataService.startEditingNewGroup(group)"
@@ -117,14 +123,18 @@
</div> </div>
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
}
<div *ngIf="(searchResults$ | async)?.payload?.totalElements === 0 && searchDone" class="alert alert-info w-100 mb-2" @if ((searchResults$ | async)?.payload?.totalElements === 0 && searchDone) {
<div class="alert alert-info w-100 mb-2"
role="alert"> role="alert">
{{messagePrefix + '.no-items' | translate}} {{messagePrefix + '.no-items' | translate}}
</div> </div>
}
</ng-container> </ng-container>

View File

@@ -1,8 +1,4 @@
import { import { AsyncPipe } from '@angular/common';
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
Input, Input,
@@ -65,12 +61,10 @@ enum SubKey {
imports: [ imports: [
RouterLink, RouterLink,
AsyncPipe, AsyncPipe,
NgForOf,
ContextHelpDirective, ContextHelpDirective,
TranslateModule, TranslateModule,
ReactiveFormsModule, ReactiveFormsModule,
PaginationComponent, PaginationComponent,
NgIf,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -4,18 +4,18 @@
<div class="d-flex justify-content-between border-bottom mb-3"> <div class="d-flex justify-content-between border-bottom mb-3">
<h1 id="header" class="pb-2">{{messagePrefix + 'head' | translate}}</h1> <h1 id="header" class="pb-2">{{messagePrefix + 'head' | translate}}</h1>
<div> <div>
<button class="mr-auto btn btn-success" <button class="me-auto btn btn-success"
[routerLink]="'create'"> [routerLink]="'create'">
<i class="fas fa-plus"></i> <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> </button>
</div> </div>
</div> </div>
<h2 id="search" class="border-bottom pb-2">{{messagePrefix + 'search.head' | translate}}</h2> <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"> <form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
<div class="flex-grow-1 mr-3"> <div class="flex-grow-1 me-3">
<div class="form-group input-group"> <div class="mb-3 input-group">
<input type="text" name="query" id="query" formControlName="query" <input type="text" name="query" id="query" formControlName="query"
class="form-control" [attr.aria-label]="messagePrefix + 'search.placeholder' | translate" class="form-control" [attr.aria-label]="messagePrefix + 'search.placeholder' | translate"
[placeholder]="(messagePrefix + 'search.placeholder' | translate)" > [placeholder]="(messagePrefix + 'search.placeholder' | translate)" >
@@ -33,14 +33,15 @@
</div> </div>
</form> </form>
<ds-loading *ngIf="loading$ | async"></ds-loading> @if (loading$ | async) {
<ds-loading></ds-loading>
}
@if ((pageInfoState$ | async)?.totalElements > 0 && (loading$ | async) !== true) {
<ds-pagination <ds-pagination
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (loading$ | async) !== true"
[paginationOptions]="config" [paginationOptions]="config"
[collectionSize]="(pageInfoState$ | async)?.totalElements" [collectionSize]="(pageInfoState$ | async)?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">
<div class="table-responsive"> <div class="table-responsive">
<table id="groups" class="table table-striped table-hover table-bordered"> <table id="groups" class="table table-striped table-hover table-bordered">
<thead> <thead>
@@ -53,46 +54,57 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let groupDto of (groupsDto$ | async)?.page"> @for (groupDto of (groupsDto$ | async)?.page; track groupDto) {
<tr>
<td>{{groupDto.group.id}}</td> <td>{{groupDto.group.id}}</td>
<td>{{ dsoNameService.getName(groupDto.group) }}</td> <td>{{ dsoNameService.getName(groupDto.group) }}</td>
<td>{{ dsoNameService.getName((groupDto.group.object | async)?.payload) }}</td> <td>{{ dsoNameService.getName((groupDto.group.object | async)?.payload) }}</td>
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td> <td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
<td> <td>
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<ng-container [ngSwitch]="groupDto.ableToEdit"> @switch (groupDto.ableToEdit) {
<button *ngSwitchCase="true" @case (true) {
<button
[routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)" [routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
class="btn btn-outline-primary btn-sm btn-edit" class="btn btn-outline-primary btn-sm btn-edit"
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: dsoNameService.getName(groupDto.group) } }}" title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: dsoNameService.getName(groupDto.group) } }}"
> >
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
<button *ngSwitchCase="false" }
[disabled]="true" @case (false) {
<button
[dsBtnDisabled]="true"
class="btn btn-outline-primary btn-sm btn-edit" class="btn btn-outline-primary btn-sm btn-edit"
placement="left" placement="left"
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate" [ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"
> >
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
</ng-container> }
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete" }
@if (!groupDto.group?.permanent && groupDto.ableToDelete) {
<button
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm btn-delete" (click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm btn-delete"
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: dsoNameService.getName(groupDto.group) } }}"> title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: dsoNameService.getName(groupDto.group) } }}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
}
</div> </div>
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
}
<div *ngIf="(pageInfoState$ | async)?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert"> @if ((pageInfoState$ | async)?.totalElements === 0) {
<div class="alert alert-info w-100 mb-2" role="alert">
{{messagePrefix + 'no-items' | translate}} {{messagePrefix + 'no-items' | translate}}
</div> </div>
}
</div> </div>
</div> </div>

View File

@@ -50,6 +50,7 @@ import { RouteService } from '../../core/services/route.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { NoContent } from '../../core/shared/NoContent.model'; import { NoContent } from '../../core/shared/NoContent.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { import {
DSONameServiceMock, DSONameServiceMock,
UNDEFINED_NAME, UNDEFINED_NAME,
@@ -208,6 +209,7 @@ describe('GroupsRegistryComponent', () => {
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot(), TranslateModule.forRoot(),
GroupsRegistryComponent, GroupsRegistryComponent,
BtnDisabledDirective,
], ],
providers: [GroupsRegistryComponent, providers: [GroupsRegistryComponent,
{ provide: DSONameService, useValue: new DSONameServiceMock() }, { 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')); const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
expect(editButtonsFound.length).toEqual(2); expect(editButtonsFound.length).toEqual(2);
editButtonsFound.forEach((editButtonFound) => { 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')); const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
expect(editButtonsFound.length).toEqual(2); expect(editButtonsFound.length).toEqual(2);
editButtonsFound.forEach((editButtonFound) => { 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')); const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
expect(editButtonsFound.length).toEqual(2); expect(editButtonsFound.length).toEqual(2);
editButtonsFound.forEach((editButtonFound) => { editButtonsFound.forEach((editButtonFound) => {
expect(editButtonFound.nativeElement.disabled).toBeTrue(); expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeTrue();
}); });
}); });
}); });

View File

@@ -1,10 +1,4 @@
import { import { AsyncPipe } from '@angular/common';
AsyncPipe,
NgForOf,
NgIf,
NgSwitch,
NgSwitchCase,
} from '@angular/common';
import { import {
Component, Component,
OnDestroy, OnDestroy,
@@ -62,6 +56,7 @@ import {
getRemoteDataPayload, getRemoteDataPayload,
} from '../../core/shared/operators'; } from '../../core/shared/operators';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
@@ -78,12 +73,9 @@ import { followLink } from '../../shared/utils/follow-link-config.model';
RouterLink, RouterLink,
ReactiveFormsModule, ReactiveFormsModule,
AsyncPipe, AsyncPipe,
NgIf,
PaginationComponent, PaginationComponent,
NgSwitch,
NgSwitchCase,
NgbTooltipModule, NgbTooltipModule,
NgForOf, BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -1,14 +1,16 @@
<div class="container"> <div class="container">
<h1 id="header">{{'admin.batch-import.page.header' | translate}}</h1> <h1 id="header">{{'admin.batch-import.page.header' | translate}}</h1>
<p>{{'admin.batch-import.page.help' | translate}}</p> <p>{{'admin.batch-import.page.help' | translate}}</p>
<p *ngIf="dso"> @if (dso) {
<p>
selected collection: <b>{{getDspaceObjectName()}}</b>&nbsp; selected collection: <b>{{getDspaceObjectName()}}</b>&nbsp;
<a href="javascript:void(0)" (click)="removeDspaceObject()">{{'admin.batch-import.page.remove' | translate}}</a> <a href="javascript:void(0)" (click)="removeDspaceObject()">{{'admin.batch-import.page.remove' | translate}}</a>
</p> </p>
}
<p> <p>
<button class="btn btn-primary" (click)="this.selectCollection();">{{'admin.metadata-import.page.button.select-collection' | translate}}</button> <button class="btn btn-primary" (click)="this.selectCollection();">{{'admin.metadata-import.page.button.select-collection' | translate}}</button>
</p> </p>
<div class="form-group"> <div class="mb-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly"> <input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
<label class="form-check-label" for="validateOnly"> <label class="form-check-label" for="validateOnly">
@@ -25,23 +27,26 @@
[uncheckedLabel]="'admin.metadata-import.page.toggle.url' | translate" [uncheckedLabel]="'admin.metadata-import.page.toggle.url' | translate"
[checked]="isUpload" [checked]="isUpload"
(change)="toggleUpload()" ></ui-switch> (change)="toggleUpload()" ></ui-switch>
<small class="form-text text-muted"> <small class="form-text text-muted d-block">
{{'admin.batch-import.page.toggle.help' | translate}} {{'admin.batch-import.page.toggle.help' | translate}}
</small> </small>
@if (isUpload) {
<ds-file-dropzone-no-uploader <ds-file-dropzone-no-uploader
*ngIf="isUpload"
data-test="file-dropzone" data-test="file-dropzone"
(onFileAdded)="setFile($event)" (onFileAdded)="setFile($event)"
[dropMessageLabel]="'admin.batch-import.page.dropMsg'" [dropMessageLabel]="'admin.batch-import.page.dropMsg'"
[dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'"> [dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'">
</ds-file-dropzone-no-uploader> </ds-file-dropzone-no-uploader>
}
<div class="form-group mt-2" *ngIf="!isUpload"> @if (!isUpload) {
<div class="mb-3 mt-2">
<input class="form-control" type="text" placeholder="{{'admin.metadata-import.page.urlMsg' | translate}}" <input class="form-control" type="text" placeholder="{{'admin.metadata-import.page.urlMsg' | translate}}"
data-test="file-url-input" [(ngModel)]="fileURL"> data-test="file-url-input" [(ngModel)]="fileURL">
</div> </div>
}
<div class="space-children-mr"> <div class="space-children-mr">
<button class="btn btn-secondary" id="backButton" <button class="btn btn-secondary" id="backButton"

View File

@@ -1,7 +1,4 @@
import { import { Location } from '@angular/common';
Location,
NgIf,
} from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@@ -36,7 +33,6 @@ import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzo
selector: 'ds-batch-import-page', selector: 'ds-batch-import-page',
templateUrl: './batch-import-page.component.html', templateUrl: './batch-import-page.component.html',
imports: [ imports: [
NgIf,
TranslateModule, TranslateModule,
FormsModule, FormsModule,
UiSwitchModule, UiSwitchModule,

View File

@@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<h1 id="header">{{'admin.metadata-import.page.header' | translate}}</h1> <h1 id="header">{{'admin.metadata-import.page.header' | translate}}</h1>
<p>{{'admin.metadata-import.page.help' | translate}}</p> <p>{{'admin.metadata-import.page.help' | translate}}</p>
<div class="form-group"> <div class="mb-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly"> <input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
<label class="form-check-label" for="validateOnly"> <label class="form-check-label" for="validateOnly">

View File

@@ -4,7 +4,8 @@
<h1 class="flex-grow-1">{{ isNewService ? ('ldn-create-service.title' | translate) : ('ldn-edit-registered-service.title' | translate) }}</h1> <h1 class="flex-grow-1">{{ isNewService ? ('ldn-create-service.title' | translate) : ('ldn-edit-registered-service.title' | translate) }}</h1>
</div> </div>
<!-- In the toggle section --> <!-- In the toggle section -->
<div class="toggle-switch-container" *ngIf="!isNewService"> @if (!isNewService) {
<div class="toggle-switch-container">
<label class="status-label font-weight-bold" for="enabled">{{ 'ldn-service-status' | translate }}</label> <label class="status-label font-weight-bold" for="enabled">{{ 'ldn-service-status' | translate }}</label>
<div> <div>
<input formControlName="enabled" hidden id="enabled" name="enabled" type="checkbox"> <input formControlName="enabled" hidden id="enabled" name="enabled" type="checkbox">
@@ -13,6 +14,7 @@
</div> </div>
</div> </div>
</div> </div>
}
<!-- In the Name section --> <!-- In the Name section -->
<div class="mb-5"> <div class="mb-5">
<label for="name" class="font-weight-bold">{{ 'ldn-new-service.form.label.name' | translate }}</label> <label for="name" class="font-weight-bold">{{ 'ldn-new-service.form.label.name' | translate }}</label>
@@ -22,9 +24,11 @@
id="name" id="name"
name="name" name="name"
type="text"> type="text">
<div *ngIf="formModel.get('name').invalid && formModel.get('name').touched" class="error-text"> @if (formModel.get('name').invalid && formModel.get('name').touched) {
<div class="error-text">
{{ 'ldn-new-service.form.error.name' | translate }} {{ 'ldn-new-service.form.error.name' | translate }}
</div> </div>
}
</div> </div>
<!-- In the description section --> <!-- In the description section -->
@@ -37,7 +41,7 @@
<div class="mb-5 mt-5"> <div class="mb-5 mt-5">
<!-- In the url section --> <!-- In the url section -->
<div class="d-flex align-items-center"> <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> <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" <input [class.invalid-field]="formModel.get('url').invalid && formModel.get('url').touched"
[placeholder]="'ldn-new-service.form.placeholder.url' | translate" class="form-control" [placeholder]="'ldn-new-service.form.placeholder.url' | translate" class="form-control"
@@ -45,9 +49,11 @@
id="url" id="url"
name="url" name="url"
type="text"> type="text">
<div *ngIf="formModel.get('url').invalid && formModel.get('url').touched" class="error-text"> @if (formModel.get('url').invalid && formModel.get('url').touched) {
<div class="error-text">
{{ 'ldn-new-service.form.error.url' | translate }} {{ 'ldn-new-service.form.error.url' | translate }}
</div> </div>
}
</div> </div>
<div class="d-flex flex-column w-50"> <div class="d-flex flex-column w-50">
@@ -61,9 +67,11 @@
step=".01" step=".01"
class="form-control" class="form-control"
type="number"> type="number">
<div *ngIf="formModel.get('score').invalid && formModel.get('score').touched" class="error-text"> @if (formModel.get('score').invalid && formModel.get('score').touched) {
<div class="error-text">
{{ 'ldn-new-service.form.error.score' | translate }} {{ 'ldn-new-service.form.error.score' | translate }}
</div> </div>
}
</div> </div>
</div> </div>
</div> </div>
@@ -73,7 +81,7 @@
<label for="lowerIp" class="font-weight-bold">{{ 'ldn-new-service.form.label.ip-range' | translate }}</label> <label for="lowerIp" class="font-weight-bold">{{ 'ldn-new-service.form.label.ip-range' | translate }}</label>
<div class="d-flex"> <div class="d-flex">
<input [class.invalid-field]="formModel.get('lowerIp').invalid && formModel.get('lowerIp').touched" <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" [placeholder]="'ldn-new-service.form.placeholder.lowerIp' | translate" class="form-control me-2"
formControlName="lowerIp" formControlName="lowerIp"
id="lowerIp" id="lowerIp"
name="lowerIp" name="lowerIp"
@@ -85,9 +93,11 @@
name="upperIp" name="upperIp"
type="text"> type="text">
</div> </div>
<div *ngIf="(formModel.get('lowerIp').invalid && formModel.get('lowerIp').touched) || (formModel.get('upperIp').invalid && formModel.get('upperIp').touched)" class="error-text"> @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 }} {{ 'ldn-new-service.form.error.ipRange' | translate }}
</div> </div>
}
<div class="text-muted"> <div class="text-muted">
{{ 'ldn-new-service.form.hint.ipRange' | translate }} {{ 'ldn-new-service.form.hint.ipRange' | translate }}
</div> </div>
@@ -102,14 +112,20 @@
id="ldnUrl" id="ldnUrl"
name="ldnUrl" name="ldnUrl"
type="text"> type="text">
<div *ngIf="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched" > @if (formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched) {
<div *ngIf="formModel.get('ldnUrl').errors['required']" class="error-text"> <div >
@if (formModel.get('ldnUrl').errors['required']) {
<div class="error-text">
{{ 'ldn-new-service.form.error.ldnurl' | translate }} {{ 'ldn-new-service.form.error.ldnurl' | translate }}
</div> </div>
<div *ngIf="formModel.get('ldnUrl').errors['ldnUrlAlreadyAssociated']" class="error-text"> }
@if (formModel.get('ldnUrl').errors['ldnUrlAlreadyAssociated']) {
<div class="error-text">
{{ 'ldn-new-service.form.error.ldnurl.ldnUrlAlreadyAssociated' | translate }} {{ 'ldn-new-service.form.error.ldnurl.ldnUrlAlreadyAssociated' | translate }}
</div> </div>
}
</div> </div>
}
</div> </div>
<!-- In the usesActorEmailId section --> <!-- In the usesActorEmailId section -->
@@ -130,31 +146,32 @@
<!-- In the Inbound Patterns Labels section --> <!-- In the Inbound Patterns Labels section -->
<div class="row mb-1 mt-5" *ngIf="areControlsInitialized"> @if (areControlsInitialized) {
<div class="row mb-1 mt-5">
<div class="col"> <div class="col">
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label> <label class="font-weight-bold">{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
</div> </div>
<ng-container *ngIf="formModel.get('notifyServiceInboundPatterns')['controls'][0]?.value?.pattern"> @if (formModel.get('notifyServiceInboundPatterns')['controls'][0]?.value?.pattern) {
<div class="col"> <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.ItemFilter' | translate }}</label>
</div> </div>
<div class="col-sm-1"> <div class="col-sm-1">
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.automatic' | translate }}</label> <label class="font-weight-bold">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
</div> </div>
</ng-container> }
<div class="col-sm-2"> <div class="col-sm-2">
</div> </div>
</div> </div>
}
<!-- In the Inbound Patterns section --> <!-- In the Inbound Patterns section -->
<div *ngIf="areControlsInitialized"> @if (areControlsInitialized) {
<div *ngFor="let patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; let i = index" <div>
@for (patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; track patternGroup; let i = $index) {
<div
[class.marked-for-deletion]="markedForDeletionInboundPattern.includes(i)" [class.marked-for-deletion]="markedForDeletionInboundPattern.includes(i)"
formGroupName="notifyServiceInboundPatterns"> formGroupName="notifyServiceInboundPatterns">
<ng-container [formGroupName]="i"> <ng-container [formGroupName]="i">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<div class="col"> <div class="col">
<div #inboundPatternDropdown="ngbDropdown" class="w-80" display="dynamic" <div #inboundPatternDropdown="ngbDropdown" class="w-80" display="dynamic"
@@ -178,23 +195,22 @@
class="dropdown-menu dropdown-menu-top w-100 " class="dropdown-menu dropdown-menu-top w-100 "
ngbDropdownMenu> ngbDropdownMenu>
<div class="scrollable-menu" role="listbox"> <div class="scrollable-menu" role="listbox">
@for (pattern of inboundPatterns; track pattern; let internalIndex = $index) {
<button (click)="selectInboundPattern(pattern, i); $event.stopPropagation()" <button (click)="selectInboundPattern(pattern, i); $event.stopPropagation()"
*ngFor="let pattern of inboundPatterns; let internalIndex = index"
[title]="'ldn-service.form.pattern.' + pattern + '.description' | translate" [title]="'ldn-service.form.pattern.' + pattern + '.description' | translate"
class="dropdown-item collection-item text-truncate w-100" class="dropdown-item collection-item text-truncate w-100"
ngbDropdownItem ngbDropdownItem
type="button"> type="button">
<div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div> <div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div>
</button> </button>
}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<ng-container @if (formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern) {
*ngIf="formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern">
<div #inboundItemfilterDropdown="ngbDropdown" class="w-100" id="constraint{{i}}" ngbDropdown <div #inboundItemfilterDropdown="ngbDropdown" class="w-100" id="constraint{{i}}" ngbDropdown
placement="top-start"> placement="top-start">
<div class="position-relative right-addon" aria-expanded="false" aria-controls="inboundItemfilterDropdown" role="combobox"> <div class="position-relative right-addon" aria-expanded="false" aria-controls="inboundItemfilterDropdown" role="combobox">
@@ -227,20 +243,20 @@
class="dropdown-item collection-item text-truncate w-100" ngbDropdownItem type="button"> class="dropdown-item collection-item text-truncate w-100" ngbDropdownItem type="button">
<span> {{'ldn-service.control-constaint-select-none' | translate}} </span> <span> {{'ldn-service.control-constaint-select-none' | translate}} </span>
</button> </button>
@for (constraint of (itemFiltersRD$ | async)?.payload?.page; track constraint; let internalIndex = $index) {
<button (click)="selectInboundItemFilter(constraint.id, i); $event.stopPropagation()" <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" class="dropdown-item collection-item text-truncate w-100"
ngbDropdownItem ngbDropdownItem
type="button"> type="button">
<div>{{ constraint.id + '.label' | translate }}</div> <div>{{ constraint.id + '.label' | translate }}</div>
</button> </button>
}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</ng-container> }
</div> </div>
<div <div
[style.visibility]="formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern ? 'visible' : 'hidden'" [style.visibility]="formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern ? 'visible' : 'hidden'"
class="col-sm-1"> class="col-sm-1">
@@ -252,8 +268,6 @@
<div class="slider"></div> <div class="slider"></div>
</div> </div>
</div> </div>
<div class="col-sm-2"> <div class="col-sm-2">
<div class="btn-group"> <div class="btn-group">
<button (click)="markForInboundPatternDeletion(i)" class="btn btn-outline-dark trash-button" <button (click)="markForInboundPatternDeletion(i)" class="btn btn-outline-dark trash-button"
@@ -261,21 +275,22 @@
type="button"> type="button">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
@if (markedForDeletionInboundPattern.includes(i)) {
<button (click)="unmarkForInboundPatternDeletion(i)" <button (click)="unmarkForInboundPatternDeletion(i)"
*ngIf="markedForDeletionInboundPattern.includes(i)"
[title]="'ldn-service-button-unmark-inbound-deletion' | translate" [title]="'ldn-service-button-unmark-inbound-deletion' | translate"
class="btn btn-warning " class="btn btn-warning "
type="button"> type="button">
<i class="fas fa-undo"></i> <i class="fas fa-undo"></i>
</button> </button>
}
</div> </div>
</div> </div>
</div> </div>
</ng-container> </ng-container>
</div> </div>
}
</div> </div>
}
<span (click)="addInboundPattern()" <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>
@@ -296,8 +311,12 @@
</div> </div>
<ng-template #confirmModal> <ng-template #confirmModal>
<div class="modal-header"> <div class="modal-header">
<h4 *ngIf="!isNewService">{{'service.overview.edit.modal' | translate }}</h4> @if (!isNewService) {
<h4 *ngIf="isNewService">{{'service.overview.create.modal' | translate }}</h4> <h4>{{'service.overview.edit.modal' | translate }}</h4>
}
@if (isNewService) {
<h4>{{'service.overview.create.modal' | translate }}</h4>
}
<button (click)="closeModal()" aria-label="Close" <button (click)="closeModal()" aria-label="Close"
class="close" type="button"> class="close" type="button">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>
@@ -305,30 +324,40 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div *ngIf="!isNewService"> @if (!isNewService) {
<div>
{{ 'service.overview.edit.body' | translate }} {{ 'service.overview.edit.body' | translate }}
</div> </div>
<span *ngIf="isNewService"> }
@if (isNewService) {
<span>
{{ 'service.overview.create.body' | translate }} {{ 'service.overview.create.body' | translate }}
</span> </span>
}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<div *ngIf="!isNewService"> @if (!isNewService) {
<button (click)="closeModal()" class="btn btn-outline-secondary mr-2" <div>
<button (click)="closeModal()" class="btn btn-outline-secondary me-2"
id="delete-confirm-edit">{{ 'service.detail.return' | translate }} id="delete-confirm-edit">{{ 'service.detail.return' | translate }}
</button> </button>
<button *ngIf="!isNewService" (click)="patchService()" @if (!isNewService) {
<button (click)="patchService()"
class="btn btn-primary">{{ 'service.detail.update' | translate }} class="btn btn-primary">{{ 'service.detail.update' | translate }}
</button> </button>
}
</div> </div>
<div *ngIf="isNewService"> }
<button (click)="closeModal()" class="btn btn-outline-secondary mr-2 " @if (isNewService) {
<div>
<button (click)="closeModal()" class="btn btn-outline-secondary me-2 "
id="delete-confirm-new">{{ 'service.refuse.create' | translate }} id="delete-confirm-new">{{ 'service.refuse.create' | translate }}
</button> </button>
<button (click)="createService()" <button (click)="createService()"
class="btn btn-primary">{{ 'service.confirm.create' | translate }} class="btn btn-primary">{{ 'service.confirm.create' | translate }}
</button> </button>
</div> </div>
}
</div> </div>
</ng-template> </ng-template>

View File

@@ -5,11 +5,7 @@ import {
transition, transition,
trigger, trigger,
} from '@angular/animations'; } from '@angular/animations';
import { import { AsyncPipe } from '@angular/common';
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
@@ -77,9 +73,7 @@ import { notifyPatterns } from '../ldn-services-patterns/ldn-service-coar-patter
imports: [ imports: [
ReactiveFormsModule, ReactiveFormsModule,
TranslateModule, TranslateModule,
NgIf,
NgbDropdownModule, NgbDropdownModule,
NgForOf,
AsyncPipe, AsyncPipe,
], ],
}) })

View File

@@ -4,9 +4,10 @@
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<button class="btn btn-success" routerLink="/admin/ldn/services/new"><i <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> </div>
<ds-pagination *ngIf="(ldnServicesRD$ | async)?.payload?.totalElements > 0" @if ((ldnServicesRD$ | async)?.payload?.totalElements > 0) {
<ds-pagination
[collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements" [collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true" [hidePagerWhenSinglePage]="true"
@@ -22,7 +23,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let ldnService of (ldnServicesRD$ | async)?.payload?.page"> @for (ldnService of (ldnServicesRD$ | async)?.payload?.page; track ldnService) {
<tr>
<td class="col-3">{{ ldnService.name }}</td> <td class="col-3">{{ ldnService.name }}</td>
<td> <td>
<ds-truncatable [id]="ldnService.id"> <ds-truncatable [id]="ldnService.id">
@@ -57,10 +59,12 @@
</div> </div>
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
}
</div> </div>
<ng-template #deleteModal> <ng-template #deleteModal>
@@ -85,7 +89,7 @@
<div class="mt-4 text-right"> <div class="mt-4 text-right">
<button (click)="closeModal()" <button (click)="closeModal()"
[attr.aria-label]="'ldn-service-overview-close-modal' | translate" [attr.aria-label]="'ldn-service-overview-close-modal' | translate"
class="btn btn-outline-secondary mr-2">{{ 'service.detail.delete.cancel' | translate }}</button> class="btn btn-outline-secondary me-2">{{ 'service.detail.delete.cancel' | translate }}</button>
<button (click)="deleteSelected(this.selectedServiceId.toString(), ldnServicesService)" <button (click)="deleteSelected(this.selectedServiceId.toString(), ldnServicesService)"
class="btn btn-danger" class="btn btn-danger"
[attr.aria-label]="'ldn-service-overview-select-delete' | translate" [attr.aria-label]="'ldn-service-overview-select-delete' | translate"

View File

@@ -1,8 +1,6 @@
import { import {
AsyncPipe, AsyncPipe,
NgClass, NgClass,
NgFor,
NgIf,
} from '@angular/common'; } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
@@ -54,8 +52,6 @@ import { LdnService } from '../ldn-services-model/ldn-services.model';
styleUrls: ['./ldn-services-directory.component.scss'], styleUrls: ['./ldn-services-directory.component.scss'],
changeDetection: ChangeDetectionStrategy.Default, changeDetection: ChangeDetectionStrategy.Default,
imports: [ imports: [
NgIf,
NgFor,
TranslateModule, TranslateModule,
AsyncPipe, AsyncPipe,
PaginationComponent, PaginationComponent,

View File

@@ -15,7 +15,9 @@
<a class="nav-link" [routerLink]="'outbound'" [queryParams]="{view: 'table'}">{{'admin.notify.dashboard.outbound-logs' | translate}}</a> <a class="nav-link" [routerLink]="'outbound'" [queryParams]="{view: 'table'}">{{'admin.notify.dashboard.outbound-logs' | translate}}</a>
</ul> </ul>
<div class="mt-2"> <div class="mt-2">
<ds-admin-notify-metrics *ngIf="(notifyMetricsRows$ | async)?.length" [boxesConfig]="notifyMetricsRows$ | async"></ds-admin-notify-metrics> @if ((notifyMetricsRows$ | async)?.length) {
<ds-admin-notify-metrics [boxesConfig]="notifyMetricsRows$ | async"></ds-admin-notify-metrics>
}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,4 @@
import { import { AsyncPipe } from '@angular/common';
AsyncPipe,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
Inject, Inject,
@@ -46,7 +43,6 @@ import {
imports: [ imports: [
AdminNotifyMetricsComponent, AdminNotifyMetricsComponent,
RouterLink, RouterLink,
NgIf,
TranslateModule, TranslateModule,
AsyncPipe, AsyncPipe,
], ],

View File

@@ -5,12 +5,14 @@
</button> </button>
</div> </div>
<div class="modal-body p-4"> <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="row mb-4">
<div class="font-weight-bold col">{{ key + '.notify-detail-modal' | translate}}</div> <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 class="col text-right">{{'notify-detail-modal.' + notifyMessage[key] | translate: {default: notifyMessage[key] ?? "n/a" } }}</div>
</div> </div>
</div> </div>
}
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<button class="btn-primary" (click)="toggleCoarMessage()"> <button class="btn-primary" (click)="toggleCoarMessage()">
@@ -18,5 +20,7 @@
</button> </button>
</div> </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> </div>

View File

@@ -1,7 +1,4 @@
import {
NgForOf,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
EventEmitter, EventEmitter,
@@ -26,9 +23,7 @@ import { AdminNotifyMessage } from '../models/admin-notify-message.model';
], ],
standalone: true, standalone: true,
imports: [ imports: [
NgForOf,
TranslateModule, TranslateModule,
NgIf,
], ],
}) })
/** /**

View File

@@ -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-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="col-md-9">
<div class="h4"> <div class="h4">
<button (click)="resetDefaultConfiguration()" *ngIf="(selectedSearchConfig$ | async) !== defaultConfiguration" class="badge badge-primary mr-1 mb-1"> @if ((selectedSearchConfig$ | async) !== defaultConfiguration) {
<button (click)="resetDefaultConfiguration()" class="badge bg-primary me-1 mb-1">
{{ 'admin-notify-logs.' + (selectedSearchConfig$ | async) | translate}} {{ 'admin-notify-logs.' + (selectedSearchConfig$ | async) | translate}}
<span> ×</span> <span> ×</span>
</button> </button>
}
</div> </div>
<ds-search-labels [inPlaceSearch]="true"></ds-search-labels> <ds-search-labels [inPlaceSearch]="true"></ds-search-labels>
</div> </div>

View File

@@ -1,7 +1,4 @@
import { import { AsyncPipe } from '@angular/common';
AsyncPipe,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
Inject, Inject,
@@ -39,7 +36,6 @@ import { ThemedSearchComponent } from '../../../../shared/search/themed-search.c
ThemedSearchComponent, ThemedSearchComponent,
AsyncPipe, AsyncPipe,
TranslateModule, TranslateModule,
NgIf,
], ],
}) })

View File

@@ -1,9 +1,13 @@
<div class="mb-5" *ngFor="let row of boxesConfig"> @for (row of boxesConfig; track row) {
<div class="mb-5">
<div class="mb-2">{{ row.title | translate }}</div> <div class="mb-2">{{ row.title | translate }}</div>
<div class="row justify-content-between"> <div class="row justify-content-between">
<div class="col-sm" *ngFor="let box of row.boxes"> @for (box of row.boxes; track box) {
<div class="col-sm">
<ds-notification-box (selectedBoxConfig)="navigateToSelectedSearchConfig($event)" [boxConfig]="box"></ds-notification-box> <ds-notification-box (selectedBoxConfig)="navigateToSelectedSearchConfig($event)" [boxConfig]="box"></ds-notification-box>
</div> </div>
}
</div> </div>
</div> </div>
}

View File

@@ -1,4 +1,4 @@
import { NgForOf } from '@angular/common';
import { import {
Component, Component,
Input, Input,
@@ -17,7 +17,6 @@ import { AdminNotifyMetricsRow } from './admin-notify-metrics.model';
imports: [ imports: [
NotificationBoxComponent, NotificationBoxComponent,
TranslateModule, TranslateModule,
NgForOf,
], ],
}) })
/** /**

View File

@@ -11,22 +11,35 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let message of (messagesSubject$ | async)"> @for (message of (messagesSubject$ | async); track message) {
<tr>
<td class="text-nowrap"> <td class="text-nowrap">
<div *ngIf="message.queueLastStartTime">{{ message.queueLastStartTime | date:"YYYY/MM/d hh:mm:ss" }}</div> @if (message.queueLastStartTime) {
<div *ngIf="!message.queueLastStartTime">n/a</div> <div>{{ message.queueLastStartTime | date:"YYYY/MM/d hh:mm:ss" }}</div>
}
@if (!message.queueLastStartTime) {
<div>n/a</div>
}
</td> </td>
<td> <td>
<ds-truncatable [id]="message.id"> <ds-truncatable [id]="message.id">
<ds-truncatable-part [id]="message.id" [minLines]="2"> <ds-truncatable-part [id]="message.id" [minLines]="2">
<a *ngIf="message.relatedItem" [routerLink]="'/items/' + (message.context || message.object)">{{ message.relatedItem }}</a> @if (message.relatedItem) {
<a [routerLink]="'/items/' + (message.context || message.object)">{{ message.relatedItem }}</a>
}
</ds-truncatable-part> </ds-truncatable-part>
</ds-truncatable> </ds-truncatable>
<div *ngIf="!message.relatedItem">n/a</div> @if (!message.relatedItem) {
<div>n/a</div>
}
</td> </td>
<td> <td>
<div *ngIf="message.ldnService">{{ message.ldnService }}</div> @if (message.ldnService) {
<div *ngIf="!message.ldnService">n/a</div> <div>{{ message.ldnService }}</div>
}
@if (!message.ldnService) {
<div>n/a</div>
}
</td> </td>
<td> <td>
<div>{{ message.activityStreamType }}</div> <div>{{ message.activityStreamType }}</div>
@@ -37,15 +50,18 @@
<td> <td>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<button class="btn mb-2 btn-dark" (click)="openDetailModal(message)">{{ 'notify-message-result.detail' | translate }}</button> <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)" @if (message.queueStatusLabel !== reprocessStatus && validStatusesForReprocess.includes(message.queueStatusLabel)) {
<button
(click)="reprocessMessage(message)" (click)="reprocessMessage(message)"
class="btn btn-warning" class="btn btn-warning"
> >
{{ 'notify-message-result.reprocess' | translate }} {{ 'notify-message-result.reprocess' | translate }}
</button> </button>
}
</div> </div>
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -1,8 +1,6 @@
import { import {
AsyncPipe, AsyncPipe,
DatePipe, DatePipe,
NgForOf,
NgIf,
} from '@angular/common'; } from '@angular/common';
import { import {
Component, Component,
@@ -42,8 +40,6 @@ import { AdminNotifyMessagesService } from '../services/admin-notify-messages.se
standalone: true, standalone: true,
imports: [ imports: [
TranslateModule, TranslateModule,
NgForOf,
NgIf,
DatePipe, DatePipe,
AsyncPipe, AsyncPipe,
TruncatableComponent, TruncatableComponent,

View File

@@ -8,8 +8,8 @@
<p id="create-new" class="mb-2"><a [routerLink]="'add'" class="btn btn-success">{{'admin.registries.bitstream-formats.create.new' | translate}}</a></p> <p id="create-new" class="mb-2"><a [routerLink]="'add'" class="btn btn-success">{{'admin.registries.bitstream-formats.create.new' | translate}}</a></p>
@if ((bitstreamFormats$ | async)?.payload?.totalElements > 0) {
<ds-pagination <ds-pagination
*ngIf="(bitstreamFormats$ | async)?.payload?.totalElements > 0"
[paginationOptions]="pageConfig" [paginationOptions]="pageConfig"
[collectionSize]="(bitstreamFormats$ | async)?.payload?.totalElements" [collectionSize]="(bitstreamFormats$ | async)?.payload?.totalElements"
[hideGear]="false" [hideGear]="false"
@@ -26,9 +26,10 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let bitstreamFormat of (bitstreamFormats$ | async)?.payload?.page"> @for (bitstreamFormat of (bitstreamFormats$ | async)?.payload?.page; track bitstreamFormat) {
<tr>
<td> <td>
<label class="mb-0"> <label class="form-label mb-0">
<input type="checkbox" <input type="checkbox"
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate" [attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
[checked]="(selectedBitstreamFormatIDs$ | async)?.includes(bitstreamFormat.id)" [checked]="(selectedBitstreamFormatIDs$ | async)?.includes(bitstreamFormat.id)"
@@ -39,20 +40,30 @@
</td> </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.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.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']">{{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> <td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
<div *ngIf="(bitstreamFormats$ | async)?.payload?.totalElements === 0" class="alert alert-info" role="alert"> }
@if ((bitstreamFormats$ | async)?.payload?.totalElements === 0) {
<div class="alert alert-info" role="alert">
{{'admin.registries.bitstream-formats.no-items' | translate}} {{'admin.registries.bitstream-formats.no-items' | translate}}
</div> </div>
}
<div> <div>
<button *ngIf="(bitstreamFormats$ | async)?.payload?.page?.length > 0" class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button> @if ((bitstreamFormats$ | async)?.payload?.page?.length > 0) {
<button *ngIf="(bitstreamFormats$ | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button> <button 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> </div>
</div> </div>

View File

@@ -1,8 +1,4 @@
import { import { AsyncPipe } from '@angular/common';
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
OnDestroy, OnDestroy,
@@ -41,12 +37,10 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
selector: 'ds-bitstream-formats', selector: 'ds-bitstream-formats',
templateUrl: './bitstream-formats.component.html', templateUrl: './bitstream-formats.component.html',
imports: [ imports: [
NgIf,
AsyncPipe, AsyncPipe,
RouterLink, RouterLink,
TranslateModule, TranslateModule,
PaginationComponent, PaginationComponent,
NgForOf,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -1,3 +1,5 @@
<ds-form *ngIf="formModel" @if (formModel) {
<ds-form
[formId]="'comcol-form-id'" [formId]="'comcol-form-id'"
[formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form> [formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form>
}

View File

@@ -1,4 +1,4 @@
import { NgIf } from '@angular/common';
import { import {
Component, Component,
EventEmitter, EventEmitter,
@@ -35,7 +35,6 @@ import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-p
templateUrl: './format-form.component.html', templateUrl: './format-form.component.html',
imports: [ imports: [
FormComponent, FormComponent,
NgIf,
], ],
standalone: true, standalone: true,
}) })
@@ -64,7 +63,7 @@ export class FormatFormComponent implements OnInit {
*/ */
arrayElementLayout: DynamicFormControlLayout = { arrayElementLayout: DynamicFormControlLayout = {
grid: { grid: {
group: 'form-row', group: 'row',
}, },
}; };

View File

@@ -8,13 +8,12 @@
<ds-metadata-schema-form (submitForm)="forceUpdateSchemas()"></ds-metadata-schema-form> <ds-metadata-schema-form (submitForm)="forceUpdateSchemas()"></ds-metadata-schema-form>
@if ((metadataSchemas | async)?.payload?.totalElements > 0) {
<ds-pagination <ds-pagination
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
[paginationOptions]="config" [paginationOptions]="config"
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements" [collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">
<div class="table-responsive"> <div class="table-responsive">
<table id="metadata-schemas" class="table table-striped table-hover"> <table id="metadata-schemas" class="table table-striped table-hover">
<thead> <thead>
@@ -26,10 +25,11 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page" @for (schema of (metadataSchemas | async)?.payload?.page; track schema) {
<tr
[ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}"> [ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}">
<td> <td>
<label class="mb-0"> <label class="form-label mb-0">
<input type="checkbox" <input type="checkbox"
[checked]="(selectedMetadataSchemaIDs$ | async)?.includes(schema.id)" [checked]="(selectedMetadataSchemaIDs$ | async)?.includes(schema.id)"
(change)="selectMetadataSchema(schema, $event)" (change)="selectMetadataSchema(schema, $event)"
@@ -41,18 +41,23 @@
<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.namespace}}</a></td>
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.prefix}}</a></td> <td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.prefix}}</a></td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
}
<div *ngIf="(metadataSchemas | async)?.payload?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert"> @if ((metadataSchemas | async)?.payload?.totalElements === 0) {
<div class="alert alert-info w-100 mb-2" role="alert">
{{'admin.registries.metadata.schemas.no-items' | translate}} {{'admin.registries.metadata.schemas.no-items' | translate}}
</div> </div>
}
<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> @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>

View File

@@ -1,8 +1,6 @@
import { import {
AsyncPipe, AsyncPipe,
NgClass, NgClass,
NgForOf,
NgIf,
} from '@angular/common'; } from '@angular/common';
import { import {
Component, Component,
@@ -49,8 +47,6 @@ import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-sch
TranslateModule, TranslateModule,
AsyncPipe, AsyncPipe,
PaginationComponent, PaginationComponent,
NgIf,
NgForOf,
NgClass, NgClass,
RouterLink, RouterLink,
], ],

View File

@@ -1,12 +1,10 @@
<div *ngIf="activeMetadataSchema$ | async; then editheader; else createHeader"></div> @if (activeMetadataSchema$ | async) {
<ng-template #createHeader>
<h2>{{messagePrefix + '.create' | translate}}</h2>
</ng-template>
<ng-template #editheader>
<h2>{{messagePrefix + '.edit' | translate}}</h2> <h2>{{messagePrefix + '.edit' | translate}}</h2>
</ng-template> } @else {
<h2>{{messagePrefix + '.create' | translate}}</h2>
}
<ds-form [formId]="formId" <ds-form [formId]="formId"
[formModel]="formModel" [formModel]="formModel"

View File

@@ -1,7 +1,4 @@
import { import { AsyncPipe } from '@angular/common';
AsyncPipe,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
EventEmitter, EventEmitter,
@@ -39,7 +36,6 @@ import { FormComponent } from '../../../../shared/form/form.component';
selector: 'ds-metadata-schema-form', selector: 'ds-metadata-schema-form',
templateUrl: './metadata-schema-form.component.html', templateUrl: './metadata-schema-form.component.html',
imports: [ imports: [
NgIf,
AsyncPipe, AsyncPipe,
TranslateModule, TranslateModule,
FormComponent, FormComponent,

View File

@@ -1,12 +1,10 @@
<div *ngIf="registryService.getActiveMetadataField() | async; then editheader; else createHeader"></div> @if (registryService.getActiveMetadataField() | async) {
<ng-template #createHeader>
<h2>{{messagePrefix + '.create' | translate}}</h2>
</ng-template>
<ng-template #editheader>
<h2>{{messagePrefix + '.edit' | translate}}</h2> <h2>{{messagePrefix + '.edit' | translate}}</h2>
</ng-template> } @else {
<h2>{{messagePrefix + '.create' | translate}}</h2>
}
<ds-form [formId]="formId" <ds-form [formId]="formId"
[formModel]="formModel" [formModel]="formModel"

View File

@@ -1,7 +1,4 @@
import { import { AsyncPipe } from '@angular/common';
AsyncPipe,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
EventEmitter, EventEmitter,
@@ -35,7 +32,6 @@ import { FormComponent } from '../../../../shared/form/form.component';
selector: 'ds-metadata-field-form', selector: 'ds-metadata-field-form',
templateUrl: './metadata-field-form.component.html', templateUrl: './metadata-field-form.component.html',
imports: [ imports: [
NgIf,
FormComponent, FormComponent,
TranslateModule, TranslateModule,
AsyncPipe, AsyncPipe,

View File

@@ -13,8 +13,8 @@
<h2>{{'admin.registries.schema.fields.head' | translate}}</h2> <h2>{{'admin.registries.schema.fields.head' | translate}}</h2>
<ng-container *ngVar="(metadataFields$ | async)?.payload as fields"> <ng-container *ngVar="(metadataFields$ | async)?.payload as fields">
@if (fields?.totalElements > 0) {
<ds-pagination <ds-pagination
*ngIf="fields?.totalElements > 0"
[paginationOptions]="config" [paginationOptions]="config"
[collectionSize]="fields?.totalElements" [collectionSize]="fields?.totalElements"
[hideGear]="false" [hideGear]="false"
@@ -30,7 +30,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let field of fields?.page" @for (field of fields?.page; track field) {
<tr
[ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}"> [ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}">
<td *ngVar="(selectedMetadataFieldIDs$ | async)?.includes(field.id) as selected"> <td *ngVar="(selectedMetadataFieldIDs$ | async)?.includes(field.id) as selected">
<input type="checkbox" <input type="checkbox"
@@ -42,18 +43,24 @@
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}{{field.qualifier ? '.' + field.qualifier : ''}}</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> <td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
}
<div *ngIf="fields?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert"> @if (fields?.totalElements === 0) {
<div class="alert alert-info w-100 mb-2" role="alert">
{{'admin.registries.schema.fields.no-items' | translate}} {{'admin.registries.schema.fields.no-items' | translate}}
</div> </div>
}
<div> <div>
<button [routerLink]="['/admin/registries/metadata']" class="btn btn-primary">{{'admin.registries.schema.return' | translate}}</button> <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> </div>
</ng-container> </ng-container>

View File

@@ -1,8 +1,6 @@
import { import {
AsyncPipe, AsyncPipe,
NgClass, NgClass,
NgForOf,
NgIf,
} from '@angular/common'; } from '@angular/common';
import { import {
Component, Component,
@@ -59,8 +57,6 @@ import { MetadataFieldFormComponent } from './metadata-field-form/metadata-field
MetadataFieldFormComponent, MetadataFieldFormComponent,
TranslateModule, TranslateModule,
PaginationComponent, PaginationComponent,
NgIf,
NgForOf,
NgClass, NgClass,
RouterLink, RouterLink,
], ],

View File

@@ -36,22 +36,30 @@
<th rowspan="2">{{ "admin.reports.collections.collection" | translate }}</th> <th rowspan="2">{{ "admin.reports.collections.collection" | translate }}</th>
<th>{{ "admin.reports.collections.nb_items" | translate }}</th> <th>{{ "admin.reports.collections.nb_items" | translate }}</th>
<th>{{ "admin.reports.collections.match_all_selected_filters" | 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> @for (filter of results.summary.values | keyvalue; track filter) {
<th>{{ ("admin.reports.commons.filters." + getGroup(filter.key) + "." + filter.key) | translate }}</th>
}
</tr> </tr>
<tr class="header"> <tr class="header">
<th class="num">{{ results.summary.nbTotalItems }}</th> <th class="num">{{ results.summary.nbTotalItems }}</th>
<th class="num">{{ results.summary.allFiltersValue }}</th> <th class="num">{{ results.summary.allFiltersValue }}</th>
<th class="num" *ngFor="let filter of results.summary.values | keyvalue">{{ filter.value }}</th> @for (filter of results.summary.values | keyvalue; track filter) {
<th class="num">{{ filter.value }}</th>
}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let coll of results.collections"> @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.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><a href="/handle/{{ coll.handle }}" rel="noopener noreferrer" target="_blank">{{ coll.label }}</a></td>
<td class="num">{{ coll.nbTotalItems }}</td> <td class="num">{{ coll.nbTotalItems }}</td>
<td class="num">{{ coll.allFiltersValue }}</td> <td class="num">{{ coll.allFiltersValue }}</td>
<td class="num" *ngFor="let filter of results.summary.values | keyvalue">{{ coll.values[filter.key] || 0 }}</td> @for (filter of results.summary.values | keyvalue; track filter) {
<td class="num">{{ coll.values[filter.key] || 0 }}</td>
}
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</ng-template> </ng-template>

View File

@@ -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 { NO_ERRORS_SCHEMA } from '@angular/core';
import { import {
ComponentFixture, ComponentFixture,
@@ -39,22 +43,21 @@ describe('FiltersComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ schemas: [NO_ERRORS_SCHEMA],
NgbAccordionModule, imports: [NgbAccordionModule,
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
useClass: TranslateLoaderMock, useClass: TranslateLoaderMock,
}, },
}), }),
HttpClientTestingModule, FilteredCollectionsComponent],
FilteredCollectionsComponent,
],
providers: [ providers: [
FormBuilder, FormBuilder,
DspaceRestService, DspaceRestService,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
], ],
schemas: [NO_ERRORS_SCHEMA],
}); });
})); }));

View File

@@ -1,7 +1,4 @@
import { import { KeyValuePipe } from '@angular/common';
KeyValuePipe,
NgForOf,
} from '@angular/common';
import { import {
Component, Component,
OnInit, OnInit,
@@ -37,7 +34,6 @@ import { FilteredCollections } from './filtered-collections.model';
NgbAccordionModule, NgbAccordionModule,
FiltersComponent, FiltersComponent,
KeyValuePipe, KeyValuePipe,
NgForOf,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -1,8 +1,10 @@
import { Item } from 'src/app/core/shared/item.model'; import { Item } from 'src/app/core/shared/item.model';
import { Collection } from '../../../core/shared/collection.model';
export class FilteredItems { export class FilteredItems {
public items: Item[] = []; public items: FilteredItem[] = [];
public itemCount: number; public itemCount: number;
public clear() { public clear() {
@@ -21,3 +23,8 @@ export class FilteredItems {
} }
} }
export interface FilteredItem extends Omit<Item, 'owningCollection'> {
index: number;
owningCollection?: Collection;
}

View File

@@ -12,7 +12,9 @@
</ng-template> </ng-template>
<ng-template ngbPanelContent> <ng-template ngbPanelContent>
<select id="collSel" name="collSel" class="form-control" multiple="multiple" size="10" formControlName="collections"> <select id="collSel" name="collSel" class="form-control" multiple="multiple" size="10" formControlName="collections">
<option *ngFor="let item of collections" [value]="item.id" [disabled]="item.disabled">{{item.name$ | async}}</option> @for (item of collections; track item) {
<option [value]="item.id" [disabled]="item.disabled">{{item.name$ | async}}</option>
}
</select> </select>
<div class="row"> <div class="row">
<span class="col-3"></span> <span class="col-3"></span>
@@ -30,22 +32,29 @@
{{'admin.reports.items.predefinedQueries' | translate}} {{'admin.reports.items.predefinedQueries' | translate}}
</label> </label>
<select id="predefselect" formControlName="presetQuery" class="form-control" (change)="setPresetQuery()"> <select id="predefselect" formControlName="presetQuery" class="form-control" (change)="setPresetQuery()">
<option *ngFor="let item of presetQueries" [value]="item.id" [selected]="item.isDefault">{{item.label | translate}}</option> @for (item of presetQueries; track item.id) {
<option [value]="item.id" [selected]="item.isDefault">{{item.label | translate}}</option>
}
</select> </select>
</fieldset> </fieldset>
<div class="row">&nbsp;</div> <div class="row">&nbsp;</div>
<div id="queries"> <div id="queries">
<div class="metadata" *ngFor="let pred of queryPredicatesArray().controls; let i = index"> @for (pred of queryPredicatesArray().controls; track pred; let i = $index) {
<div class="metadata">
<div [formGroup]="pred" class="form-group"> <div [formGroup]="pred" class="form-group">
<div class="form-row"> <div class="form-row">
<div class="col-4"> <div class="col-4">
<select class="query-tool" formControlName="field" class="form-control"> <select class="query-tool" formControlName="field" class="form-control">
<option *ngFor="let item of metadataFieldsWithAny" [value]="item.id">{{item.name$ | async}}</option> @for (item of metadataFieldsWithAny; track item) {
<option [value]="item.id">{{item.name$ | async}}</option>
}
</select> </select>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<select class="query-tool" formControlName="operator" class="form-control"> <select class="query-tool" formControlName="operator" class="form-control">
<option *ngFor="let item of predicates" [value]="item.id">{{item.name$ | async | translate}}</option> @for (item of predicates; track item) {
<option [value]="item.id">{{item.name$ | async | translate}}</option>
}
</select> </select>
</div> </div>
<div class="col"> <div class="col">
@@ -54,11 +63,12 @@
<div class="col-auto"> <div class="col-auto">
<button class="btn btn-light" (click)="addQueryPredicate()">+</button> <button class="btn btn-light" (click)="addQueryPredicate()">+</button>
&nbsp; &nbsp;
<button class="btn btn-light" [disabled]="deleteQueryPredicateDisabled()" (click)="deleteQueryPredicate(i)"></button> <button class="btn btn-light" [dsBtnDisabled]="deleteQueryPredicateDisabled()" (click)="deleteQueryPredicate(i)"></button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
}
</div> </div>
<div class="row"> <div class="row">
<span class="col-3"></span> <span class="col-3"></span>
@@ -75,7 +85,9 @@
<label for="limit" class="col-sm-2 col-form-label">{{'admin.reports.items.limit' | translate}}:</label> <label for="limit" class="col-sm-2 col-form-label">{{'admin.reports.items.limit' | translate}}:</label>
<div class="col-6"> <div class="col-6">
<select id="limit" name="limit" formControlName="pageLimit" class="form-control col-6"> <select id="limit" name="limit" formControlName="pageLimit" class="form-control col-6">
<option *ngFor="let item of pageLimits" value="{{item.id}}" [selected]="item.isDefault">{{item.name$ | async}}</option> @for (item of pageLimits; track item) {
<option value="{{item.id}}" [selected]="item.isDefault">{{item.name$ | async}}</option>
}
</select> </select>
</div> </div>
</div> </div>
@@ -114,7 +126,9 @@
<ng-template ngbPanelContent> <ng-template ngbPanelContent>
<div id="show-fields"> <div id="show-fields">
<select class="query-tool" name="show_fields" multiple="multiple" size="8" class="form-control" formControlName="additionalFields"> <select class="query-tool" name="show_fields" multiple="multiple" size="8" class="form-control" formControlName="additionalFields">
<option *ngFor="let item of metadataFields" [value]="item.id">{{item.name$ | async}}</option> @for (item of metadataFields; track item) {
<option [value]="item.id">{{item.name$ | async}}</option>
}
</select> </select>
</div> </div>
<div class="row"> <div class="row">
@@ -136,30 +150,42 @@
<th>{{ "admin.reports.items.collection" | translate }}</th> <th>{{ "admin.reports.items.collection" | translate }}</th>
<th>{{ "admin.reports.items.handle" | translate }}</th> <th>{{ "admin.reports.items.handle" | translate }}</th>
<th>{{ "admin.reports.items.title" | translate }}</th> <th>{{ "admin.reports.items.title" | translate }}</th>
<th *ngFor="let field of queryForm.value['additionalFields']">{{ field }}</th> @for (field of queryForm.value['additionalFields']; track field) {
<th>{{ field }}</th>
}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let item of results$ | async"> @for (item of results$ | async; track item) {
<tr>
<td class="num">{{ item.index }}</td> <td class="num">{{ item.index }}</td>
<td>{{ item.uuid }}</td> <td>{{ item.uuid }}</td>
<td><a *ngIf="item.owningCollection" href="/handle/{{ item.owningCollection.handle }}" rel="noopener noreferrer" target="_blank">{{ item.owningCollection.name }}</a></td> <td>@if (item.owningCollection) {
<td><a *ngIf="item.handle" href="/handle/{{ item.handle }}" rel="noopener noreferrer" target="_blank">{{ item.handle }}</a></td> <a href="/handle/{{ item.owningCollection.handle }}" rel="noopener noreferrer" target="_blank">{{ item.owningCollection.name }}</a>
}</td>
<td>@if (item.handle) {
<a href="/handle/{{ item.handle }}" rel="noopener noreferrer" target="_blank">{{ item.handle }}</a>
}</td>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td class="num" *ngFor="let field of queryForm.value['additionalFields']"> @for (field of queryForm.value['additionalFields']; track field) {
<span *ngFor="let mdvalue of item.metadata[field]"> <td class="num">
@for (mdvalue of item.metadata[field]; track mdvalue) {
<span>
{{ mdvalue.value || "" }} {{ mdvalue.value || "" }}
</span> </span>
}
</td> </td>
}
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
<div> <div>
{{'admin.reports.commons.page' | translate}} {{ currentPage + 1 }} {{'admin.reports.commons.of' | translate}} {{ pageCount() }} {{'admin.reports.commons.page' | translate}} {{ currentPage + 1 }} {{'admin.reports.commons.of' | translate}} {{ pageCount() }}
</div> </div>
<div> <div>
<button id="prev" class="btn btn-light" (click)="prevPage()" [disabled]="!canNavigatePrevious()">{{'admin.reports.commons.previous-page' | translate}}</button> <button id="prev" class="btn btn-light" (click)="prevPage()" [dsBtnDisabled]="!canNavigatePrevious()">{{'admin.reports.commons.previous-page' | translate}}</button>
<button id="next" class="btn btn-light" (click)="nextPage()" [disabled]="!canNavigateNext()">{{'admin.reports.commons.next-page' | translate}}</button> <button id="next" class="btn btn-light" (click)="nextPage()" [dsBtnDisabled]="!canNavigateNext()">{{'admin.reports.commons.next-page' | translate}}</button>
<!-- <!--
<button id="export">{{'admin.reports.commons.export' | translate}}</button> <button id="export">{{'admin.reports.commons.export' | translate}}</button>
--> -->

View File

@@ -1,8 +1,4 @@
import { import { AsyncPipe } from '@angular/common';
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
OnInit, OnInit,
@@ -38,13 +34,16 @@ import { MetadataField } from 'src/app/core/metadata/metadata-field.model';
import { MetadataSchema } from 'src/app/core/metadata/metadata-schema.model'; import { MetadataSchema } from 'src/app/core/metadata/metadata-schema.model';
import { Collection } from 'src/app/core/shared/collection.model'; import { Collection } from 'src/app/core/shared/collection.model';
import { Community } from 'src/app/core/shared/community.model'; import { Community } from 'src/app/core/shared/community.model';
import { Item } from 'src/app/core/shared/item.model';
import { getFirstSucceededRemoteListPayload } from 'src/app/core/shared/operators'; import { getFirstSucceededRemoteListPayload } from 'src/app/core/shared/operators';
import { isEmpty } from 'src/app/shared/empty.util'; import { isEmpty } from 'src/app/shared/empty.util';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { FiltersComponent } from '../filters-section/filters-section.component'; import { FiltersComponent } from '../filters-section/filters-section.component';
import { FilteredItems } from './filtered-items-model'; import {
FilteredItem,
FilteredItems,
} from './filtered-items-model';
import { OptionVO } from './option-vo.model'; import { OptionVO } from './option-vo.model';
import { PresetQuery } from './preset-query.model'; import { PresetQuery } from './preset-query.model';
import { QueryPredicate } from './query-predicate.model'; import { QueryPredicate } from './query-predicate.model';
@@ -61,9 +60,8 @@ import { QueryPredicate } from './query-predicate.model';
NgbAccordionModule, NgbAccordionModule,
TranslateModule, TranslateModule,
AsyncPipe, AsyncPipe,
NgIf,
NgForOf,
FiltersComponent, FiltersComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })
@@ -79,7 +77,7 @@ export class FilteredItemsComponent implements OnInit {
queryForm: FormGroup; queryForm: FormGroup;
currentPage = 0; currentPage = 0;
results: FilteredItems = new FilteredItems(); results: FilteredItems = new FilteredItems();
results$: Observable<Item[]>; results$: Observable<FilteredItem[]>;
@ViewChild('acc') accordionComponent: NgbAccordion; @ViewChild('acc') accordionComponent: NgbAccordion;
constructor( constructor(

View File

@@ -9,6 +9,7 @@ export class OptionVO {
id: string; id: string;
name$: Observable<string>; name$: Observable<string>;
disabled = false; disabled = false;
isDefault?: boolean;
static collection(id: string, name: string, disabled: boolean = false): OptionVO { static collection(id: string, name: string, disabled: boolean = false): OptionVO {
const opt = new OptionVO(); const opt = new OptionVO();

View File

@@ -5,6 +5,7 @@ export class PresetQuery {
id: string; id: string;
label: string; label: string;
predicates: QueryPredicate[]; predicates: QueryPredicate[];
isDefault?: boolean;
static of(id: string, label: string, predicates: QueryPredicate[]) { static of(id: string, label: string, predicates: QueryPredicate[]) {
const query = new PresetQuery(); const query = new PresetQuery();

View File

@@ -9,11 +9,15 @@
<span class="col-12">&nbsp;</span> <span class="col-12">&nbsp;</span>
</ng-container> </ng-container>
</fieldset> </fieldset>
<fieldset class="row row-cols-2" *ngFor="let group of allFilters()"> @for (group of allFilters(); track group) {
<fieldset class="row row-cols-2">
<legend>{{group.key | translate}}</legend> <legend>{{group.key | translate}}</legend>
<ng-container [formGroup]="filtersForm"> <ng-container [formGroup]="filtersForm">
<div *ngFor="let filter of group.filters" class="col-6"> @for (filter of group.filters; track filter) {
<input type="checkbox" id="flt-{{filter.id}}" value="{{filter.id}}" class="form-check-input col-1 align-baseline" formControlName="{{filter.id}}"><label for="flt-{{filter.id}}" class="col-11 align-middle" title="{{filter.tooltipKey | translate}}">{{filter.key | translate}}</label> <div class="col-6">
<input type="checkbox" id="flt-{{filter.id}}" value="{{filter.id}}" class="form-check-input col-1" formControlName="{{filter.id}}"><label for="flt-{{filter.id}}" class="col-11 align-middle" title="{{filter.tooltipKey | translate}}">{{filter.key | translate}}</label>
</div> </div>
}
</ng-container> </ng-container>
</fieldset> </fieldset>
}

View File

@@ -0,0 +1,8 @@
.col-6 > label.col-11.align-middle {
padding-left: 15px;
padding-right: 15px;
}
fieldset.row-cols-2 > legend {
float: none !important;
}

View File

@@ -1,4 +1,4 @@
import { NgForOf } from '@angular/common';
import { import {
Component, Component,
Input, Input,
@@ -23,7 +23,6 @@ import { FilterGroup } from './filter-group.model';
templateUrl: './filters-section.component.html', templateUrl: './filters-section.component.html',
styleUrls: ['./filters-section.component.scss'], styleUrls: ['./filters-section.component.scss'],
imports: [ imports: [
NgForOf,
ReactiveFormsModule, ReactiveFormsModule,
TranslateModule, TranslateModule,
], ],

View File

@@ -11,8 +11,8 @@ import {
REGISTRIES_MODULE_PATH, REGISTRIES_MODULE_PATH,
REPORTS_MODULE_PATH, REPORTS_MODULE_PATH,
} from './admin-routing-paths'; } from './admin-routing-paths';
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; import { ThemedAdminSearchPageComponent } from './admin-search-page/themed-admin-search-page.component';
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; import { ThemedAdminWorkflowPageComponent } from './admin-workflow-page/themed-admin-workflow-page.component';
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
{ {
@@ -28,13 +28,13 @@ export const ROUTES: Route[] = [
{ {
path: 'search', path: 'search',
resolve: { breadcrumb: i18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
component: AdminSearchPageComponent, component: ThemedAdminSearchPageComponent,
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }, data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' },
}, },
{ {
path: 'workflow', path: 'workflow',
resolve: { breadcrumb: i18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
component: AdminWorkflowPageComponent, component: ThemedAdminWorkflowPageComponent,
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }, data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' },
}, },
{ {

View File

@@ -4,7 +4,7 @@ import { Context } from '../../core/shared/context.model';
import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component'; import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component';
@Component({ @Component({
selector: 'ds-admin-search-page', selector: 'ds-base-admin-search-page',
templateUrl: './admin-search-page.component.html', templateUrl: './admin-search-page.component.html',
styleUrls: ['./admin-search-page.component.scss'], styleUrls: ['./admin-search-page.component.scss'],
standalone: true, standalone: true,

View File

@@ -1,30 +1,52 @@
<div class="space-children-mr my-1"> <div class="space-children-mr my-1">
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary move-link" [routerLink]="[getMoveRoute()]" [title]="'admin.search.item.move' | translate"> <a [ngClass]="{'btn-sm': small}" class="btn btn-secondary move-link" [routerLink]="[getMoveRoute()]" [title]="'admin.search.item.move' | translate">
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span> <i class="fa fa-arrow-circle-right"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
}
</a> </a>
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-secondary private-link" [routerLink]="[getPrivateRoute()]" [title]="'admin.search.item.make-private' | translate"> @if (item && item.isDiscoverable) {
<i class="fa fa-eye-slash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span> <a [ngClass]="{'btn-sm': small}" class="btn btn-secondary private-link" [routerLink]="[getPrivateRoute()]" [title]="'admin.search.item.make-private' | translate">
<i class="fa fa-eye-slash"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span>
}
</a> </a>
}
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-secondary public-link" [routerLink]="[getPublicRoute()]" [title]="'admin.search.item.make-public' | translate"> @if (item && !item.isDiscoverable) {
<i class="fa fa-eye"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span> <a [ngClass]="{'btn-sm': small}" class="btn btn-secondary public-link" [routerLink]="[getPublicRoute()]" [title]="'admin.search.item.make-public' | translate">
<i class="fa fa-eye"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span>
}
</a> </a>
}
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary edit-link" [routerLink]="[getEditRoute()]" [title]="'admin.search.item.edit' | translate"> <a [ngClass]="{'btn-sm': small}" class="btn btn-secondary edit-link" [routerLink]="[getEditRoute()]" [title]="'admin.search.item.edit' | translate">
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span> <i class="fa fa-edit"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
}
</a> </a>
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-warning t withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate"> @if (item && !item.isWithdrawn) {
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span> <a [ngClass]="{'btn-sm': small}" class="btn btn-warning t withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate">
<i class="fa fa-ban"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
}
</a> </a>
}
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-warning reinstate-link" [routerLink]="[getReinstateRoute()]" [title]="'admin.search.item.reinstate' | translate"> @if (item && item.isWithdrawn) {
<i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span> <a [ngClass]="{'btn-sm': small}" class="btn btn-warning reinstate-link" [routerLink]="[getReinstateRoute()]" [title]="'admin.search.item.reinstate' | translate">
<i class="fa fa-undo"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
}
</a> </a>
}
<a [ngClass]="{'btn-sm': small}" class="btn btn-danger delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.search.item.delete' | translate"> <a [ngClass]="{'btn-sm': small}" class="btn btn-danger delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.search.item.delete' | translate">
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span> <i class="fa fa-trash"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span>
}
</a> </a>
</div> </div>

View File

@@ -1,7 +1,4 @@
import { import { NgClass } from '@angular/common';
NgClass,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
Input, Input,
@@ -26,7 +23,7 @@ import { getItemEditRoute } from '../../../item-page/item-page-routing-paths';
styleUrls: ['./item-admin-search-result-actions.component.scss'], styleUrls: ['./item-admin-search-result-actions.component.scss'],
templateUrl: './item-admin-search-result-actions.component.html', templateUrl: './item-admin-search-result-actions.component.html',
standalone: true, standalone: true,
imports: [NgClass, RouterLink, NgIf, TranslateModule], imports: [NgClass, RouterLink, TranslateModule],
}) })
/** /**
* The component for displaying the actions for a list element for an item search result on the admin search page * The component for displaying the actions for a list element for an item search result on the admin search page

View File

@@ -0,0 +1,29 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { AdminSearchPageComponent } from './admin-search-page.component';
/**
* Themed wrapper for {@link AdminSearchPageComponent}
*/
@Component({
selector: 'ds-admin-search-page',
templateUrl: '../../shared/theme-support/themed.component.html',
standalone: true,
imports: [AdminSearchPageComponent],
})
export class ThemedAdminSearchPageComponent extends ThemedComponent<AdminSearchPageComponent> {
protected getComponentName(): string {
return 'AdminSearchPageComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/admin/admin-search-page/admin-search-page.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import('./admin-search-page.component');
}
}

View File

@@ -1,3 +1,4 @@
@if (menuVisible | async) {
<nav class="navbar navbar-dark p-0 vh-100" <nav class="navbar navbar-dark p-0 vh-100"
id="admin-sidebar" id="admin-sidebar"
[attr.aria-label]="'menu.header.nav.description' | translate" [attr.aria-label]="'menu.header.nav.description' | translate"
@@ -6,12 +7,9 @@
value: ((sidebarExpanded | async) !== true ? 'collapsed' : 'expanded'), value: ((sidebarExpanded | async) !== true ? 'collapsed' : 'expanded'),
params: { collapsedWidth: (collapsedSidebarWidth$ | async), expandedWidth: (expandedSidebarWidth$ | async) } params: { collapsedWidth: (collapsedSidebarWidth$ | async), expandedWidth: (expandedSidebarWidth$ | async) }
}" (@slideSidebar.done)="finishSlide($event)" (@slideSidebar.start)="startSlide($event)" }" (@slideSidebar.done)="finishSlide($event)" (@slideSidebar.start)="startSlide($event)"
*ngIf="menuVisible | async"
(mouseenter)="handleMouseEnter($event)" (mouseenter)="handleMouseEnter($event)"
(mouseleave)="handleMouseLeave($event)"> (mouseleave)="handleMouseLeave($event)">
<!-- HEADER --> <!-- HEADER -->
<div class="sidebar-full-width-container" id="sidebar-header-container" aria-hidden="true"> <div class="sidebar-full-width-container" id="sidebar-header-container" aria-hidden="true">
<div class="sidebar-section-wrapper"> <div class="sidebar-section-wrapper">
<div class="sidebar-fixed-element-wrapper"> <div class="sidebar-fixed-element-wrapper">
@@ -24,21 +22,17 @@
</div> </div>
</div> </div>
</div> </div>
<!-- ITEMS --> <!-- ITEMS -->
<div class="sidebar-full-width-container" id="sidebar-top-level-items-container"> <div class="sidebar-full-width-container" id="sidebar-top-level-items-container">
<div class="sidebar-full-width-container" id="sidebar-top-level-items" role="menubar" <div class="sidebar-full-width-container" id="sidebar-top-level-items" role="menubar"
[attr.aria-label]="'menu.header.admin.description' |translate"> [attr.aria-label]="'menu.header.admin.description' |translate">
<ng-container *ngFor="let section of (sections | async)"> @for (section of (sections | async); track section) {
<ng-container <ng-container
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container> *ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
</ng-container> }
</div> </div>
</div> </div>
<!-- TOGGLER --> <!-- TOGGLER -->
<div class="sidebar-full-width-container" id="sidebar-collapse-toggle-container"> <div class="sidebar-full-width-container" id="sidebar-collapse-toggle-container">
<a class="sidebar-section-wrapper sidebar-full-width-container" <a class="sidebar-section-wrapper sidebar-full-width-container"
id="sidebar-collapse-toggle" id="sidebar-collapse-toggle"
@@ -48,10 +42,14 @@
(keyup.space)="toggle($event)" (keyup.space)="toggle($event)"
> >
<div class="sidebar-fixed-element-wrapper"> <div class="sidebar-fixed-element-wrapper">
<i *ngIf="(menuCollapsed | async)" class="fas fa-fw fa-angle-double-right" @if ((menuCollapsed | async)) {
<i class="fas fa-fw fa-angle-double-right"
[title]="'menu.section.icon.pin' | translate"></i> [title]="'menu.section.icon.pin' | translate"></i>
<i *ngIf="(menuCollapsed | async) !== true" class="fas fa-fw fa-angle-double-left" }
@if ((menuCollapsed | async) !== true) {
<i class="fas fa-fw fa-angle-double-left"
[title]="'menu.section.icon.unpin' | translate"></i> [title]="'menu.section.icon.unpin' | translate"></i>
}
</div> </div>
<div class="sidebar-collapsible-element-outer-wrapper"> <div class="sidebar-collapsible-element-outer-wrapper">
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item"> <div class="sidebar-collapsible-element-inner-wrapper sidebar-item">
@@ -60,5 +58,5 @@
</div> </div>
</a> </a>
</div> </div>
</nav> </nav>
}

View File

@@ -2,8 +2,6 @@ import {
AsyncPipe, AsyncPipe,
NgClass, NgClass,
NgComponentOutlet, NgComponentOutlet,
NgFor,
NgIf,
} from '@angular/common'; } from '@angular/common';
import { import {
Component, Component,
@@ -47,7 +45,7 @@ import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe';
styleUrls: ['./admin-sidebar.component.scss'], styleUrls: ['./admin-sidebar.component.scss'],
animations: [slideSidebar], animations: [slideSidebar],
standalone: true, standalone: true,
imports: [NgIf, NgbDropdownModule, NgClass, NgFor, NgComponentOutlet, AsyncPipe, TranslateModule, BrowserOnlyPipe], imports: [NgbDropdownModule, NgClass, NgComponentOutlet, AsyncPipe, TranslateModule, BrowserOnlyPipe],
}) })
export class AdminSidebarComponent extends MenuComponent implements OnInit { export class AdminSidebarComponent extends MenuComponent implements OnInit {
/** /**

View File

@@ -29,17 +29,21 @@
</div> </div>
</div> </div>
</a> </a>
<div class="sidebar-section-wrapper subsection" @slide *ngIf="(isExpanded$ | async)"> @if ((isExpanded$ | async)) {
<div class="sidebar-section-wrapper subsection" @slide>
<div class="sidebar-fixed-element-wrapper"></div> <div class="sidebar-fixed-element-wrapper"></div>
<div class="sidebar-collapsible-element-outer-wrapper"> <div class="sidebar-collapsible-element-outer-wrapper">
<div class="sidebar-collapsible-element-inner-wrapper"> <div class="sidebar-collapsible-element-inner-wrapper">
<div class="sidebar-sub-level-item-list" role="menu" [id]="adminMenuSectionId(section.id)" [attr.aria-label]="('menu.section.' + section.id) | translate"> <div class="sidebar-sub-level-item-list" role="menu" [id]="adminMenuSectionId(section.id)" [attr.aria-label]="('menu.section.' + section.id) | translate">
<div class="sidebar-item" *ngFor="let subSection of (subSections$ | async)"> @for (subSection of (subSections$ | async); track subSection) {
<div class="sidebar-item">
<ng-container <ng-container
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container> *ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
</div> </div>
}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
}
</div> </div>

View File

@@ -2,8 +2,6 @@ import {
AsyncPipe, AsyncPipe,
NgClass, NgClass,
NgComponentOutlet, NgComponentOutlet,
NgFor,
NgIf,
} from '@angular/common'; } from '@angular/common';
import { import {
Component, Component,
@@ -37,7 +35,7 @@ import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sid
styleUrls: ['./expandable-admin-sidebar-section.component.scss'], styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
animations: [rotate, slide, bgColor], animations: [rotate, slide, bgColor],
standalone: true, standalone: true,
imports: [NgClass, NgComponentOutlet, NgIf, NgFor, AsyncPipe, TranslateModule, BrowserOnlyPipe], imports: [NgClass, NgComponentOutlet, AsyncPipe, TranslateModule, BrowserOnlyPipe],
}) })
export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionComponent implements OnInit { export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionComponent implements OnInit {
@@ -85,7 +83,7 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
this.sidebarActiveBg$ = this.variableService.getVariable('--ds-admin-sidebar-active-bg'); this.sidebarActiveBg$ = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
this.isSidebarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID); this.isSidebarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID);
this.isSidebarPreviewCollapsed$ = this.menuService.isMenuPreviewCollapsed(this.menuID); this.isSidebarPreviewCollapsed$ = this.menuService.isMenuPreviewCollapsed(this.menuID);
this.isExpanded$ = combineLatestObservable([this.active, this.isSidebarCollapsed$, this.isSidebarPreviewCollapsed$]).pipe( this.isExpanded$ = combineLatestObservable([this.active$, this.isSidebarCollapsed$, this.isSidebarPreviewCollapsed$]).pipe(
map(([active, sidebarCollapsed, sidebarPreviewCollapsed]) => (active && (!sidebarCollapsed || !sidebarPreviewCollapsed))), map(([active, sidebarCollapsed, sidebarPreviewCollapsed]) => (active && (!sidebarCollapsed || !sidebarPreviewCollapsed))),
); );
} }

View File

@@ -4,11 +4,13 @@ import { Context } from '../../core/shared/context.model';
import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component'; import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component';
@Component({ @Component({
selector: 'ds-admin-workflow-page', selector: 'ds-base-admin-workflow-page',
templateUrl: './admin-workflow-page.component.html', templateUrl: './admin-workflow-page.component.html',
styleUrls: ['./admin-workflow-page.component.scss'], styleUrls: ['./admin-workflow-page.component.scss'],
standalone: true, standalone: true,
imports: [ThemedConfigurationSearchPageComponent], imports: [
ThemedConfigurationSearchPageComponent,
],
}) })
/** /**

View File

@@ -1,8 +1,12 @@
<div class="space-children-mr"> <div class="space-children-mr">
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.workflow.item.delete' | translate"> <a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.workflow.item.delete' | translate">
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.workflow.item.delete" | translate}}</span> <i class="fa fa-trash"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.workflow.item.delete" | translate}}</span>
}
</a> </a>
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 send-back-link" [routerLink]="[getSendBackRoute()]" [title]="'admin.workflow.item.send-back' | translate"> <a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 send-back-link" [routerLink]="[getSendBackRoute()]" [title]="'admin.workflow.item.send-back' | translate">
<i class="fa fa-hand-point-left"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.workflow.item.send-back" | translate}}</span> <i class="fa fa-hand-point-left"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.workflow.item.send-back" | translate}}</span>
}
</a> </a>
</div> </div>

View File

@@ -1,7 +1,4 @@
import { import { NgClass } from '@angular/common';
NgClass,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
Input, Input,
@@ -20,7 +17,7 @@ import {
styleUrls: ['./workflow-item-admin-workflow-actions.component.scss'], styleUrls: ['./workflow-item-admin-workflow-actions.component.scss'],
templateUrl: './workflow-item-admin-workflow-actions.component.html', templateUrl: './workflow-item-admin-workflow-actions.component.html',
standalone: true, standalone: true,
imports: [NgClass, RouterLink, NgIf, TranslateModule], imports: [NgClass, RouterLink, TranslateModule],
}) })
/** /**
* The component for displaying the actions for a list element for a workflow-item on the admin workflow search page * The component for displaying the actions for a list element for a workflow-item on the admin workflow search page

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