diff --git a/.eslintrc.json b/.eslintrc.json
index 4cc8c6dbc5a..1151e6831be 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -12,7 +12,6 @@
"eslint-plugin-rxjs",
"eslint-plugin-simple-import-sort",
"eslint-plugin-import-newlines",
- "eslint-plugin-jsonc",
"dspace-angular-ts",
"dspace-angular-html"
],
@@ -165,6 +164,7 @@
"@angular-eslint/no-output-native": "warn",
"@angular-eslint/no-output-on-prefix": "warn",
"@angular-eslint/no-conflicting-lifecycle": "warn",
+ "@angular-eslint/use-lifecycle-interface": "error",
"@typescript-eslint/no-inferrable-types":[
"error",
@@ -292,7 +292,8 @@
],
"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"
}
},
{
@@ -300,10 +301,13 @@
"*.json5"
],
"extends": [
- "plugin:jsonc/recommended-with-jsonc"
+ "plugin:jsonc/recommended-with-json5"
],
"rules": {
- "no-irregular-whitespace": "error",
+ // The ESLint core no-irregular-whitespace rule doesn't work well in JSON
+ // See: https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-irregular-whitespace.html
+ "no-irregular-whitespace": "off",
+ "jsonc/no-irregular-whitespace": "error",
"no-trailing-spaces": "error",
"jsonc/comma-dangle": [
"error",
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4f2a84ce8a6..8e3613acae2 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,7 +7,8 @@ name: Build
on: [push, pull_request]
permissions:
- contents: read # to fetch code (actions/checkout)
+ contents: read # to fetch code (actions/checkout)
+ packages: read # to fetch private images from GitHub Container Registry (GHCR)
jobs:
tests:
@@ -28,6 +29,8 @@ jobs:
DSPACE_CACHE_SERVERSIDE_ANONYMOUSCACHE_MAX: 0
# Tell Cypress to run e2e tests using the same UI URL
CYPRESS_BASE_URL: http://127.0.0.1:4000
+ # Disable the cookie consent banner in e2e tests to avoid errors because of elements hidden by it
+ DSPACE_INFO_ENABLECOOKIECONSENTPOPUP: false
# When Chrome version is specified, we pin to a specific version of Chrome
# Comment this out to use the latest release
#CHROME_VERSION: "90.0.4430.212-1"
@@ -35,6 +38,9 @@ jobs:
NODE_OPTIONS: '--max-old-space-size=4096'
# Project name to use when running "docker compose" prior to e2e tests
COMPOSE_PROJECT_NAME: 'ci'
+ # Docker Registry to use for Docker compose scripts below.
+ # We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub.
+ DOCKER_REGISTRY: ghcr.io
strategy:
# Create a matrix of Node versions to test against (in parallel)
matrix:
@@ -114,6 +120,14 @@ jobs:
path: 'coverage/dspace-angular/lcov.info'
retention-days: 14
+ # Login to our Docker registry, so that we can access private Docker images using "docker compose" below.
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ github.repository_owner }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
# Using "docker compose" start backend using CI configuration
# and load assetstore from a cached copy
- name: Start DSpace REST Backend via Docker (for e2e tests)
@@ -178,12 +192,115 @@ jobs:
# Get homepage and verify that the tag includes "DSpace".
# If it does, then SSR is working, as this tag is created by our MetadataService.
# This step also prints entire HTML of homepage for easier debugging if grep fails.
- - name: Verify SSR (server-side rendering)
+ - name: Verify SSR (server-side rendering) on Homepage
run: |
result=$(wget -O- -q http://127.0.0.1:4000/home)
echo "$result"
echo "$result" | grep -oE "]*>" | grep DSpace
+ # Get a specific community in our test data and verify that the "
" tag includes "Publications" (the community name).
+ # If it does, then SSR is working.
+ - name: Verify SSR on a Community page
+ run: |
+ result=$(wget -O- -q http://127.0.0.1:4000/communities/0958c910-2037-42a9-81c7-dca80e3892b4)
+ echo "$result"
+ echo "$result" | grep -oE "
]*>[^><]*
" | grep Publications
+
+ # Get a specific collection in our test data and verify that the "
" tag includes "Articles" (the collection name).
+ # If it does, then SSR is working.
+ - name: Verify SSR on a Collection page
+ run: |
+ result=$(wget -O- -q http://127.0.0.1:4000/collections/282164f5-d325-4740-8dd1-fa4d6d3e7200)
+ echo "$result"
+ echo "$result" | grep -oE "
]*>[^><]*
" | grep Articles
+
+ # Get a specific publication in our test data and verify that the tag includes
+ # the title of this publication. If it does, then SSR is working.
+ - name: Verify SSR on a Publication page
+ run: |
+ result=$(wget -O- -q http://127.0.0.1:4000/entities/publication/6160810f-1e53-40db-81ef-f6621a727398)
+ echo "$result"
+ echo "$result" | grep -oE "]*>" | grep "An Economic Model of Mortality Salience"
+
+ # Get a specific person in our test data and verify that the tag includes
+ # the name of the person. If it does, then SSR is working.
+ - name: Verify SSR on a Person page
+ run: |
+ result=$(wget -O- -q http://127.0.0.1:4000/entities/person/b1b2c768-bda1-448a-a073-fc541e8b24d9)
+ echo "$result"
+ echo "$result" | grep -oE "]*>" | grep "Simmons, Cameron"
+
+ # Get a specific project in our test data and verify that the tag includes
+ # the name of the project. If it does, then SSR is working.
+ - name: Verify SSR on a Project page
+ run: |
+ result=$(wget -O- -q http://127.0.0.1:4000/entities/project/46ccb608-a74c-4bf6-bc7a-e29cc7defea9)
+ echo "$result"
+ echo "$result" | grep -oE "]*>" | grep "University Research Fellowship"
+
+ # Get a specific orgunit in our test data and verify that the tag includes
+ # the name of the orgunit. If it does, then SSR is working.
+ - name: Verify SSR on an OrgUnit page
+ run: |
+ result=$(wget -O- -q http://127.0.0.1:4000/entities/orgunit/9851674d-bd9a-467b-8d84-068deb568ccf)
+ echo "$result"
+ echo "$result" | grep -oE "]*>" | grep "Law and Development"
+
+ # Get a specific journal in our test data and verify that the tag includes
+ # the name of the journal. If it does, then SSR is working.
+ - name: Verify SSR on a Journal page
+ run: |
+ result=$(wget -O- -q http://127.0.0.1:4000/entities/journal/d4af6c3e-53d0-4757-81eb-566f3b45d63a)
+ echo "$result"
+ echo "$result" | grep -oE "]*>" | grep "Environmental & Architectural Phenomenology"
+
+ # Get a specific journal volume in our test data and verify that the tag includes
+ # the name of the volume. If it does, then SSR is working.
+ - name: Verify SSR on a Journal Volume page
+ run: |
+ result=$(wget -O- -q http://127.0.0.1:4000/entities/journalvolume/07c6249f-4bf7-494d-9ce3-6ffdb2aed538)
+ echo "$result"
+ echo "$result" | grep -oE "]*>" | grep "Environmental & Architectural Phenomenology Volume 28 (2017)"
+
+ # Get a specific journal issue in our test data and verify that the tag includes
+ # the name of the issue. If it does, then SSR is working.
+ - name: Verify SSR on a Journal Issue page
+ run: |
+ result=$(wget -O- -q http://127.0.0.1:4000/entities/journalissue/44c29473-5de2-48fa-b005-e5029aa1a50b)
+ echo "$result"
+ echo "$result" | grep -oE "]*>" | grep "Environmental & Architectural Phenomenology Vol. 28, No. 1"
+
+ # Verify 301 Handle redirect behavior
+ # Note: /handle/123456789/260 is the same test Publication used by our e2e tests
+ - name: Verify 301 redirect from '/handle' URLs
+ run: |
+ result=$(wget --server-response --quiet http://127.0.0.1:4000/handle/123456789/260 2>&1 | head -1 | awk '{print $2}')
+ echo "$result"
+ [[ "$result" -eq "301" ]]
+
+ # Verify 403 error code behavior
+ - name: Verify 403 error code from '/403'
+ run: |
+ result=$(wget --server-response --quiet http://127.0.0.1:4000/403 2>&1 | head -1 | awk '{print $2}')
+ echo "$result"
+ [[ "$result" -eq "403" ]]
+
+ # Verify 404 error code behavior
+ - name: Verify 404 error code from '/404' and on invalid pages
+ run: |
+ result=$(wget --server-response --quiet http://127.0.0.1:4000/404 2>&1 | head -1 | awk '{print $2}')
+ echo "$result"
+ result2=$(wget --server-response --quiet http://127.0.0.1:4000/invalidurl 2>&1 | head -1 | awk '{print $2}')
+ echo "$result2"
+ [[ "$result" -eq "404" && "$result2" -eq "404" ]]
+
+ # Verify 500 error code behavior
+ - name: Verify 500 error code from '/500'
+ run: |
+ result=$(wget --server-response --quiet http://127.0.0.1:4000/500 2>&1 | head -1 | awk '{print $2}')
+ echo "$result"
+ [[ "$result" -eq "500" ]]
+
- name: Stop running app
run: kill -9 $(lsof -t -i:4000)
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index d0b4cd0939f..bae8c013005 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -16,7 +16,8 @@ on:
pull_request:
permissions:
- contents: read # to fetch code (actions/checkout)
+ contents: read # to fetch code (actions/checkout)
+ packages: write # to write images to GitHub Container Registry (GHCR)
jobs:
#############################################################
diff --git a/Dockerfile b/Dockerfile
index 8fac7495e1f..e395e4b90e2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
# This image will be published as dspace/dspace-angular
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
-FROM node:18-alpine
+FROM docker.io/node:18-alpine
# Ensure Python and other build tools are available
# These are needed to install some node modules, especially on linux/arm64
@@ -24,5 +24,5 @@ ENV NODE_OPTIONS="--max_old_space_size=4096"
# Listen / accept connections from all IP addresses.
# NOTE: At this time it is only possible to run Docker container in Production mode
# if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485
-ENV NODE_ENV development
+ENV NODE_ENV=development
CMD yarn serve --host 0.0.0.0
diff --git a/Dockerfile.dist b/Dockerfile.dist
index 4c47b0cb406..be72de4afc4 100644
--- a/Dockerfile.dist
+++ b/Dockerfile.dist
@@ -2,9 +2,9 @@
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
# Test build:
-# docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist .
+# docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-8_x-dist .
-FROM node:18-alpine AS build
+FROM docker.io/node:18-alpine AS build
# Ensure Python and other build tools are available
# These are needed to install some node modules, especially on linux/arm64
@@ -26,6 +26,6 @@ COPY --chown=node:node docker/dspace-ui.json /app/dspace-ui.json
WORKDIR /app
USER node
-ENV NODE_ENV production
+ENV NODE_ENV=production
EXPOSE 4000
CMD pm2-runtime start dspace-ui.json --json
diff --git a/README.md b/README.md
index ebc24f8b918..fe2af85aa40 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
Quick start
-----------
-**Ensure you're running [Node](https://nodejs.org) `v16.x` or `v18.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`**
+**Ensure you're running [Node](https://nodejs.org) `v18.x` or `v20.x`, [npm](https://www.npmjs.com/) >= `v10.x` and [yarn](https://yarnpkg.com) == `v1.x`**
```bash
# clone the repo
@@ -90,7 +90,7 @@ Requirements
------------
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
-- Ensure you're running node `v16.x` or `v18.x` and yarn == `v1.x`
+- Ensure you're running node `v18.x` or `v20.x` and yarn == `v1.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.
diff --git a/angular.json b/angular.json
index 5f0204249b1..02fd69b1e1b 100644
--- a/angular.json
+++ b/angular.json
@@ -30,7 +30,6 @@
"lodash",
"jwt-decode",
"uuid",
- "webfontloader",
"zone.js"
],
"outputPath": "dist/browser",
diff --git a/config/config.example.yml b/config/config.example.yml
index 93386274e63..8ea58b96e40 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -1,7 +1,7 @@
# NOTE: will log all redux actions and transfers in console
debug: false
-# Angular Universal server settings
+# Angular User Inteface settings
# NOTE: these settings define where Node.js will start your UI application. Therefore, these
# "ui" settings usually specify a localhost port/URL which is later proxied to a public URL (using Apache or similar)
ui:
@@ -17,12 +17,51 @@ ui:
# Trust X-FORWARDED-* headers from proxies (default = true)
useProxies: true
-universal:
- # Whether to inline "critical" styles into the server-side rendered HTML.
- # Determining which styles are critical is a relatively expensive operation;
- # this option can be disabled to boost server performance at the expense of
- # loading smoothness.
- inlineCriticalCss: true
+# Angular Server Side Rendering (SSR) settings
+ssr:
+ # Whether to tell Angular to inline "critical" styles into the server-side rendered HTML.
+ # Determining which styles are critical is a relatively expensive operation; this option is
+ # disabled (false) by default to boost server performance at the expense of loading smoothness.
+ inlineCriticalCss: false
+ # Patterns to be run as regexes against the path of the page to check if SSR is allowed.
+ # If the path match any of the regexes it will be served directly in CSR.
+ # By default, excludes community and collection browse, global browse, global search, community list, statistics and various administrative tools.
+ excludePathPatterns:
+ - pattern: "^/communities/[a-f0-9-]{36}/browse(/.*)?$"
+ flag: "i"
+ - pattern: "^/collections/[a-f0-9-]{36}/browse(/.*)?$"
+ flag: "i"
+ - pattern: "^/browse/"
+ - pattern: "^/search$"
+ - pattern: "^/community-list$"
+ - pattern: "^/admin/"
+ - pattern: "^/processes/?"
+ - pattern: "^/notifications/"
+ - pattern: "^/statistics/?"
+ - pattern: "^/access-control/"
+ - pattern: "^/health$"
+
+ # Whether to enable rendering of Search component on SSR.
+ # If set to true the component will be included in the HTML returned from the server side rendering.
+ # If set to false the component will not be included in the HTML returned from the server side rendering.
+ enableSearchComponent: false
+ # Whether to enable rendering of Browse component on SSR.
+ # If set to true the component will be included in the HTML returned from the server side rendering.
+ # If set to false the component will not be included in the HTML returned from the server side rendering.
+ enableBrowseComponent: false
+ # Enable state transfer from the server-side application to the client-side application.
+ # Defaults to true.
+ # Note: When using an external application cache layer, it's recommended not to transfer the state to avoid caching it.
+ # Disabling it ensures that dynamic state information is not inadvertently cached, which can improve security and
+ # ensure that users always use the most up-to-date state.
+ transferState: true
+ # When a different REST base URL is used for the server-side application, the generated state contains references to
+ # REST resources with the internal URL configured. By default, these internal URLs are replaced with public URLs.
+ # Disable this setting to avoid URL replacement during SSR. In this the state is not transferred to avoid security issues.
+ replaceRestUrl: true
+ # Enable request performance profiling data collection and printing the results in the server console.
+ # Defaults to false. Enabling in production is NOT recommended
+ #enablePerformanceProfiler: false
# The REST API server settings
# NOTE: these settings define which (publicly available) REST API to use. They are usually
@@ -33,6 +72,9 @@ rest:
port: 443
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
nameSpace: /server
+ # Provide a different REST url to be used during SSR execution. It must contain the whole url including protocol, server port and
+ # server namespace (uncomment to use it).
+ #ssrBaseUrl: http://localhost:8080/server
# Caching settings
cache:
@@ -325,12 +367,20 @@ item:
# Community Page Config
community:
+ # Default tab to be shown when browsing a Community. Valid values are: comcols, search, or browse_
+ # must be any of the configured "browse by" fields, e.g., dateissued, author, title, or subject
+ # When the default tab is not the 'search' tab, the search tab is moved to the last position
+ defaultBrowseTab: search
# Search tab config
searchSection:
showSidebar: true
# Collection Page Config
collection:
+ # Default tab to be shown when browsing a Collection. Valid values are: search, or browse_
+ # must be any of the configured "browse by" fields, e.g., dateissued, author, title, or subject
+ # When the default tab is not the 'search' tab, the search tab is moved to the last position
+ defaultBrowseTab: search
# Search tab config
searchSection:
showSidebar: true
@@ -448,6 +498,12 @@ search:
enabled: false
# List of filters to enable in "Advanced Search" dropdown
filter: [ 'title', 'author', 'subject', 'entityType' ]
+ #
+ # Number used to render n UI elements called loading skeletons that act as placeholders.
+ # These elements indicate that some content will be loaded in their stead.
+ # Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count.
+ # e.g. If we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved.
+ defaultFiltersCount: 5
# Notify metrics
@@ -503,6 +559,21 @@ notifyMetrics:
description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description'
-
-
-
+# Live Region configuration
+# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms:
+# Live regions are perceivable regions of a web page that are typically updated as a
+# result of an external event when user focus may be elsewhere.
+#
+# The DSpace live region is a component present at the bottom of all pages that is invisible by default, but is useful
+# for screen readers. Any message pushed to the live region will be announced by the screen reader. These messages
+# usually contain information about changes on the page that might not be in focus.
+liveRegion:
+ # The duration after which messages disappear from the live region in milliseconds
+ messageTimeOutDurationMs: 30000
+ # The visibility of the live region. Setting this to true is only useful for debugging purposes.
+ isVisible: false
+
+# Configuration for storing accessibility settings, used by the AccessibilitySettingsService
+accessibility:
+ # The duration in days after which the accessibility settings cookie expires
+ cookieExpirationDuration: 7
diff --git a/cypress.config.ts b/cypress.config.ts
index 458b035a484..36d8120342a 100644
--- a/cypress.config.ts
+++ b/cypress.config.ts
@@ -1,6 +1,7 @@
import { defineConfig } from 'cypress';
export default defineConfig({
+ video: true,
videosFolder: 'cypress/videos',
screenshotsFolder: 'cypress/screenshots',
fixturesFolder: 'cypress/fixtures',
@@ -18,6 +19,7 @@ export default defineConfig({
// Admin account used for administrative tests
DSPACE_TEST_ADMIN_USER: 'dspacedemo+admin@gmail.com',
+ DSPACE_TEST_ADMIN_USER_UUID: '335647b6-8a52-4ecb-a8c1-7ebabb199bda',
DSPACE_TEST_ADMIN_PASSWORD: 'dspace',
// Community/collection/publication used for view/edit tests
DSPACE_TEST_COMMUNITY: '0958c910-2037-42a9-81c7-dca80e3892b4',
@@ -33,6 +35,8 @@ export default defineConfig({
// Account used to test basic submission process
DSPACE_TEST_SUBMIT_USER: 'dspacedemo+submit@gmail.com',
DSPACE_TEST_SUBMIT_USER_PASSWORD: 'dspace',
+ // Administrator users group
+ DSPACE_ADMINISTRATOR_GROUP: 'e59f5659-bff9-451e-b28f-439e7bd467e4'
},
e2e: {
// Setup our plugins for e2e tests
diff --git a/cypress/e2e/admin-add-new-modals.cy.ts b/cypress/e2e/admin-add-new-modals.cy.ts
new file mode 100644
index 00000000000..332d44da138
--- /dev/null
+++ b/cypress/e2e/admin-add-new-modals.cy.ts
@@ -0,0 +1,54 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Admin Add New Modals', () => {
+ beforeEach(() => {
+ // Must login as an Admin for sidebar to appear
+ cy.visit('/login');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('Add new Community modal should pass accessibility tests', () => {
+ // Pin the sidebar open
+ cy.get('#sidebar-collapse-toggle').trigger('mouseover');
+ cy.get('#sidebar-collapse-toggle').click();
+
+ // Click on entry of menu
+ cy.get('#admin-menu-section-new-title').should('be.visible');
+ cy.get('#admin-menu-section-new-title').click();
+
+ cy.get('a[data-test="menu.section.new_community"]').click();
+
+ // Analyze for accessibility
+ testA11y('ds-create-community-parent-selector');
+ });
+
+ it('Add new Collection modal should pass accessibility tests', () => {
+ // Pin the sidebar open
+ cy.get('#sidebar-collapse-toggle').trigger('mouseover');
+ cy.get('#sidebar-collapse-toggle').click();
+
+ // Click on entry of menu
+ cy.get('#admin-menu-section-new-title').should('be.visible');
+ cy.get('#admin-menu-section-new-title').click();
+
+ cy.get('a[data-test="menu.section.new_collection"]').click();
+
+ // Analyze for accessibility
+ testA11y('ds-create-collection-parent-selector');
+ });
+
+ it('Add new Item modal should pass accessibility tests', () => {
+ // Pin the sidebar open
+ cy.get('#sidebar-collapse-toggle').trigger('mouseover');
+ cy.get('#sidebar-collapse-toggle').click();
+
+ // Click on entry of menu
+ cy.get('#admin-menu-section-new-title').should('be.visible');
+ cy.get('#admin-menu-section-new-title').click();
+
+ cy.get('a[data-test="menu.section.new_item"]').click();
+
+ // Analyze for accessibility
+ testA11y('ds-create-item-parent-selector');
+ });
+});
diff --git a/cypress/e2e/admin-curation-tasks.cy.ts b/cypress/e2e/admin-curation-tasks.cy.ts
new file mode 100644
index 00000000000..e66f0ccaad8
--- /dev/null
+++ b/cypress/e2e/admin-curation-tasks.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Admin Curation Tasks', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/admin/curation-tasks');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Page must first be visible
+ cy.get('ds-admin-curation-task').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-admin-curation-task');
+ });
+});
diff --git a/cypress/e2e/admin-edit-modals.cy.ts b/cypress/e2e/admin-edit-modals.cy.ts
new file mode 100644
index 00000000000..8ba524d5be1
--- /dev/null
+++ b/cypress/e2e/admin-edit-modals.cy.ts
@@ -0,0 +1,54 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Admin Edit Modals', () => {
+ beforeEach(() => {
+ // Must login as an Admin for sidebar to appear
+ cy.visit('/login');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('Edit Community modal should pass accessibility tests', () => {
+ // Pin the sidebar open
+ cy.get('#sidebar-collapse-toggle').trigger('mouseover');
+ cy.get('#sidebar-collapse-toggle').click();
+
+ // Click on entry of menu
+ cy.get('#admin-menu-section-edit-title').should('be.visible');
+ cy.get('#admin-menu-section-edit-title').click();
+
+ cy.get('a[data-test="menu.section.edit_community"]').click();
+
+ // Analyze for accessibility
+ testA11y('ds-edit-community-selector');
+ });
+
+ it('Edit Collection modal should pass accessibility tests', () => {
+ // Pin the sidebar open
+ cy.get('#sidebar-collapse-toggle').trigger('mouseover');
+ cy.get('#sidebar-collapse-toggle').click();
+
+ // Click on entry of menu
+ cy.get('#admin-menu-section-edit-title').should('be.visible');
+ cy.get('#admin-menu-section-edit-title').click();
+
+ cy.get('a[data-test="menu.section.edit_collection"]').click();
+
+ // Analyze for accessibility
+ testA11y('ds-edit-collection-selector');
+ });
+
+ it('Edit Item modal should pass accessibility tests', () => {
+ // Pin the sidebar open
+ cy.get('#sidebar-collapse-toggle').trigger('mouseover');
+ cy.get('#sidebar-collapse-toggle').click();
+
+ // Click on entry of menu
+ cy.get('#admin-menu-section-edit-title').should('be.visible');
+ cy.get('#admin-menu-section-edit-title').click();
+
+ cy.get('a[data-test="menu.section.edit_item"]').click();
+
+ // Analyze for accessibility
+ testA11y('ds-edit-item-selector');
+ });
+});
diff --git a/cypress/e2e/admin-export-modals.cy.ts b/cypress/e2e/admin-export-modals.cy.ts
new file mode 100644
index 00000000000..884db4ed33e
--- /dev/null
+++ b/cypress/e2e/admin-export-modals.cy.ts
@@ -0,0 +1,39 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Admin Export Modals', () => {
+ beforeEach(() => {
+ // Must login as an Admin for sidebar to appear
+ cy.visit('/login');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('Export metadata modal should pass accessibility tests', () => {
+ // Pin the sidebar open
+ cy.get('#sidebar-collapse-toggle').trigger('mouseover');
+ cy.get('#sidebar-collapse-toggle').click();
+
+ // Click on entry of menu
+ cy.get('#admin-menu-section-export-title').should('be.visible');
+ cy.get('#admin-menu-section-export-title').click();
+
+ cy.get('a[data-test="menu.section.export_metadata"]').click();
+
+ // Analyze for accessibility
+ testA11y('ds-export-metadata-selector');
+ });
+
+ it('Export batch modal should pass accessibility tests', () => {
+ // Pin the sidebar open
+ cy.get('#sidebar-collapse-toggle').trigger('mouseover');
+ cy.get('#sidebar-collapse-toggle').click();
+
+ // Click on entry of menu
+ cy.get('#admin-menu-section-export-title').should('be.visible');
+ cy.get('#admin-menu-section-export-title').click();
+
+ cy.get('a[data-test="menu.section.export_batch"]').click();
+
+ // Analyze for accessibility
+ testA11y('ds-export-batch-selector');
+ });
+});
diff --git a/cypress/e2e/admin-notifications-publication-claim-page.cy.ts b/cypress/e2e/admin-notifications-publication-claim-page.cy.ts
new file mode 100644
index 00000000000..877a0542e25
--- /dev/null
+++ b/cypress/e2e/admin-notifications-publication-claim-page.cy.ts
@@ -0,0 +1,17 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Admin Notifications Publication Claim Page', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/admin/notifications/publication-claim');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+
+ //Page must first be visible
+ cy.get('ds-admin-notifications-publication-claim-page').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-admin-notifications-publication-claim-page');
+ });
+});
diff --git a/cypress/e2e/admin-search-page.cy.ts b/cypress/e2e/admin-search-page.cy.ts
new file mode 100644
index 00000000000..4fbf8939fe4
--- /dev/null
+++ b/cypress/e2e/admin-search-page.cy.ts
@@ -0,0 +1,21 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Admin Search Page', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/admin/search');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ //Page must first be visible
+ cy.get('ds-admin-search-page').should('be.visible');
+ // At least one search result should be displayed
+ cy.get('[data-test="list-object"]').should('be.visible');
+ // Click each filter toggle to open *every* filter
+ // (As we want to scan filter section for accessibility issues as well)
+ cy.get('[data-test="filter-toggle"]').click({ multiple: true });
+ // Analyze for accessibility issues
+ testA11y('ds-admin-search-page');
+ });
+});
diff --git a/cypress/e2e/admin-workflow-page.cy.ts b/cypress/e2e/admin-workflow-page.cy.ts
new file mode 100644
index 00000000000..c3c235e346d
--- /dev/null
+++ b/cypress/e2e/admin-workflow-page.cy.ts
@@ -0,0 +1,21 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Admin Workflow Page', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/admin/workflow');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Page must first be visible
+ cy.get('ds-admin-workflow-page').should('be.visible');
+ // At least one search result should be displayed
+ cy.get('[data-test="list-object"]').should('be.visible');
+ // Click each filter toggle to open *every* filter
+ // (As we want to scan filter section for accessibility issues as well)
+ cy.get('[data-test="filter-toggle"]').click({ multiple: true });
+ // Analyze for accessibility issues
+ testA11y('ds-admin-workflow-page');
+ });
+});
diff --git a/cypress/e2e/batch-import-page.cy.ts b/cypress/e2e/batch-import-page.cy.ts
new file mode 100644
index 00000000000..871b8644ce1
--- /dev/null
+++ b/cypress/e2e/batch-import-page.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Batch Import Page', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see processes
+ cy.visit('/admin/batch-import');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Batch import form must first be visible
+ cy.get('ds-batch-import-page').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-batch-import-page');
+ });
+});
diff --git a/cypress/e2e/bitstreams-format.cy.ts b/cypress/e2e/bitstreams-format.cy.ts
new file mode 100644
index 00000000000..f113d45ebce
--- /dev/null
+++ b/cypress/e2e/bitstreams-format.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Bitstreams Formats', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/admin/registries/bitstream-formats');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Page must first be visible
+ cy.get('ds-bitstream-formats').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-bitstream-formats');
+ });
+});
diff --git a/cypress/e2e/bulk-access.cy.ts b/cypress/e2e/bulk-access.cy.ts
new file mode 100644
index 00000000000..87033e13e4f
--- /dev/null
+++ b/cypress/e2e/bulk-access.cy.ts
@@ -0,0 +1,31 @@
+import { testA11y } from 'cypress/support/utils';
+import { Options } from 'cypress-axe';
+
+describe('Bulk Access', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/access-control/bulk-access');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Page must first be visible
+ cy.get('ds-bulk-access').should('be.visible');
+ // At least one search result should be displayed
+ cy.get('[data-test="list-object"]').should('be.visible');
+ // Click each filter toggle to open *every* filter
+ // (As we want to scan filter section for accessibility issues as well)
+ cy.get('[data-test="filter-toggle"]').click({ multiple: true });
+ // Analyze for accessibility issues
+ testA11y('ds-bulk-access', {
+ rules: {
+ // All panels are accordians & fail "aria-required-children" and "nested-interactive".
+ // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
+ 'aria-required-children': { enabled: false },
+ 'nested-interactive': { enabled: false },
+ // Card titles fail this test currently
+ 'heading-order': { enabled: false },
+ },
+ } as Options);
+ });
+});
diff --git a/cypress/e2e/create-eperson.cy.ts b/cypress/e2e/create-eperson.cy.ts
new file mode 100644
index 00000000000..d23986ba29d
--- /dev/null
+++ b/cypress/e2e/create-eperson.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Create Eperson', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/access-control/epeople/create');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Form must first be visible
+ cy.get('ds-eperson-form').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-eperson-form');
+ });
+});
diff --git a/cypress/e2e/create-group.cy.ts b/cypress/e2e/create-group.cy.ts
new file mode 100644
index 00000000000..135c041a8d5
--- /dev/null
+++ b/cypress/e2e/create-group.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Create Group', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/access-control/groups/create');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Form must first be visible
+ cy.get('ds-group-form').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-group-form');
+ });
+});
diff --git a/cypress/e2e/edit-eperson.cy.ts b/cypress/e2e/edit-eperson.cy.ts
new file mode 100644
index 00000000000..166c913b8c8
--- /dev/null
+++ b/cypress/e2e/edit-eperson.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Edit Eperson', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/access-control/epeople/'.concat(Cypress.env('DSPACE_TEST_ADMIN_USER_UUID')).concat('/edit'));
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Form must first be visible
+ cy.get('ds-eperson-form').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-eperson-form');
+ });
+});
diff --git a/cypress/e2e/edit-group.cy.ts b/cypress/e2e/edit-group.cy.ts
new file mode 100644
index 00000000000..e43ede978ad
--- /dev/null
+++ b/cypress/e2e/edit-group.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Edit Group', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/access-control/groups/'.concat(Cypress.env('DSPACE_ADMINISTRATOR_GROUP')).concat('/edit'));
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Form must first be visible
+ cy.get('ds-group-form').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-group-form');
+ });
+});
diff --git a/cypress/e2e/end-user-agreement.cy.ts b/cypress/e2e/end-user-agreement.cy.ts
new file mode 100644
index 00000000000..989d21ce60f
--- /dev/null
+++ b/cypress/e2e/end-user-agreement.cy.ts
@@ -0,0 +1,13 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('End User Agreement', () => {
+ it('should pass accessibility tests', () => {
+ cy.visit('/info/end-user-agreement');
+
+ // Page must first be visible
+ cy.get('ds-end-user-agreement').should('be.visible');
+
+ // Analyze for accessibility
+ testA11y('ds-end-user-agreement');
+ });
+});
diff --git a/cypress/e2e/epeople-registry.cy.ts b/cypress/e2e/epeople-registry.cy.ts
new file mode 100644
index 00000000000..a6192f13d95
--- /dev/null
+++ b/cypress/e2e/epeople-registry.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Epeople registry', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/access-control/epeople');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Epeople registry page must first be visible
+ cy.get('ds-epeople-registry').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-epeople-registry');
+ });
+});
diff --git a/cypress/e2e/feedback.cy.ts b/cypress/e2e/feedback.cy.ts
new file mode 100644
index 00000000000..75fe1097c63
--- /dev/null
+++ b/cypress/e2e/feedback.cy.ts
@@ -0,0 +1,13 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Feedback', () => {
+ it('should pass accessibility tests', () => {
+ cy.visit('/info/feedback');
+
+ // Page must first be visible
+ cy.get('ds-feedback').should('be.visible');
+
+ // Analyze for accessibility
+ testA11y('ds-feedback');
+ });
+});
diff --git a/cypress/e2e/groups-registry.cy.ts b/cypress/e2e/groups-registry.cy.ts
new file mode 100644
index 00000000000..5c0099c2f1f
--- /dev/null
+++ b/cypress/e2e/groups-registry.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Groups registry', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/access-control/groups');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Epeople registry page must first be visible
+ cy.get('ds-groups-registry').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-groups-registry');
+ });
+});
diff --git a/cypress/e2e/header.cy.ts b/cypress/e2e/header.cy.ts
index 043d67dd2b9..aa65aee570e 100644
--- a/cypress/e2e/header.cy.ts
+++ b/cypress/e2e/header.cy.ts
@@ -10,4 +10,29 @@ describe('Header', () => {
// Analyze for accessibility
testA11y('ds-header');
});
+
+ it('should allow for changing language to German (for example)', () => {
+ cy.visit('/');
+
+ // Click the language switcher (globe) in header
+ cy.get('button[data-test="lang-switch"]').click();
+ // Click on the "Deusch" language in dropdown
+ cy.get('#language-menu-list div[role="option"]').contains('Deutsch').click();
+
+ // HTML "lang" attribute should switch to "de"
+ cy.get('html').invoke('attr', 'lang').should('eq', 'de');
+
+ // Login menu should now be in German
+ cy.get('[data-test="login-menu"]').contains('Anmelden');
+
+ // Change back to English from language switcher
+ cy.get('button[data-test="lang-switch"]').click();
+ cy.get('#language-menu-list div[role="option"]').contains('English').click();
+
+ // HTML "lang" attribute should switch to "en"
+ cy.get('html').invoke('attr', 'lang').should('eq', 'en');
+
+ // Login menu should now be in English
+ cy.get('[data-test="login-menu"]').contains('Log In');
+ });
});
diff --git a/cypress/e2e/health-page.cy.ts b/cypress/e2e/health-page.cy.ts
new file mode 100644
index 00000000000..c702fa72d79
--- /dev/null
+++ b/cypress/e2e/health-page.cy.ts
@@ -0,0 +1,62 @@
+import { testA11y } from 'cypress/support/utils';
+import { Options } from 'cypress-axe';
+
+
+beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/health');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+});
+
+describe('Health Page > Status Tab', () => {
+ it('should pass accessibility tests', () => {
+ cy.intercept('GET', '/server/actuator/health').as('status');
+ cy.wait('@status');
+
+ cy.get('a[data-test="health-page.status-tab"]').click();
+ // Page must first be visible
+ cy.get('ds-health-page').should('be.visible');
+ cy.get('ds-health-panel').should('be.visible');
+
+ // wait for all the ds-health-info-component components to be rendered
+ cy.get('div[role="tabpanel"]').each(($panel: HTMLDivElement) => {
+ cy.wrap($panel).find('ds-health-component').should('be.visible');
+ });
+ // Analyze for accessibility issues
+ testA11y('ds-health-page', {
+ rules: {
+ // All panels are accordians & fail "aria-required-children" and "nested-interactive".
+ // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
+ 'aria-required-children': { enabled: false },
+ 'nested-interactive': { enabled: false },
+ },
+ } as Options);
+ });
+});
+
+describe('Health Page > Info Tab', () => {
+ it('should pass accessibility tests', () => {
+ cy.intercept('GET', '/server/actuator/info').as('info');
+ cy.wait('@info');
+
+ cy.get('a[data-test="health-page.info-tab"]').click();
+ // Page must first be visible
+ cy.get('ds-health-page').should('be.visible');
+ cy.get('ds-health-info').should('be.visible');
+
+ // wait for all the ds-health-info-component components to be rendered
+ cy.get('div[role="tabpanel"]').each(($panel: HTMLDivElement) => {
+ cy.wrap($panel).find('ds-health-info-component').should('be.visible');
+ });
+
+ // Analyze for accessibility issues
+ testA11y('ds-health-info', {
+ rules: {
+ // All panels are accordions & fail "aria-required-children" and "nested-interactive".
+ // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
+ 'aria-required-children': { enabled: false },
+ 'nested-interactive': { enabled: false },
+ },
+ } as Options);
+ });
+});
diff --git a/cypress/e2e/item-edit.cy.ts b/cypress/e2e/item-edit.cy.ts
index 60e1b924eed..ad5d8ea0930 100644
--- a/cypress/e2e/item-edit.cy.ts
+++ b/cypress/e2e/item-edit.cy.ts
@@ -13,8 +13,13 @@ beforeEach(() => {
describe('Edit Item > Edit Metadata tab', () => {
it('should pass accessibility tests', () => {
+ cy.get('a[data-test="metadata"]').should('be.visible');
cy.get('a[data-test="metadata"]').click();
+ // Our selected tab should be both visible & active
+ cy.get('a[data-test="metadata"]').should('be.visible');
+ cy.get('a[data-test="metadata"]').should('have.class', 'active');
+
// tag must be loaded
cy.get('ds-edit-item-page').should('be.visible');
@@ -31,8 +36,13 @@ describe('Edit Item > Edit Metadata tab', () => {
describe('Edit Item > Status tab', () => {
it('should pass accessibility tests', () => {
+ cy.get('a[data-test="status"]').should('be.visible');
cy.get('a[data-test="status"]').click();
+ // Our selected tab should be both visible & active
+ cy.get('a[data-test="status"]').should('be.visible');
+ cy.get('a[data-test="status"]').should('have.class', 'active');
+
// tag must be loaded
cy.get('ds-item-status').should('be.visible');
@@ -44,8 +54,13 @@ describe('Edit Item > Status tab', () => {
describe('Edit Item > Bitstreams tab', () => {
it('should pass accessibility tests', () => {
+ cy.get('a[data-test="bitstreams"]').should('be.visible');
cy.get('a[data-test="bitstreams"]').click();
+ // Our selected tab should be both visible & active
+ cy.get('a[data-test="bitstreams"]').should('be.visible');
+ cy.get('a[data-test="bitstreams"]').should('have.class', 'active');
+
// tag must be loaded
cy.get('ds-item-bitstreams').should('be.visible');
@@ -68,8 +83,13 @@ describe('Edit Item > Bitstreams tab', () => {
describe('Edit Item > Curate tab', () => {
it('should pass accessibility tests', () => {
+ cy.get('a[data-test="curate"]').should('be.visible');
cy.get('a[data-test="curate"]').click();
+ // Our selected tab should be both visible & active
+ cy.get('a[data-test="curate"]').should('be.visible');
+ cy.get('a[data-test="curate"]').should('have.class', 'active');
+
// tag must be loaded
cy.get('ds-item-curate').should('be.visible');
@@ -81,8 +101,13 @@ describe('Edit Item > Curate tab', () => {
describe('Edit Item > Relationships tab', () => {
it('should pass accessibility tests', () => {
+ cy.get('a[data-test="relationships"]').should('be.visible');
cy.get('a[data-test="relationships"]').click();
+ // Our selected tab should be both visible & active
+ cy.get('a[data-test="relationships"]').should('be.visible');
+ cy.get('a[data-test="relationships"]').should('have.class', 'active');
+
// tag must be loaded
cy.get('ds-item-relationships').should('be.visible');
@@ -94,8 +119,13 @@ describe('Edit Item > Relationships tab', () => {
describe('Edit Item > Version History tab', () => {
it('should pass accessibility tests', () => {
+ cy.get('a[data-test="versionhistory"]').should('be.visible');
cy.get('a[data-test="versionhistory"]').click();
+ // Our selected tab should be both visible & active
+ cy.get('a[data-test="versionhistory"]').should('be.visible');
+ cy.get('a[data-test="versionhistory"]').should('have.class', 'active');
+
// tag must be loaded
cy.get('ds-item-version-history').should('be.visible');
@@ -107,8 +137,13 @@ describe('Edit Item > Version History tab', () => {
describe('Edit Item > Access Control tab', () => {
it('should pass accessibility tests', () => {
+ cy.get('a[data-test="access-control"]').should('be.visible');
cy.get('a[data-test="access-control"]').click();
+ // Our selected tab should be both visible & active
+ cy.get('a[data-test="access-control"]').should('be.visible');
+ cy.get('a[data-test="access-control"]').should('have.class', 'active');
+
// tag must be loaded
cy.get('ds-item-access-control').should('be.visible');
@@ -120,8 +155,13 @@ describe('Edit Item > Access Control tab', () => {
describe('Edit Item > Collection Mapper tab', () => {
it('should pass accessibility tests', () => {
+ cy.get('a[data-test="mapper"]').should('be.visible');
cy.get('a[data-test="mapper"]').click();
+ // Our selected tab should be both visible & active
+ cy.get('a[data-test="mapper"]').should('be.visible');
+ cy.get('a[data-test="mapper"]').should('have.class', 'active');
+
// tag must be loaded
cy.get('ds-item-collection-mapper').should('be.visible');
diff --git a/cypress/e2e/login-modal.cy.ts b/cypress/e2e/login-modal.cy.ts
index 3d978dfaca2..1d72306accd 100644
--- a/cypress/e2e/login-modal.cy.ts
+++ b/cypress/e2e/login-modal.cy.ts
@@ -3,31 +3,31 @@ import { testA11y } from 'cypress/support/utils';
const page = {
openLoginMenu() {
// Click the "Log In" dropdown menu in header
- cy.get('ds-header [data-test="login-menu"]').click();
+ cy.get('[data-test="login-menu"]').click();
},
openUserMenu() {
// Once logged in, click the User menu in header
- cy.get('ds-header [data-test="user-menu"]').click();
+ cy.get('[data-test="user-menu"]').click();
},
submitLoginAndPasswordByPressingButton(email, password) {
// Enter email
- cy.get('ds-header [data-test="email"]').type(email);
+ cy.get('[data-test="email"]').type(email);
// Enter password
- cy.get('ds-header [data-test="password"]').type(password);
+ cy.get('[data-test="password"]').type(password);
// Click login button
- cy.get('ds-header [data-test="login-button"]').click();
+ cy.get('[data-test="login-button"]').click();
},
submitLoginAndPasswordByPressingEnter(email, password) {
// In opened Login modal, fill out email & password, then click Enter
- cy.get('ds-header [data-test="email"]').type(email);
- cy.get('ds-header [data-test="password"]').type(password);
- cy.get('ds-header [data-test="password"]').type('{enter}');
+ cy.get('[data-test="email"]').type(email);
+ cy.get('[data-test="password"]').type(password);
+ cy.get('[data-test="password"]').type('{enter}');
},
submitLogoutByPressingButton() {
// This is the POST command that will actually log us out
cy.intercept('POST', '/server/api/authn/logout').as('logout');
// Click logout button
- cy.get('ds-header [data-test="logout-button"]').click();
+ cy.get('[data-test="logout-button"]').click();
// Wait until above POST command responds before continuing
// (This ensures next action waits until logout completes)
cy.wait('@logout');
@@ -67,7 +67,7 @@ describe('Login Modal', () => {
// Login, and the tag should no longer exist
page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
- cy.get('.form-login').should('not.exist');
+ cy.get('ds-log-in').should('not.exist');
// Verify we are still on homepage
cy.url().should('include', '/home');
diff --git a/cypress/e2e/metadata-import-page.cy.ts b/cypress/e2e/metadata-import-page.cy.ts
new file mode 100644
index 00000000000..a31c18e4ebb
--- /dev/null
+++ b/cypress/e2e/metadata-import-page.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Metadata Import Page', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/admin/metadata-import');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Metadata import form must first be visible
+ cy.get('ds-metadata-import-page').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-metadata-import-page');
+ });
+});
diff --git a/cypress/e2e/metadata-registry.cy.ts b/cypress/e2e/metadata-registry.cy.ts
new file mode 100644
index 00000000000..0402d33153e
--- /dev/null
+++ b/cypress/e2e/metadata-registry.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Metadata Registry', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/admin/registries/metadata');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Page must first be visible
+ cy.get('ds-metadata-registry').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-metadata-registry');
+ });
+});
diff --git a/cypress/e2e/metadata-schema.cy.ts b/cypress/e2e/metadata-schema.cy.ts
new file mode 100644
index 00000000000..9ff0db0714b
--- /dev/null
+++ b/cypress/e2e/metadata-schema.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Metadata Schema', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/admin/registries/metadata/dc');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Page must first be visible
+ cy.get('ds-metadata-schema').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-metadata-schema');
+ });
+});
diff --git a/cypress/e2e/new-process.cy.ts b/cypress/e2e/new-process.cy.ts
new file mode 100644
index 00000000000..d26da7cc4df
--- /dev/null
+++ b/cypress/e2e/new-process.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('New Process', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/processes/new');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Process form must first be visible
+ cy.get('ds-new-process').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-new-process');
+ });
+});
diff --git a/cypress/e2e/privacy.cy.ts b/cypress/e2e/privacy.cy.ts
new file mode 100644
index 00000000000..16e049f701e
--- /dev/null
+++ b/cypress/e2e/privacy.cy.ts
@@ -0,0 +1,13 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Privacy', () => {
+ it('should pass accessibility tests', () => {
+ cy.visit('/info/privacy');
+
+ // Page must first be visible
+ cy.get('ds-privacy').should('be.visible');
+
+ // Analyze for accessibility
+ testA11y('ds-privacy');
+ });
+});
diff --git a/cypress/e2e/processes-overview.cy.ts b/cypress/e2e/processes-overview.cy.ts
new file mode 100644
index 00000000000..2be3bd4c181
--- /dev/null
+++ b/cypress/e2e/processes-overview.cy.ts
@@ -0,0 +1,17 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Processes Overview', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/processes');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+
+ // Process overview must first be visible
+ cy.get('ds-process-overview').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-process-overview');
+ });
+});
diff --git a/cypress/e2e/profile-page.cy.ts b/cypress/e2e/profile-page.cy.ts
new file mode 100644
index 00000000000..911ef33ba58
--- /dev/null
+++ b/cypress/e2e/profile-page.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Profile page', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/profile');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Process form must first be visible
+ cy.get('ds-profile-page').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-profile-page');
+ });
+});
diff --git a/cypress/e2e/quality-assurance-source-page.cy.ts b/cypress/e2e/quality-assurance-source-page.cy.ts
new file mode 100644
index 00000000000..722917ef16b
--- /dev/null
+++ b/cypress/e2e/quality-assurance-source-page.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('Quality Assurance Source Page', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/notifications/quality-assurance');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Source page must first be visible
+ cy.get('ds-quality-assurance-source-page-component').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-quality-assurance-source-page-component');
+ });
+});
diff --git a/cypress/e2e/system-wide-alert.cy.ts b/cypress/e2e/system-wide-alert.cy.ts
new file mode 100644
index 00000000000..046bfe619fe
--- /dev/null
+++ b/cypress/e2e/system-wide-alert.cy.ts
@@ -0,0 +1,16 @@
+import { testA11y } from 'cypress/support/utils';
+
+describe('System Wide Alert', () => {
+ beforeEach(() => {
+ // Must login as an Admin to see the page
+ cy.visit('/admin/system-wide-alert');
+ cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
+ });
+
+ it('should pass accessibility tests', () => {
+ // Page must first be visible
+ cy.get('ds-system-wide-alert-form').should('be.visible');
+ // Analyze for accessibility issues
+ testA11y('ds-system-wide-alert-form');
+ });
+});
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index b3e3b9630bb..8cc2c5c721b 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -101,11 +101,11 @@ Cypress.Commands.add('login', login);
*/
function loginViaForm(email: string, password: string): void {
// Enter email
- cy.get('ds-log-in [data-test="email"]').type(email);
+ cy.get('[data-test="email"]').type(email);
// Enter password
- cy.get('ds-log-in [data-test="password"]').type(password);
+ cy.get('[data-test="password"]').type(password);
// Click login button
- cy.get('ds-log-in [data-test="login-button"]').click();
+ cy.get('[data-test="login-button"]').click();
}
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
Cypress.Commands.add('loginViaForm', loginViaForm);
diff --git a/docker/README.md b/docker/README.md
index 3dc5fd50550..6360124b601 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -23,14 +23,14 @@ the Docker compose scripts in this 'docker' folder.
This Dockerfile is used to build a *development* DSpace Angular UI image, published as 'dspace/dspace-angular'
```
-docker build -t dspace/dspace-angular:latest .
+docker build -t dspace/dspace-angular:dspace-8_x .
```
This image is built *automatically* after each commit is made to the `main` branch.
Admins to our DockerHub repo can manually publish with the following command.
```
-docker push dspace/dspace-angular:latest
+docker push dspace/dspace-angular:dspace-8_x
```
### Dockerfile.dist
@@ -39,7 +39,7 @@ The `Dockerfile.dist` is used to generate a *production* build and runtime envir
```bash
# build the latest image
-docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist .
+docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-8_x-dist .
```
A default/demo version of this image is built *automatically*.
diff --git a/docker/cli.yml b/docker/cli.yml
index 9b1973426f3..7c17b14b1bd 100644
--- a/docker/cli.yml
+++ b/docker/cli.yml
@@ -14,14 +14,14 @@
# Therefore, it should be kept in sync with that file
networks:
# Default to using network named 'dspacenet' from docker-compose-rest.yml.
- # Its full name will be prepended with the project name (e.g. "-p d7" means it will be named "d7_dspacenet")
+ # Its full name will be prepended with the project name (e.g. "-p d8" means it will be named "d8_dspacenet")
# If COMPOSITE_PROJECT_NAME is missing, default value will be "docker" (name of folder this file is in)
default:
name: ${COMPOSE_PROJECT_NAME:-docker}_dspacenet
external: true
services:
dspace-cli:
- image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}"
+ image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-8_x}"
container_name: dspace-cli
environment:
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
diff --git a/docker/db.entities.yml b/docker/db.entities.yml
index b3cf5bd86f4..464253f07be 100644
--- a/docker/db.entities.yml
+++ b/docker/db.entities.yml
@@ -14,10 +14,11 @@
# # Therefore, it should be kept in sync with that file
services:
dspacedb:
- image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql"
+ image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}-loadsql"
environment:
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
+ # NOTE: currently there is no dspace8 version
- LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
dspace:
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml
index c5c419a4a74..d2589bb3f32 100644
--- a/docker/docker-compose-ci.yml
+++ b/docker/docker-compose-ci.yml
@@ -33,7 +33,7 @@ services:
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
solr__D__statistics__P__autoCommit: 'false'
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
- image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
+ image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-8_x-test}"
depends_on:
- dspacedb
networks:
@@ -60,11 +60,12 @@ services:
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
dspacedb:
container_name: dspacedb
- image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql"
+ image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}-loadsql"
environment:
# This LOADSQL should be kept in sync with the LOADSQL in
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
+ # NOTE: currently there is no dspace8 version
LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
PGDATA: /pgdata
POSTGRES_PASSWORD: dspace
@@ -81,7 +82,7 @@ services:
# DSpace Solr container
dspacesolr:
container_name: dspacesolr
- image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
+ image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-8_x}"
networks:
- dspacenet
ports:
diff --git a/docker/docker-compose-dist.yml b/docker/docker-compose-dist.yml
index 67eba167852..03e5e9da709 100644
--- a/docker/docker-compose-dist.yml
+++ b/docker/docker-compose-dist.yml
@@ -26,7 +26,7 @@ services:
DSPACE_REST_HOST: sandbox.dspace.org
DSPACE_REST_PORT: 443
DSPACE_REST_NAMESPACE: /server
- image: dspace/dspace-angular:${DSPACE_VER:-latest}-dist
+ image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-8_x}-dist"
build:
context: ..
dockerfile: Dockerfile.dist
diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml
index 09dfcf2a5f7..37a5d23e77b 100644
--- a/docker/docker-compose-rest.yml
+++ b/docker/docker-compose-rest.yml
@@ -40,7 +40,7 @@ services:
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
proxies__P__trusted__P__ipranges: '172.23.0'
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
- image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
+ image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-8_x-test}"
depends_on:
- dspacedb
networks:
@@ -68,7 +68,7 @@ services:
dspacedb:
container_name: dspacedb
# Uses a custom Postgres image with pgcrypto installed
- image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
+ image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}"
environment:
PGDATA: /pgdata
POSTGRES_PASSWORD: dspace
@@ -85,7 +85,7 @@ services:
# DSpace Solr container
dspacesolr:
container_name: dspacesolr
- image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
+ image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-8_x}"
networks:
- dspacenet
ports:
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 1c268b84b7b..8e85520f9fa 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -23,7 +23,7 @@ services:
DSPACE_REST_HOST: localhost
DSPACE_REST_PORT: 8080
DSPACE_REST_NAMESPACE: /server
- image: dspace/dspace-angular:${DSPACE_VER:-latest}
+ image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-8_x}"
build:
context: ..
dockerfile: Dockerfile
diff --git a/docs/lint/html/index.md b/docs/lint/html/index.md
index 15d693843c0..e134e1070f4 100644
--- a/docs/lint/html/index.md
+++ b/docs/lint/html/index.md
@@ -2,3 +2,4 @@
_______
- [`dspace-angular-html/themed-component-usages`](./rules/themed-component-usages.md): Themeable components should be used via the selector of their `ThemedComponent` wrapper class
+- [`dspace-angular-html/no-disabled-attribute-on-button`](./rules/no-disabled-attribute-on-button.md): Buttons should use the `dsBtnDisabled` directive instead of the HTML `disabled` attribute.
diff --git a/docs/lint/html/rules/no-disabled-attribute-on-button.md b/docs/lint/html/rules/no-disabled-attribute-on-button.md
new file mode 100644
index 00000000000..d9d39ce82ca
--- /dev/null
+++ b/docs/lint/html/rules/no-disabled-attribute-on-button.md
@@ -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
+
+```
+
+##### disabled attribute is still valid on non-button elements
+
+```html
+
+```
+
+##### [disabled] attribute is still valid on non-button elements
+
+```html
+
+```
+
+##### angular dynamic attributes that use disabled are still valid
+
+```html
+
+```
+
+
+
+
+#### Invalid code & automatic fixes
+
+##### should not use disabled attribute in HTML templates
+
+```html
+
+```
+Will produce the following error(s):
+```
+Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
+```
+
+Result of `yarn lint --fix`:
+```html
+
+```
+
+
+##### should not use [disabled] attribute in HTML templates
+
+```html
+
+```
+Will produce the following error(s):
+```
+Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
+```
+
+Result of `yarn lint --fix`:
+```html
+
+```
+
+
+
diff --git a/lint/src/rules/html/index.ts b/lint/src/rules/html/index.ts
index 7c1370ae2d4..3d425c3ad48 100644
--- a/lint/src/rules/html/index.ts
+++ b/lint/src/rules/html/index.ts
@@ -10,10 +10,13 @@ import {
bundle,
RuleExports,
} from '../../util/structure';
+import * as noDisabledAttributeOnButton from './no-disabled-attribute-on-button';
import * as themedComponentUsages from './themed-component-usages';
const index = [
themedComponentUsages,
+ noDisabledAttributeOnButton,
+
] as unknown as RuleExports[];
export = {
diff --git a/lint/src/rules/html/no-disabled-attribute-on-button.ts b/lint/src/rules/html/no-disabled-attribute-on-button.ts
new file mode 100644
index 00000000000..bf1a72d70d0
--- /dev/null
+++ b/lint/src/rules/html/no-disabled-attribute-on-button.ts
@@ -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) {
+ 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: `
+
+ `,
+ },
+ {
+ name: 'disabled attribute is still valid on non-button elements',
+ code: `
+
+ `,
+ },
+ {
+ name: '[disabled] attribute is still valid on non-button elements',
+ code: `
+
+ `,
+ },
+ {
+ name: 'angular dynamic attributes that use disabled are still valid',
+ code: `
+
+ `,
+ },
+ ],
+ invalid: [
+ {
+ name: 'should not use disabled attribute in HTML templates',
+ code: `
+
+ `,
+ errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
+ output: `
+
+ `,
+ },
+ {
+ name: 'should not use [disabled] attribute in HTML templates',
+ code: `
+
+ `,
+ errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
+ output: `
+
+ `,
+ },
+ ],
+} as NamedTests;
+
+export default rule;
diff --git a/lint/src/rules/html/themed-component-usages.ts b/lint/src/rules/html/themed-component-usages.ts
index 0b9a13456a6..e907285dbca 100644
--- a/lint/src/rules/html/themed-component-usages.ts
+++ b/lint/src/rules/html/themed-component-usages.ts
@@ -7,10 +7,8 @@
*/
import { TmplAstElement } from '@angular-eslint/bundled-angular-compiler';
import { TemplateParserServices } from '@angular-eslint/utils';
-import {
- ESLintUtils,
- TSESLint,
-} from '@typescript-eslint/utils';
+import { ESLintUtils } from '@typescript-eslint/utils';
+import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
import { fixture } from '../../../test/fixture';
import {
@@ -52,7 +50,7 @@ The only exception to this rule are unit tests, where we may want to use the bas
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
- create(context: TSESLint.RuleContext) {
+ create(context: RuleContext) {
if (getFilename(context).includes('.spec.ts')) {
// skip inline templates in unit tests
return {};
diff --git a/lint/src/rules/ts/themed-component-classes.ts b/lint/src/rules/ts/themed-component-classes.ts
index 66c37395b4c..527655adfa4 100644
--- a/lint/src/rules/ts/themed-component-classes.ts
+++ b/lint/src/rules/ts/themed-component-classes.ts
@@ -7,9 +7,9 @@
*/
import {
ESLintUtils,
- TSESLint,
TSESTree,
} from '@typescript-eslint/utils';
+import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
import { fixture } from '../../../test/fixture';
import {
@@ -57,7 +57,7 @@ export const info = {
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
- create(context: TSESLint.RuleContext) {
+ create(context: RuleContext) {
const filename = getFilename(context);
if (filename.endsWith('.spec.ts')) {
diff --git a/lint/src/rules/ts/themed-component-selectors.ts b/lint/src/rules/ts/themed-component-selectors.ts
index e06f5ababf6..c27fd66d662 100644
--- a/lint/src/rules/ts/themed-component-selectors.ts
+++ b/lint/src/rules/ts/themed-component-selectors.ts
@@ -7,9 +7,9 @@
*/
import {
ESLintUtils,
- TSESLint,
TSESTree,
} from '@typescript-eslint/utils';
+import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
import { fixture } from '../../../test/fixture';
import { getComponentSelectorNode } from '../../util/angular';
@@ -58,7 +58,7 @@ Unit tests are exempt from this rule, because they may redefine components using
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
- create(context: TSESLint.RuleContext) {
+ create(context: RuleContext) {
const filename = getFilename(context);
if (filename.endsWith('.spec.ts')) {
diff --git a/lint/src/rules/ts/themed-component-usages.ts b/lint/src/rules/ts/themed-component-usages.ts
index 96e9962ccf7..83fe6f8ea89 100644
--- a/lint/src/rules/ts/themed-component-usages.ts
+++ b/lint/src/rules/ts/themed-component-usages.ts
@@ -7,9 +7,9 @@
*/
import {
ESLintUtils,
- TSESLint,
TSESTree,
} from '@typescript-eslint/utils';
+import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
import { fixture } from '../../../test/fixture';
import {
@@ -68,7 +68,7 @@ There are a few exceptions where the base class can still be used:
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
- create(context: TSESLint.RuleContext) {
+ create(context: RuleContext) {
const filename = getFilename(context);
function handleUnthemedUsagesInTypescript(node: TSESTree.Identifier) {
diff --git a/lint/src/util/structure.ts b/lint/src/util/structure.ts
index bfbf7ec7f27..2e3aebd9ab4 100644
--- a/lint/src/util/structure.ts
+++ b/lint/src/util/structure.ts
@@ -5,13 +5,17 @@
*
* http://www.dspace.org/license/
*/
-import { TSESLint } from '@typescript-eslint/utils';
-import { RuleTester } from 'eslint';
+import {
+ InvalidTestCase,
+ RuleMetaData,
+ RuleModule,
+ ValidTestCase,
+} from '@typescript-eslint/utils/ts-eslint';
import { EnumType } from 'typescript';
-export type Meta = TSESLint.RuleMetaData;
-export type Valid = TSESLint.ValidTestCase | RuleTester.ValidTestCase;
-export type Invalid = TSESLint.InvalidTestCase | RuleTester.InvalidTestCase;
+export type Meta = RuleMetaData;
+export type Valid = ValidTestCase;
+export type Invalid = InvalidTestCase;
export interface DSpaceESLintRuleInfo {
name: string;
@@ -28,7 +32,7 @@ export interface NamedTests {
export interface RuleExports {
Message: EnumType,
info: DSpaceESLintRuleInfo,
- rule: TSESLint.RuleModule,
+ rule: RuleModule,
tests: NamedTests,
default: unknown,
}
diff --git a/lint/src/util/typescript.ts b/lint/src/util/typescript.ts
index 3fecad270e6..0d04ef1a3d9 100644
--- a/lint/src/util/typescript.ts
+++ b/lint/src/util/typescript.ts
@@ -5,17 +5,18 @@
*
* http://www.dspace.org/license/
*/
+import { TSESTree } from '@typescript-eslint/utils';
import {
- TSESLint,
- TSESTree,
-} from '@typescript-eslint/utils';
+ RuleContext,
+ SourceCode,
+} from '@typescript-eslint/utils/ts-eslint';
import {
match,
toUnixStylePath,
} from './misc';
-export type AnyRuleContext = TSESLint.RuleContext;
+export type AnyRuleContext = RuleContext;
/**
* Return the current filename based on the ESLint rule context as a Unix-style path.
@@ -27,7 +28,7 @@ export function getFilename(context: AnyRuleContext): string {
return toUnixStylePath(context.getFilename());
}
-export function getSourceCode(context: AnyRuleContext): TSESLint.SourceCode {
+export function getSourceCode(context: AnyRuleContext): SourceCode {
// TSESLint claims this is deprecated, but the suggested alternative is undefined (could be a version mismatch between ESLint and TSESlint?)
// eslint-disable-next-line deprecation/deprecation
return context.getSourceCode();
diff --git a/lint/test/testing.ts b/lint/test/testing.ts
index cfa54c5b85c..53faf320699 100644
--- a/lint/test/testing.ts
+++ b/lint/test/testing.ts
@@ -7,7 +7,7 @@
*/
import { RuleTester as TypeScriptRuleTester } from '@typescript-eslint/rule-tester';
-import { RuleTester } from 'eslint';
+import { RuleTester } from '@typescript-eslint/utils/ts-eslint';
import { themeableComponents } from '../src/util/theme-support';
import {
diff --git a/package.json b/package.json
index ada05251493..d166842f73f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "dspace-angular",
- "version": "8.1.0-next",
+ "version": "8.2.0",
"scripts": {
"ng": "ng",
"config:watch": "nodemon",
@@ -12,7 +12,6 @@
"preserve": "yarn base-href",
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
"serve:ssr": "node dist/server/main",
- "analyze": "webpack-bundle-analyzer dist/browser/stats.json",
"build": "ng build --configuration development",
"build:stats": "ng build --stats-json",
"build:prod": "cross-env NODE_ENV=production yarn run build:ssr",
@@ -55,11 +54,6 @@
"https": false
},
"private": true,
- "resolutions": {
- "minimist": "^1.2.5",
- "webdriver-manager": "^12.1.8",
- "ts-node": "10.2.1"
- },
"dependencies": {
"@angular/animations": "^17.3.11",
"@angular/cdk": "^17.3.10",
@@ -67,16 +61,14 @@
"@angular/compiler": "^17.3.11",
"@angular/core": "^17.3.11",
"@angular/forms": "^17.3.11",
- "@angular/localize": "17.3.11",
+ "@angular/localize": "17.3.12",
"@angular/platform-browser": "^17.3.11",
"@angular/platform-browser-dynamic": "^17.3.11",
"@angular/platform-server": "^17.3.11",
"@angular/router": "^17.3.11",
- "@angular/ssr": "^17.3.8",
- "@babel/runtime": "7.21.0",
+ "@angular/ssr": "^17.3.17",
+ "@babel/runtime": "7.27.6",
"@kolkov/ngx-gallery": "^2.0.1",
- "@material-ui/core": "^4.11.0",
- "@material-ui/icons": "^4.11.3",
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@ng-dynamic-forms/core": "^16.0.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
@@ -85,108 +77,103 @@
"@ngrx/store": "^17.1.1",
"@ngx-translate/core": "^14.0.0",
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
- "@types/grecaptcha": "^3.0.4",
- "angular-idle-preload": "3.0.0",
"angulartics2": "^12.2.0",
- "axios": "^1.6.0",
+ "axios": "^1.10.0",
"bootstrap": "^4.6.1",
"cerialize": "0.1.18",
"cli-progress": "^3.12.0",
"colors": "^1.4.0",
- "compression": "^1.7.4",
- "cookie-parser": "1.4.6",
- "core-js": "^3.30.1",
+ "compression": "^1.8.0",
+ "cookie-parser": "1.4.7",
+ "core-js": "^3.42.0",
"date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7",
"deepmerge": "^4.3.1",
"ejs": "^3.1.10",
- "express": "^4.19.2",
+ "express": "^4.21.2",
"express-rate-limit": "^5.1.3",
"fast-json-patch": "^3.1.1",
"filesize": "^6.1.0",
- "http-proxy-middleware": "^1.0.5",
+ "http-proxy-middleware": "^2.0.9",
"http-terminator": "^3.2.0",
- "isbot": "^3.6.10",
+ "isbot": "^5.1.28",
"js-cookie": "2.2.1",
"js-yaml": "^4.1.0",
"json5": "^2.2.3",
- "jsonschema": "1.4.1",
+ "jsonschema": "1.5.0",
"jwt-decode": "^3.1.2",
"klaro": "^0.7.18",
"lodash": "^4.17.21",
"lru-cache": "^7.14.1",
"markdown-it": "^13.0.1",
- "mirador": "^3.3.0",
+ "mirador": "^3.4.3",
"mirador-dl-plugin": "^0.13.0",
- "mirador-share-plugin": "^0.11.0",
+ "mirador-share-plugin": "^0.16.0",
"morgan": "^1.10.0",
- "ng-mocks": "^14.10.0",
"ng2-file-upload": "5.0.0",
"ng2-nouislider": "^2.0.0",
"ngx-infinite-scroll": "^16.0.0",
"ngx-pagination": "6.0.3",
+ "ngx-skeleton-loader": "^9.0.0",
"ngx-ui-switch": "^14.1.0",
"nouislider": "^15.7.1",
- "pem": "1.14.7",
- "prop-types": "^15.8.1",
- "react-copy-to-clipboard": "^5.1.0",
- "reflect-metadata": "^0.1.13",
- "rxjs": "^7.8.0",
- "sanitize-html": "^2.12.1",
- "sortablejs": "1.15.0",
+ "pem": "1.14.8",
+ "reflect-metadata": "^0.2.2",
+ "rxjs": "^7.8.2",
"uuid": "^8.3.2",
- "webfontloader": "1.6.28",
- "zone.js": "~0.14.4"
+ "zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-builders/custom-webpack": "~17.0.2",
- "@angular-devkit/build-angular": "^17.3.8",
- "@angular-eslint/builder": "17.2.1",
- "@angular-eslint/bundled-angular-compiler": "17.2.1",
- "@angular-eslint/eslint-plugin": "17.2.1",
- "@angular-eslint/eslint-plugin-template": "17.2.1",
- "@angular-eslint/schematics": "17.2.1",
- "@angular-eslint/template-parser": "17.2.1",
- "@angular/cli": "^17.3.8",
+ "@angular-devkit/build-angular": "^17.3.17",
+ "@angular-eslint/builder": "17.5.3",
+ "@angular-eslint/bundled-angular-compiler": "17.5.3",
+ "@angular-eslint/eslint-plugin": "17.5.3",
+ "@angular-eslint/eslint-plugin-template": "17.5.3",
+ "@angular-eslint/schematics": "17.5.3",
+ "@angular-eslint/template-parser": "17.5.3",
+ "@angular/cli": "^17.3.17",
"@angular/compiler-cli": "^17.3.11",
"@angular/language-service": "^17.3.11",
"@cypress/schematic": "^1.5.0",
- "@fortawesome/fontawesome-free": "^6.4.0",
+ "@fortawesome/fontawesome-free": "^6.7.2",
+ "@material-ui/core": "^4.12.4",
+ "@material-ui/icons": "^4.11.3",
"@ngrx/store-devtools": "^17.1.1",
- "@ngtools/webpack": "^16.2.12",
- "@types/deep-freeze": "0.1.2",
+ "@ngtools/webpack": "^16.2.16",
+ "@types/deep-freeze": "0.1.5",
"@types/ejs": "^3.1.2",
"@types/express": "^4.17.17",
+ "@types/grecaptcha": "^3.0.9",
"@types/jasmine": "~3.6.0",
"@types/js-cookie": "2.2.6",
- "@types/lodash": "^4.14.194",
+ "@types/lodash": "^4.17.17",
"@types/node": "^14.14.9",
- "@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@typescript-eslint/rule-tester": "^7.2.0",
"@typescript-eslint/utils": "^7.2.0",
- "axe-core": "^4.7.2",
- "browser-sync": "^3.0.0",
+ "axe-core": "^4.10.3",
"compression-webpack-plugin": "^9.2.0",
"copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3",
- "cypress": "12.17.4",
- "cypress-axe": "^1.4.0",
+ "csstype": "^3.1.3",
+ "cypress": "^13.17.0",
+ "cypress-axe": "^1.6.0",
"deep-freeze": "0.0.1",
"eslint": "^8.39.0",
"eslint-plugin-deprecation": "^1.4.1",
"eslint-plugin-dspace-angular-html": "link:./lint/dist/src/rules/html",
"eslint-plugin-dspace-angular-ts": "link:./lint/dist/src/rules/ts",
- "eslint-plugin-import": "^2.27.5",
+ "eslint-plugin-import": "^2.31.0",
"eslint-plugin-import-newlines": "^1.3.1",
"eslint-plugin-jsdoc": "^45.0.0",
- "eslint-plugin-jsonc": "^2.6.0",
+ "eslint-plugin-jsonc": "^2.20.1",
"eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-rxjs": "^5.0.3",
"eslint-plugin-simple-import-sort": "^10.0.0",
- "eslint-plugin-unused-imports": "^2.0.0",
- "express-static-gzip": "^2.1.7",
+ "eslint-plugin-unused-imports": "^3.2.0",
+ "express-static-gzip": "^2.2.0",
"jasmine": "^3.8.0",
"jasmine-core": "^3.8.0",
"jasmine-marbles": "0.9.2",
@@ -196,25 +183,24 @@
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"karma-mocha-reporter": "2.2.5",
+ "ng-mocks": "^14.13.5",
"ngx-mask": "14.2.4",
"nodemon": "^2.0.22",
- "postcss": "^8.4",
- "postcss-apply": "0.12.0",
+ "postcss": "^8.5",
"postcss-import": "^14.0.0",
"postcss-loader": "^4.0.3",
"postcss-preset-env": "^7.4.2",
- "postcss-responsive-type": "1.0.0",
+ "prop-types": "^15.8.1",
"react": "^16.14.0",
+ "react-copy-to-clipboard": "^5.1.0",
"react-dom": "^16.14.0",
"rimraf": "^3.0.2",
- "rxjs-spy": "^8.0.2",
- "sass": "~1.62.0",
+ "sass": "~1.89.2",
"sass-loader": "^12.6.0",
"sass-resources-loader": "^2.2.5",
"ts-node": "^8.10.2",
- "typescript": "~5.3.3",
- "webpack": "5.90.3",
- "webpack-bundle-analyzer": "^4.8.0",
+ "typescript": "~5.4.5",
+ "webpack": "5.99.9",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
diff --git a/postcss.config.js b/postcss.config.js
index df092d1d39f..f8b9666b312 100644
--- a/postcss.config.js
+++ b/postcss.config.js
@@ -1,8 +1,6 @@
module.exports = {
plugins: [
require('postcss-import')(),
- require('postcss-preset-env')(),
- require('postcss-apply')(),
- require('postcss-responsive-type')()
+ require('postcss-preset-env')()
]
};
diff --git a/server.ts b/server.ts
index 22f34232874..84c07229472 100644
--- a/server.ts
+++ b/server.ts
@@ -27,7 +27,7 @@ import * as expressStaticGzip from 'express-static-gzip';
/* eslint-enable import/no-namespace */
import axios from 'axios';
import LRU from 'lru-cache';
-import isbot from 'isbot';
+import { isbot } from 'isbot';
import { createCertificate } from 'pem';
import { createServer } from 'https';
import { json } from 'body-parser';
@@ -58,6 +58,7 @@ import {
REQUEST,
RESPONSE,
} from './src/express.tokens';
+import { SsrExcludePatterns } from "./src/config/ssr-config.interface";
/*
* Set path for the browser application's dist folder
@@ -81,6 +82,9 @@ let anonymousCache: LRU;
// extend environment with app config for server
extendEnvironmentWithAppConfig(environment, appConfig);
+// The REST server base URL
+const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl;
+
// The Express app is exported so that it can be used by serverless Functions.
export function app() {
@@ -156,7 +160,7 @@ export function app() {
* Proxy the sitemaps
*/
router.use('/sitemap**', createProxyMiddleware({
- target: `${environment.rest.baseUrl}/sitemaps`,
+ target: `${REST_BASE_URL}/sitemaps`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true,
}));
@@ -165,7 +169,7 @@ export function app() {
* Proxy the linksets
*/
router.use('/signposting**', createProxyMiddleware({
- target: `${environment.rest.baseUrl}`,
+ target: `${REST_BASE_URL}`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true,
}));
@@ -218,7 +222,7 @@ export function app() {
* The callback function to serve server side angular
*/
function ngApp(req, res, next) {
- if (environment.ssr.enabled) {
+ if (environment.ssr.enabled && req.method === 'GET' && (req.path === '/' || !isExcludedFromSsr(req.path, environment.ssr.excludePathPatterns))) {
// Render the page to user via SSR (server side rendering)
serverSideRender(req, res, next);
} else {
@@ -266,6 +270,11 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) {
})
.then((html) => {
if (hasValue(html)) {
+ // Replace REST URL with UI URL
+ if (environment.ssr.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
+ html = html.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
+ }
+
// save server side rendered page to cache (if any are enabled)
saveToCache(req, html);
if (sendToUser) {
@@ -619,11 +628,26 @@ function start() {
}
}
+/**
+ * Check if SSR should be skipped for path
+ *
+ * @param path
+ * @param excludePathPattern
+ */
+function isExcludedFromSsr(path: string, excludePathPattern: SsrExcludePatterns[]): boolean {
+ const patterns = excludePathPattern.map(p =>
+ new RegExp(p.pattern, p.flag || '')
+ );
+ return patterns.some((regex) => {
+ return regex.test(path)
+ });
+}
+
/*
* The callback function to serve health check requests
*/
function healthCheck(req, res) {
- const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
+ const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
axios.get(baseUrl)
.then((response) => {
res.status(response.status).send(response.data);
diff --git a/src/app/access-control/bulk-access/bulk-access.component.html b/src/app/access-control/bulk-access/bulk-access.component.html
index c164cc5c319..cda6b805bcc 100644
--- a/src/app/access-control/bulk-access/bulk-access.component.html
+++ b/src/app/access-control/bulk-access/bulk-access.component.html
@@ -10,7 +10,7 @@