Merge branch 'main' into use-applied-filter-to-display-label-on-search_contribute-main

# Conflicts:
#	src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.spec.ts
#	src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts
#	src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.ts
#	src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.spec.ts
#	src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.ts
#	src/app/shared/search/search-filters/search-filter/search-filter.component.ts
#	src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts
#	src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts
#	src/app/shared/search/search-filters/search-filters.component.ts
#	src/app/shared/search/search-labels/search-label/search-label.component.spec.ts
#	src/app/shared/search/search-labels/search-label/search-label.component.ts
#	src/app/shared/search/search-labels/search-labels.component.ts
#	src/app/shared/search/search-sidebar/search-sidebar.component.ts
#	src/app/shared/search/search.component.html
#	src/app/shared/search/search.component.ts
#	src/app/shared/search/search.module.ts
This commit is contained in:
Alexandre Vryghem
2024-04-25 01:18:52 +02:00
1970 changed files with 28202 additions and 20921 deletions

View File

@@ -152,7 +152,6 @@
} }
], ],
"@angular-eslint/no-attribute-decorator": "error", "@angular-eslint/no-attribute-decorator": "error",
"@angular-eslint/no-forward-ref": "error",
"@angular-eslint/no-output-native": "warn", "@angular-eslint/no-output-native": "warn",
"@angular-eslint/no-output-on-prefix": "warn", "@angular-eslint/no-output-on-prefix": "warn",
"@angular-eslint/no-conflicting-lifecycle": "warn", "@angular-eslint/no-conflicting-lifecycle": "warn",

View File

@@ -33,12 +33,12 @@ jobs:
#CHROME_VERSION: "90.0.4430.212-1" #CHROME_VERSION: "90.0.4430.212-1"
# Bump Node heap size (OOM in CI after upgrading to Angular 15) # Bump Node heap size (OOM in CI after upgrading to Angular 15)
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'
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:
node-version: [16.x, 18.x] node-version: [18.x, 20.x]
# Do NOT exit immediately if one matrix job fails # Do NOT exit immediately if one matrix job fails
fail-fast: false fail-fast: false
# These are the actual CI steps to perform per job # These are the actual CI steps to perform per job
@@ -74,7 +74,7 @@ jobs:
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Cache Yarn dependencies - name: Cache Yarn dependencies
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
# Cache entire Yarn cache directory (see previous step) # Cache entire Yarn cache directory (see previous step)
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -101,19 +101,19 @@ jobs:
# so that it can be shared with the 'codecov' job (see below) # so that it can be shared with the 'codecov' job (see below)
# NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286 # NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286
- name: Upload code coverage report to Artifact - name: Upload code coverage report to Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: matrix.node-version == '18.x' if: matrix.node-version == '18.x'
with: with:
name: dspace-angular coverage report name: coverage-report-${{ matrix.node-version }}
path: 'coverage/dspace-angular/lcov.info' path: 'coverage/dspace-angular/lcov.info'
retention-days: 14 retention-days: 14
# 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)
run: | run: |
docker-compose -f ./docker/docker-compose-ci.yml up -d docker compose -f ./docker/docker-compose-ci.yml up -d
docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli docker compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
docker container ls docker container ls
# Run integration tests via Cypress.io # Run integration tests via Cypress.io
@@ -135,19 +135,19 @@ jobs:
# Cypress always creates a video of all e2e tests (whether they succeeded or failed) # Cypress always creates a video of all e2e tests (whether they succeeded or failed)
# Save those in an Artifact # Save those in an Artifact
- name: Upload e2e test videos to Artifacts - name: Upload e2e test videos to Artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: e2e-test-videos name: e2e-test-videos-${{ matrix.node-version }}
path: cypress/videos path: cypress/videos
# If e2e tests fail, Cypress creates a screenshot of what happened # If e2e tests fail, Cypress creates a screenshot of what happened
# Save those in an Artifact # Save those in an Artifact
- name: Upload e2e test failure screenshots to Artifacts - name: Upload e2e test failure screenshots to Artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: e2e-test-screenshots name: e2e-test-screenshots-${{ matrix.node-version }}
path: cypress/screenshots path: cypress/screenshots
- name: Stop app (in case it stays up after e2e tests) - name: Stop app (in case it stays up after e2e tests)
@@ -182,7 +182,7 @@ jobs:
run: kill -9 $(lsof -t -i:4000) run: kill -9 $(lsof -t -i:4000)
- name: Shutdown Docker containers - name: Shutdown Docker containers
run: docker-compose -f ./docker/docker-compose-ci.yml down run: docker compose -f ./docker/docker-compose-ci.yml down
# Codecov upload is a separate job in order to allow us to restart this separate from the entire build/test # Codecov upload is a separate job in order to allow us to restart this separate from the entire build/test
# job above. This is necessary because Codecov uploads seem to randomly fail at times. # job above. This is necessary because Codecov uploads seem to randomly fail at times.
@@ -197,7 +197,7 @@ jobs:
# Download artifacts from previous 'tests' job # Download artifacts from previous 'tests' job
- name: Download coverage artifacts - name: Download coverage artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
# Now attempt upload to Codecov using its action. # Now attempt upload to Codecov using its action.
# NOTE: We use a retry action to retry the Codecov upload if it fails the first time. # NOTE: We use a retry action to retry the Codecov upload if it fails the first time.
@@ -207,11 +207,12 @@ jobs:
- name: Upload coverage to Codecov.io - name: Upload coverage to Codecov.io
uses: Wandalen/wretry.action@v1.3.0 uses: Wandalen/wretry.action@v1.3.0
with: with:
action: codecov/codecov-action@v3 action: codecov/codecov-action@v4
# Ensure codecov-action throws an error when it fails to upload # Ensure codecov-action throws an error when it fails to upload
# This allows us to auto-restart the action if an error is thrown # This allows us to auto-restart the action if an error is thrown
with: | with: |
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
# Try re-running action 5 times max # Try re-running action 5 times max
attempt_limit: 5 attempt_limit: 5
# Run again in 30 seconds # Run again in 30 seconds

View File

@@ -28,7 +28,7 @@ jobs:
# Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image # Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image
uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main
with: with:
build_id: dspace-angular build_id: dspace-angular-dev
image_name: dspace/dspace-angular image_name: dspace/dspace-angular
dockerfile_path: ./Dockerfile dockerfile_path: ./Dockerfile
secrets: secrets:

View File

@@ -16,7 +16,7 @@ jobs:
# Only add to project board if issue is flagged as "needs triage" or has no labels # Only add to project board if issue is flagged as "needs triage" or has no labels
# NOTE: By default we flag new issues as "needs triage" in our issue template # NOTE: By default we flag new issues as "needs triage" in our issue template
if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '') if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '')
uses: actions/add-to-project@v0.5.0 uses: actions/add-to-project@v1.0.0
# Note, the authentication token below is an ORG level Secret. # Note, the authentication token below is an ORG level Secret.
# It must be created/recreated manually via a personal access token with admin:org, project, public_repo permissions # It must be created/recreated manually via a personal access token with admin:org, project, public_repo permissions
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token

View File

@@ -21,4 +21,4 @@ jobs:
# Assign the PR to whomever created it. This is useful for visualizing assignments on project boards # Assign the PR to whomever created it. This is useful for visualizing assignments on project boards
# See https://github.com/toshimaru/auto-author-assign # See https://github.com/toshimaru/auto-author-assign
- name: Assign PR to creator - name: Assign PR to creator
uses: toshimaru/auto-author-assign@v2.0.1 uses: toshimaru/auto-author-assign@v2.1.0

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/.angular/cache /.angular/cache
/.nx
/__build__ /__build__
/__server_build__ /__server_build__
/node_modules /node_modules

View File

@@ -109,22 +109,22 @@
"serve": { "serve": {
"builder": "@angular-builders/custom-webpack:dev-server", "builder": "@angular-builders/custom-webpack:dev-server",
"options": { "options": {
"browserTarget": "dspace-angular:build", "buildTarget": "dspace-angular:build",
"port": 4000 "port": 4000
}, },
"configurations": { "configurations": {
"development": { "development": {
"browserTarget": "dspace-angular:build:development" "buildTarget": "dspace-angular:build:development"
}, },
"production": { "production": {
"browserTarget": "dspace-angular:build:production" "buildTarget": "dspace-angular:build:production"
} }
} }
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "dspace-angular:build" "buildTarget": "dspace-angular:build"
} }
}, },
"test": { "test": {
@@ -217,23 +217,23 @@
} }
}, },
"serve-ssr": { "serve-ssr": {
"builder": "@nguniversal/builders:ssr-dev-server", "builder": "@angular-devkit/build-angular:ssr-dev-server",
"options": { "options": {
"browserTarget": "dspace-angular:build", "buildTarget": "dspace-angular:build",
"serverTarget": "dspace-angular:server", "serverTarget": "dspace-angular:server",
"port": 4000 "port": 4000
}, },
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "dspace-angular:build:production", "buildTarget": "dspace-angular:build:production",
"serverTarget": "dspace-angular:server:production" "serverTarget": "dspace-angular:server:production"
} }
} }
}, },
"prerender": { "prerender": {
"builder": "@nguniversal/builders:prerender", "builder": "@angular-devkit/build-angular:prerender",
"options": { "options": {
"browserTarget": "dspace-angular:build:production", "buildTarget": "dspace-angular:build:production",
"serverTarget": "dspace-angular:server:production", "serverTarget": "dspace-angular:server:production",
"routes": [ "routes": [
"/" "/"

View File

@@ -17,6 +17,13 @@ ui:
# Trust X-FORWARDED-* headers from proxies (default = true) # Trust X-FORWARDED-* headers from proxies (default = true)
useProxies: 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
# 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
# 'synced' with the 'dspace.server.url' setting in your backend's local.cfg. # 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.
@@ -400,10 +407,11 @@ mediaViewer:
# Whether the end user agreement is required before users use the repository. # Whether the end user agreement is required before users use the repository.
# If enabled, the user will be required to accept the agreement before they can use the repository. # If enabled, the user will be required to accept the agreement before they can use the repository.
# And whether the privacy statement should exist or not. # And whether the privacy statement/COAR notify support page should exist or not.
info: info:
enableEndUserAgreement: true enableEndUserAgreement: true
enablePrivacyStatement: true enablePrivacyStatement: true
enableCOARNotifySupport: true
# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/) # Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
# display in supported metadata fields. By default, only dc.description.abstract is supported. # display in supported metadata fields. By default, only dc.description.abstract is supported.

View File

@@ -1,28 +1,28 @@
import { Options } from 'cypress-axe';
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
import { Options } from 'cypress-axe';
describe('Admin Sidebar', () => { describe('Admin Sidebar', () => {
beforeEach(() => { beforeEach(() => {
// Must login as an Admin for sidebar to appear // Must login as an Admin for sidebar to appear
cy.visit('/login'); cy.visit('/login');
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'));
}); });
it('should be pinnable and pass accessibility tests', () => { it('should be pinnable and pass accessibility tests', () => {
// Pin the sidebar open // Pin the sidebar open
cy.get('#sidebar-collapse-toggle').click(); cy.get('#sidebar-collapse-toggle').click();
// Click on every expandable section to open all menus // Click on every expandable section to open all menus
cy.get('ds-expandable-admin-sidebar-section').click({multiple: true}); cy.get('ds-expandable-admin-sidebar-section').click({ multiple: true });
// Analyze <ds-admin-sidebar> for accessibility // Analyze <ds-admin-sidebar> for accessibility
testA11y('ds-admin-sidebar', testA11y('ds-admin-sidebar',
{ {
rules: { rules: {
// Currently all expandable sections have nested interactive elements // Currently all expandable sections have nested interactive elements
// See https://github.com/DSpace/dspace-angular/issues/2178 // See https://github.com/DSpace/dspace-angular/issues/2178
'nested-interactive': { enabled: false }, 'nested-interactive': { enabled: false },
} },
} as Options); } as Options);
}); });
}); });

View File

@@ -1,14 +1,14 @@
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('Breadcrumbs', () => { describe('Breadcrumbs', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
// Visit an Item, as those have more breadcrumbs // Visit an Item, as those have more breadcrumbs
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'))); cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
// Wait for breadcrumbs to be visible // Wait for breadcrumbs to be visible
cy.get('ds-breadcrumbs').should('be.visible'); cy.get('ds-breadcrumbs').should('be.visible');
// Analyze <ds-breadcrumbs> for accessibility // Analyze <ds-breadcrumbs> for accessibility
testA11y('ds-breadcrumbs'); testA11y('ds-breadcrumbs');
}); });
}); });

View File

@@ -1,13 +1,13 @@
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('Browse By Author', () => { describe('Browse By Author', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.visit('/browse/author'); cy.visit('/browse/author');
// Wait for <ds-browse-by-metadata-page> to be visible // Wait for <ds-browse-by-metadata-page> to be visible
cy.get('ds-browse-by-metadata').should('be.visible'); cy.get('ds-browse-by-metadata').should('be.visible');
// Analyze <ds-browse-by-metadata-page> for accessibility // Analyze <ds-browse-by-metadata-page> for accessibility
testA11y('ds-browse-by-metadata'); testA11y('ds-browse-by-metadata');
}); });
}); });

View File

@@ -1,13 +1,13 @@
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('Browse By Date Issued', () => { describe('Browse By Date Issued', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.visit('/browse/dateissued'); cy.visit('/browse/dateissued');
// Wait for <ds-browse-by-date-page> to be visible // Wait for <ds-browse-by-date-page> to be visible
cy.get('ds-browse-by-date').should('be.visible'); cy.get('ds-browse-by-date').should('be.visible');
// Analyze <ds-browse-by-date-page> for accessibility // Analyze <ds-browse-by-date-page> for accessibility
testA11y('ds-browse-by-date'); testA11y('ds-browse-by-date');
}); });
}); });

View File

@@ -1,13 +1,13 @@
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('Browse By Subject', () => { describe('Browse By Subject', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.visit('/browse/subject'); cy.visit('/browse/subject');
// Wait for <ds-browse-by-metadata-page> to be visible // Wait for <ds-browse-by-metadata-page> to be visible
cy.get('ds-browse-by-metadata').should('be.visible'); cy.get('ds-browse-by-metadata').should('be.visible');
// Analyze <ds-browse-by-metadata-page> for accessibility // Analyze <ds-browse-by-metadata-page> for accessibility
testA11y('ds-browse-by-metadata'); testA11y('ds-browse-by-metadata');
}); });
}); });

View File

@@ -1,13 +1,13 @@
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('Browse By Title', () => { describe('Browse By Title', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.visit('/browse/title'); cy.visit('/browse/title');
// Wait for <ds-browse-by-title-page> to be visible // Wait for <ds-browse-by-title-page> to be visible
cy.get('ds-browse-by-title').should('be.visible'); cy.get('ds-browse-by-title').should('be.visible');
// Analyze <ds-browse-by-title-page> for accessibility // Analyze <ds-browse-by-title-page> for accessibility
testA11y('ds-browse-by-title'); testA11y('ds-browse-by-title');
}); });
}); });

View File

@@ -3,126 +3,126 @@ import { testA11y } from 'cypress/support/utils';
const COLLECTION_EDIT_PAGE = '/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('/edit'); const COLLECTION_EDIT_PAGE = '/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('/edit');
beforeEach(() => { beforeEach(() => {
// All tests start with visiting the Edit Collection Page // All tests start with visiting the Edit Collection Page
cy.visit(COLLECTION_EDIT_PAGE); cy.visit(COLLECTION_EDIT_PAGE);
// 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'));
}); });
describe('Edit Collection > Edit Metadata tab', () => { describe('Edit Collection > Edit Metadata tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
// <ds-edit-collection> tag must be loaded // <ds-edit-collection> tag must be loaded
cy.get('ds-edit-collection').should('be.visible'); cy.get('ds-edit-collection').should('be.visible');
// Analyze <ds-edit-collection> for accessibility issues // Analyze <ds-edit-collection> for accessibility issues
testA11y('ds-edit-collection'); testA11y('ds-edit-collection');
}); });
}); });
describe('Edit Collection > Assign Roles tab', () => { describe('Edit Collection > Assign Roles tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="roles"]').click(); cy.get('a[data-test="roles"]').click();
// <ds-collection-roles> tag must be loaded // <ds-collection-roles> tag must be loaded
cy.get('ds-collection-roles').should('be.visible'); cy.get('ds-collection-roles').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-collection-roles'); testA11y('ds-collection-roles');
}); });
}); });
describe('Edit Collection > Content Source tab', () => { describe('Edit Collection > Content Source tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="source"]').click(); cy.get('a[data-test="source"]').click();
// <ds-collection-source> tag must be loaded // <ds-collection-source> tag must be loaded
cy.get('ds-collection-source').should('be.visible'); cy.get('ds-collection-source').should('be.visible');
// Check the external source checkbox (to display all fields on the page) // Check the external source checkbox (to display all fields on the page)
cy.get('#externalSourceCheck').check(); cy.get('#externalSourceCheck').check();
// Wait for the source controls to appear // Wait for the source controls to appear
// cy.get('ds-collection-source-controls').should('be.visible'); // cy.get('ds-collection-source-controls').should('be.visible');
// Analyze entire page for accessibility issues // Analyze entire page for accessibility issues
testA11y('ds-collection-source'); testA11y('ds-collection-source');
}); });
}); });
describe('Edit Collection > Curate tab', () => { describe('Edit Collection > Curate tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="curate"]').click(); cy.get('a[data-test="curate"]').click();
// <ds-collection-curate> tag must be loaded // <ds-collection-curate> tag must be loaded
cy.get('ds-collection-curate').should('be.visible'); cy.get('ds-collection-curate').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-collection-curate'); testA11y('ds-collection-curate');
}); });
}); });
describe('Edit Collection > Access Control tab', () => { describe('Edit Collection > Access Control tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="access-control"]').click(); cy.get('a[data-test="access-control"]').click();
// <ds-collection-access-control> tag must be loaded // <ds-collection-access-control> tag must be loaded
cy.get('ds-collection-access-control').should('be.visible'); cy.get('ds-collection-access-control').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-collection-access-control'); testA11y('ds-collection-access-control');
}); });
}); });
describe('Edit Collection > Authorizations tab', () => { describe('Edit Collection > Authorizations tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="authorizations"]').click(); cy.get('a[data-test="authorizations"]').click();
// <ds-collection-authorizations> tag must be loaded // <ds-collection-authorizations> tag must be loaded
cy.get('ds-collection-authorizations').should('be.visible'); cy.get('ds-collection-authorizations').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-collection-authorizations'); testA11y('ds-collection-authorizations');
}); });
}); });
describe('Edit Collection > Item Mapper tab', () => { describe('Edit Collection > Item Mapper tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="mapper"]').click(); cy.get('a[data-test="mapper"]').click();
// <ds-collection-item-mapper> tag must be loaded // <ds-collection-item-mapper> tag must be loaded
cy.get('ds-collection-item-mapper').should('be.visible'); cy.get('ds-collection-item-mapper').should('be.visible');
// Analyze entire page for accessibility issues // Analyze entire page for accessibility issues
testA11y('ds-collection-item-mapper'); testA11y('ds-collection-item-mapper');
// Click on the "Map new Items" tab // Click on the "Map new Items" tab
cy.get('li[data-test="mapTab"] a').click(); cy.get('li[data-test="mapTab"] a').click();
// Make sure search form is now visible // Make sure search form is now visible
cy.get('ds-search-form').should('be.visible'); cy.get('ds-search-form').should('be.visible');
// Analyze entire page (again) for accessibility issues // Analyze entire page (again) for accessibility issues
testA11y('ds-collection-item-mapper'); testA11y('ds-collection-item-mapper');
}); });
}); });
describe('Edit Collection > Delete page', () => { describe('Edit Collection > Delete page', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="delete-button"]').click(); cy.get('a[data-test="delete-button"]').click();
// <ds-delete-collection> tag must be loaded // <ds-delete-collection> tag must be loaded
cy.get('ds-delete-collection').should('be.visible'); cy.get('ds-delete-collection').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-delete-collection'); testA11y('ds-delete-collection');
}); });
}); });

View File

@@ -2,13 +2,13 @@ import { testA11y } from 'cypress/support/utils';
describe('Collection Page', () => { describe('Collection Page', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'))); cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
// <ds-collection-page> tag must be loaded // <ds-collection-page> tag must be loaded
cy.get('ds-collection-page').should('be.visible'); cy.get('ds-collection-page').should('be.visible');
// Analyze <ds-collection-page> for accessibility issues // Analyze <ds-collection-page> for accessibility issues
testA11y('ds-collection-page'); testA11y('ds-collection-page');
}); });
}); });

View File

@@ -2,36 +2,36 @@ import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('Collection Statistics Page', () => { describe('Collection Statistics Page', () => {
const COLLECTIONSTATISTICSPAGE = '/statistics/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')); const COLLECTIONSTATISTICSPAGE = '/statistics/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'));
it('should load if you click on "Statistics" from a Collection page', () => { it('should load if you click on "Statistics" from a Collection page', () => {
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'))); cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click(); cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE); cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
}); });
it('should contain a "Total visits" section', () => { it('should contain a "Total visits" section', () => {
cy.visit(COLLECTIONSTATISTICSPAGE); cy.visit(COLLECTIONSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible'); cy.get('table[data-test="TotalVisits"]').should('be.visible');
}); });
it('should contain a "Total visits per month" section', () => { it('should contain a "Total visits per month" section', () => {
cy.visit(COLLECTIONSTATISTICSPAGE); cy.visit(COLLECTIONSTATISTICSPAGE);
// Check just for existence because this table is empty in CI environment as it's historical data // Check just for existence because this table is empty in CI environment as it's historical data
cy.get('.'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('_TotalVisitsPerMonth')).should('exist'); cy.get('.'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('_TotalVisitsPerMonth')).should('exist');
}); });
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.visit(COLLECTIONSTATISTICSPAGE); cy.visit(COLLECTIONSTATISTICSPAGE);
// <ds-collection-statistics-page> tag must be loaded // <ds-collection-statistics-page> tag must be loaded
cy.get('ds-collection-statistics-page').should('be.visible'); cy.get('ds-collection-statistics-page').should('be.visible');
// Verify / wait until "Total Visits" table's label is non-empty // Verify / wait until "Total Visits" table's label is non-empty
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page) // (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT); cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
// Analyze <ds-collection-statistics-page> for accessibility issues // Analyze <ds-collection-statistics-page> for accessibility issues
testA11y('ds-collection-statistics-page'); testA11y('ds-collection-statistics-page');
}); });
}); });

View File

@@ -3,84 +3,84 @@ import { testA11y } from 'cypress/support/utils';
const COMMUNITY_EDIT_PAGE = '/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')).concat('/edit'); const COMMUNITY_EDIT_PAGE = '/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')).concat('/edit');
beforeEach(() => { beforeEach(() => {
// All tests start with visiting the Edit Community Page // All tests start with visiting the Edit Community Page
cy.visit(COMMUNITY_EDIT_PAGE); cy.visit(COMMUNITY_EDIT_PAGE);
// 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'));
}); });
describe('Edit Community > Edit Metadata tab', () => { describe('Edit Community > Edit Metadata tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
// <ds-edit-community> tag must be loaded // <ds-edit-community> tag must be loaded
cy.get('ds-edit-community').should('be.visible'); cy.get('ds-edit-community').should('be.visible');
// Analyze <ds-edit-community> for accessibility issues // Analyze <ds-edit-community> for accessibility issues
testA11y('ds-edit-community'); testA11y('ds-edit-community');
}); });
}); });
describe('Edit Community > Assign Roles tab', () => { describe('Edit Community > Assign Roles tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="roles"]').click(); cy.get('a[data-test="roles"]').click();
// <ds-community-roles> tag must be loaded // <ds-community-roles> tag must be loaded
cy.get('ds-community-roles').should('be.visible'); cy.get('ds-community-roles').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-community-roles'); testA11y('ds-community-roles');
}); });
}); });
describe('Edit Community > Curate tab', () => { describe('Edit Community > Curate tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="curate"]').click(); cy.get('a[data-test="curate"]').click();
// <ds-community-curate> tag must be loaded // <ds-community-curate> tag must be loaded
cy.get('ds-community-curate').should('be.visible'); cy.get('ds-community-curate').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-community-curate'); testA11y('ds-community-curate');
}); });
}); });
describe('Edit Community > Access Control tab', () => { describe('Edit Community > Access Control tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="access-control"]').click(); cy.get('a[data-test="access-control"]').click();
// <ds-community-access-control> tag must be loaded // <ds-community-access-control> tag must be loaded
cy.get('ds-community-access-control').should('be.visible'); cy.get('ds-community-access-control').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-community-access-control'); testA11y('ds-community-access-control');
}); });
}); });
describe('Edit Community > Authorizations tab', () => { describe('Edit Community > Authorizations tab', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="authorizations"]').click(); cy.get('a[data-test="authorizations"]').click();
// <ds-community-authorizations> tag must be loaded // <ds-community-authorizations> tag must be loaded
cy.get('ds-community-authorizations').should('be.visible'); cy.get('ds-community-authorizations').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-community-authorizations'); testA11y('ds-community-authorizations');
}); });
}); });
describe('Edit Community > Delete page', () => { describe('Edit Community > Delete page', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.get('a[data-test="delete-button"]').click(); cy.get('a[data-test="delete-button"]').click();
// <ds-delete-community> tag must be loaded // <ds-delete-community> tag must be loaded
cy.get('ds-delete-community').should('be.visible'); cy.get('ds-delete-community').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-delete-community'); testA11y('ds-delete-community');
}); });
}); });

View File

@@ -2,16 +2,16 @@ import { testA11y } from 'cypress/support/utils';
describe('Community List Page', () => { describe('Community List Page', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.visit('/community-list'); cy.visit('/community-list');
// <ds-community-list-page> tag must be loaded // <ds-community-list-page> tag must be loaded
cy.get('ds-community-list-page').should('be.visible'); cy.get('ds-community-list-page').should('be.visible');
// Open every expand button on page, so that we can scan sub-elements as well // Open every expand button on page, so that we can scan sub-elements as well
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');
}); });
}); });

View File

@@ -2,13 +2,13 @@ import { testA11y } from 'cypress/support/utils';
describe('Community Page', () => { describe('Community Page', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY'))); cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
// <ds-community-page> tag must be loaded // <ds-community-page> tag must be loaded
cy.get('ds-community-page').should('be.visible'); cy.get('ds-community-page').should('be.visible');
// Analyze <ds-community-page> for accessibility issues // Analyze <ds-community-page> for accessibility issues
testA11y('ds-community-page'); testA11y('ds-community-page');
}); });
}); });

View File

@@ -2,36 +2,36 @@ import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('Community Statistics Page', () => { describe('Community Statistics Page', () => {
const COMMUNITYSTATISTICSPAGE = '/statistics/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')); const COMMUNITYSTATISTICSPAGE = '/statistics/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY'));
it('should load if you click on "Statistics" from a Community page', () => { it('should load if you click on "Statistics" from a Community page', () => {
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY'))); cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click(); cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE); cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
}); });
it('should contain a "Total visits" section', () => { it('should contain a "Total visits" section', () => {
cy.visit(COMMUNITYSTATISTICSPAGE); cy.visit(COMMUNITYSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible'); cy.get('table[data-test="TotalVisits"]').should('be.visible');
}); });
it('should contain a "Total visits per month" section', () => { it('should contain a "Total visits per month" section', () => {
cy.visit(COMMUNITYSTATISTICSPAGE); cy.visit(COMMUNITYSTATISTICSPAGE);
// Check just for existence because this table is empty in CI environment as it's historical data // Check just for existence because this table is empty in CI environment as it's historical data
cy.get('.'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')).concat('_TotalVisitsPerMonth')).should('exist'); cy.get('.'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')).concat('_TotalVisitsPerMonth')).should('exist');
}); });
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.visit(COMMUNITYSTATISTICSPAGE); cy.visit(COMMUNITYSTATISTICSPAGE);
// <ds-community-statistics-page> tag must be loaded // <ds-community-statistics-page> tag must be loaded
cy.get('ds-community-statistics-page').should('be.visible'); cy.get('ds-community-statistics-page').should('be.visible');
// Verify / wait until "Total Visits" table's label is non-empty // Verify / wait until "Total Visits" table's label is non-empty
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page) // (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT); cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
// Analyze <ds-community-statistics-page> for accessibility issues // Analyze <ds-community-statistics-page> for accessibility issues
testA11y('ds-community-statistics-page'); testA11y('ds-community-statistics-page');
}); });
}); });

View File

@@ -1,13 +1,13 @@
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('Footer', () => { describe('Footer', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.visit('/'); cy.visit('/');
// Footer must first be visible // Footer must first be visible
cy.get('ds-footer').should('be.visible'); cy.get('ds-footer').should('be.visible');
// Analyze <ds-footer> for accessibility // Analyze <ds-footer> for accessibility
testA11y('ds-footer'); testA11y('ds-footer');
}); });
}); });

View File

@@ -1,13 +1,13 @@
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('Header', () => { describe('Header', () => {
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.visit('/'); cy.visit('/');
// Header must first be visible // Header must first be visible
cy.get('ds-header').should('be.visible'); cy.get('ds-header').should('be.visible');
// Analyze <ds-header> for accessibility // Analyze <ds-header> for accessibility
testA11y('ds-header'); testA11y('ds-header');
}); });
}); });

View File

@@ -1,31 +1,32 @@
import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
import { testA11y } from 'cypress/support/utils';
import '../support/commands'; import '../support/commands';
import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
import { testA11y } from 'cypress/support/utils';
describe('Site Statistics Page', () => { describe('Site Statistics Page', () => {
it('should load if you click on "Statistics" from homepage', () => { it('should load if you click on "Statistics" from homepage', () => {
cy.visit('/'); cy.visit('/');
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click(); cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', '/statistics'); cy.location('pathname').should('eq', '/statistics');
}); });
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
// generate 2 view events on an Item's page // generate 2 view events on an Item's page
cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item'); cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item');
cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item'); cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item');
cy.visit('/statistics'); cy.visit('/statistics');
// <ds-site-statistics-page> tag must be visable // <ds-site-statistics-page> tag must be visable
cy.get('ds-site-statistics-page').should('be.visible'); cy.get('ds-site-statistics-page').should('be.visible');
// Verify / wait until "Total Visits" table's *last* label is non-empty // Verify / wait until "Total Visits" table's *last* label is non-empty
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page) // (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').last().contains(REGEX_MATCH_NON_EMPTY_TEXT); cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').last().contains(REGEX_MATCH_NON_EMPTY_TEXT);
// Wait an extra 500ms, just so all entries in Total Visits have loaded. // Wait an extra 500ms, just so all entries in Total Visits have loaded.
cy.wait(500); cy.wait(500);
// Analyze <ds-site-statistics-page> for accessibility issues // Analyze <ds-site-statistics-page> for accessibility issues
testA11y('ds-site-statistics-page'); testA11y('ds-site-statistics-page');
}); });
}); });

View File

@@ -1,135 +1,135 @@
import { Options } from 'cypress-axe';
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
import { Options } from 'cypress-axe';
const ITEM_EDIT_PAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('/edit'); const ITEM_EDIT_PAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('/edit');
beforeEach(() => { beforeEach(() => {
// All tests start with visiting the Edit Item Page // All tests start with visiting the Edit Item Page
cy.visit(ITEM_EDIT_PAGE); cy.visit(ITEM_EDIT_PAGE);
// 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'));
}); });
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"]').click(); cy.get('a[data-test="metadata"]').click();
// <ds-edit-item-page> tag must be loaded // <ds-edit-item-page> tag must be loaded
cy.get('ds-edit-item-page').should('be.visible'); cy.get('ds-edit-item-page').should('be.visible');
// Analyze <ds-edit-item-page> for accessibility issues // Analyze <ds-edit-item-page> for accessibility issues
testA11y('ds-edit-item-page'); testA11y('ds-edit-item-page');
}); });
}); });
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"]').click(); cy.get('a[data-test="status"]').click();
// <ds-item-status> tag must be loaded // <ds-item-status> tag must be loaded
cy.get('ds-item-status').should('be.visible'); cy.get('ds-item-status').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-item-status'); testA11y('ds-item-status');
}); });
}); });
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"]').click(); cy.get('a[data-test="bitstreams"]').click();
// <ds-item-bitstreams> tag must be loaded // <ds-item-bitstreams> tag must be loaded
cy.get('ds-item-bitstreams').should('be.visible'); cy.get('ds-item-bitstreams').should('be.visible');
// Table of item bitstreams must also be loaded // Table of item bitstreams must also be loaded
cy.get('div.item-bitstreams').should('be.visible'); cy.get('div.item-bitstreams').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-item-bitstreams', testA11y('ds-item-bitstreams',
{ {
rules: { rules: {
// Currently Bitstreams page loads a pagination component per Bundle // Currently Bitstreams page loads a pagination component per Bundle
// and they all use the same 'id="p-dad"'. // and they all use the same 'id="p-dad"'.
'duplicate-id': { enabled: false }, 'duplicate-id': { enabled: false },
} },
} as Options } as Options,
); );
}); });
}); });
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"]').click(); cy.get('a[data-test="curate"]').click();
// <ds-item-curate> tag must be loaded // <ds-item-curate> tag must be loaded
cy.get('ds-item-curate').should('be.visible'); cy.get('ds-item-curate').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-item-curate'); testA11y('ds-item-curate');
}); });
}); });
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"]').click(); cy.get('a[data-test="relationships"]').click();
// <ds-item-relationships> tag must be loaded // <ds-item-relationships> tag must be loaded
cy.get('ds-item-relationships').should('be.visible'); cy.get('ds-item-relationships').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-item-relationships'); testA11y('ds-item-relationships');
}); });
}); });
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"]').click(); cy.get('a[data-test="versionhistory"]').click();
// <ds-item-version-history> tag must be loaded // <ds-item-version-history> tag must be loaded
cy.get('ds-item-version-history').should('be.visible'); cy.get('ds-item-version-history').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-item-version-history'); testA11y('ds-item-version-history');
}); });
}); });
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"]').click(); cy.get('a[data-test="access-control"]').click();
// <ds-item-access-control> tag must be loaded // <ds-item-access-control> tag must be loaded
cy.get('ds-item-access-control').should('be.visible'); cy.get('ds-item-access-control').should('be.visible');
// Analyze for accessibility issues // Analyze for accessibility issues
testA11y('ds-item-access-control'); testA11y('ds-item-access-control');
}); });
}); });
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"]').click(); cy.get('a[data-test="mapper"]').click();
// <ds-item-collection-mapper> tag must be loaded // <ds-item-collection-mapper> tag must be loaded
cy.get('ds-item-collection-mapper').should('be.visible'); cy.get('ds-item-collection-mapper').should('be.visible');
// Analyze entire page for accessibility issues // Analyze entire page for accessibility issues
testA11y('ds-item-collection-mapper'); testA11y('ds-item-collection-mapper');
// Click on the "Map new collections" tab // Click on the "Map new collections" tab
cy.get('li[data-test="mapTab"] a').click(); cy.get('li[data-test="mapTab"] a').click();
// Make sure search form is now visible // Make sure search form is now visible
cy.get('ds-search-form').should('be.visible'); cy.get('ds-search-form').should('be.visible');
// Analyze entire page (again) for accessibility issues // Analyze entire page (again) for accessibility issues
testA11y('ds-item-collection-mapper'); testA11y('ds-item-collection-mapper');
}); });
}); });

View File

@@ -1,32 +1,32 @@
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('Item Page', () => { describe('Item Page', () => {
const ITEMPAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); const ITEMPAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
const ENTITYPAGE = '/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); const ENTITYPAGE = '/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
// Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid] // Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid]
it('should redirect to the entity page when navigating to an item page', () => { it('should redirect to the entity page when navigating to an item page', () => {
cy.visit(ITEMPAGE); cy.visit(ITEMPAGE);
cy.location('pathname').should('eq', ENTITYPAGE); cy.location('pathname').should('eq', ENTITYPAGE);
}); });
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.visit(ENTITYPAGE); cy.visit(ENTITYPAGE);
// <ds-item-page> tag must be loaded // <ds-item-page> tag must be loaded
cy.get('ds-item-page').should('be.visible'); cy.get('ds-item-page').should('be.visible');
// Analyze <ds-item-page> for accessibility issues // Analyze <ds-item-page> for accessibility issues
testA11y('ds-item-page'); testA11y('ds-item-page');
}); });
it('should pass accessibility tests on full item page', () => { it('should pass accessibility tests on full item page', () => {
cy.visit(ENTITYPAGE + '/full'); cy.visit(ENTITYPAGE + '/full');
// <ds-full-item-page> tag must be loaded // <ds-full-item-page> tag must be loaded
cy.get('ds-full-item-page').should('be.visible'); cy.get('ds-full-item-page').should('be.visible');
// Analyze <ds-full-item-page> for accessibility issues // Analyze <ds-full-item-page> for accessibility issues
testA11y('ds-full-item-page'); testA11y('ds-full-item-page');
}); });
}); });

View File

@@ -2,42 +2,42 @@ import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('Item Statistics Page', () => { describe('Item Statistics Page', () => {
const ITEMSTATISTICSPAGE = '/statistics/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); const ITEMSTATISTICSPAGE = '/statistics/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
it('should load if you click on "Statistics" from an Item/Entity page', () => { it('should load if you click on "Statistics" from an Item/Entity page', () => {
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'))); cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click(); cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE); cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
}); });
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => { it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
cy.visit(ITEMSTATISTICSPAGE); cy.visit(ITEMSTATISTICSPAGE);
cy.get('ds-item-statistics-page').should('be.visible'); cy.get('ds-item-statistics-page').should('be.visible');
cy.get('ds-item-page').should('not.exist'); cy.get('ds-item-page').should('not.exist');
}); });
it('should contain a "Total visits" section', () => { it('should contain a "Total visits" section', () => {
cy.visit(ITEMSTATISTICSPAGE); cy.visit(ITEMSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible'); cy.get('table[data-test="TotalVisits"]').should('be.visible');
}); });
it('should contain a "Total visits per month" section', () => { it('should contain a "Total visits per month" section', () => {
cy.visit(ITEMSTATISTICSPAGE); cy.visit(ITEMSTATISTICSPAGE);
// Check just for existence because this table is empty in CI environment as it's historical data // Check just for existence because this table is empty in CI environment as it's historical data
cy.get('.'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('_TotalVisitsPerMonth')).should('exist'); cy.get('.'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('_TotalVisitsPerMonth')).should('exist');
}); });
it('should pass accessibility tests', () => { it('should pass accessibility tests', () => {
cy.visit(ITEMSTATISTICSPAGE); cy.visit(ITEMSTATISTICSPAGE);
// <ds-item-statistics-page> tag must be loaded // <ds-item-statistics-page> tag must be loaded
cy.get('ds-item-statistics-page').should('be.visible'); cy.get('ds-item-statistics-page').should('be.visible');
// Verify / wait until "Total Visits" table's label is non-empty // Verify / wait until "Total Visits" table's label is non-empty
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page) // (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT); cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
// Analyze <ds-item-statistics-page> for accessibility issues // Analyze <ds-item-statistics-page> for accessibility issues
testA11y('ds-item-statistics-page'); testA11y('ds-item-statistics-page');
}); });
}); });

View File

@@ -1,150 +1,150 @@
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
const page = { const page = {
openLoginMenu() { openLoginMenu() {
// Click the "Log In" dropdown menu in header // Click the "Log In" dropdown menu in header
cy.get('ds-themed-header [data-test="login-menu"]').click(); cy.get('ds-themed-header [data-test="login-menu"]').click();
}, },
openUserMenu() { openUserMenu() {
// Once logged in, click the User menu in header // Once logged in, click the User menu in header
cy.get('ds-themed-header [data-test="user-menu"]').click(); cy.get('ds-themed-header [data-test="user-menu"]').click();
}, },
submitLoginAndPasswordByPressingButton(email, password) { submitLoginAndPasswordByPressingButton(email, password) {
// Enter email // Enter email
cy.get('ds-themed-header [data-test="email"]').type(email); cy.get('ds-themed-header [data-test="email"]').type(email);
// Enter password // Enter password
cy.get('ds-themed-header [data-test="password"]').type(password); cy.get('ds-themed-header [data-test="password"]').type(password);
// Click login button // Click login button
cy.get('ds-themed-header [data-test="login-button"]').click(); cy.get('ds-themed-header [data-test="login-button"]').click();
}, },
submitLoginAndPasswordByPressingEnter(email, password) { submitLoginAndPasswordByPressingEnter(email, password) {
// In opened Login modal, fill out email & password, then click Enter // In opened Login modal, fill out email & password, then click Enter
cy.get('ds-themed-header [data-test="email"]').type(email); cy.get('ds-themed-header [data-test="email"]').type(email);
cy.get('ds-themed-header [data-test="password"]').type(password); cy.get('ds-themed-header [data-test="password"]').type(password);
cy.get('ds-themed-header [data-test="password"]').type('{enter}'); cy.get('ds-themed-header [data-test="password"]').type('{enter}');
}, },
submitLogoutByPressingButton() { submitLogoutByPressingButton() {
// This is the POST command that will actually log us out // This is the POST command that will actually log us out
cy.intercept('POST', '/server/api/authn/logout').as('logout'); cy.intercept('POST', '/server/api/authn/logout').as('logout');
// Click logout button // Click logout button
cy.get('ds-themed-header [data-test="logout-button"]').click(); cy.get('ds-themed-header [data-test="logout-button"]').click();
// Wait until above POST command responds before continuing // Wait until above POST command responds before continuing
// (This ensures next action waits until logout completes) // (This ensures next action waits until logout completes)
cy.wait('@logout'); cy.wait('@logout');
} },
}; };
describe('Login Modal', () => { describe('Login Modal', () => {
it('should login when clicking button & stay on same page', () => { it('should login when clicking button & stay on same page', () => {
const ENTITYPAGE = '/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); const ENTITYPAGE = '/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
cy.visit(ENTITYPAGE); cy.visit(ENTITYPAGE);
// Login menu should exist // Login menu should exist
cy.get('ds-log-in').should('exist'); cy.get('ds-log-in').should('exist');
// Login, and the <ds-log-in> tag should no longer exist // Login, and the <ds-log-in> tag should no longer exist
page.openLoginMenu(); page.openLoginMenu();
cy.get('.form-login').should('be.visible'); cy.get('.form-login').should('be.visible');
page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
cy.get('ds-log-in').should('not.exist'); cy.get('ds-log-in').should('not.exist');
// Verify we are still on the same page // Verify we are still on the same page
cy.url().should('include', ENTITYPAGE); cy.url().should('include', ENTITYPAGE);
// Open user menu, verify user menu & logout button now available // Open user menu, verify user menu & logout button now available
page.openUserMenu(); page.openUserMenu();
cy.get('ds-user-menu').should('be.visible'); cy.get('ds-user-menu').should('be.visible');
cy.get('ds-log-out').should('be.visible'); cy.get('ds-log-out').should('be.visible');
}); });
it('should login when clicking enter key & stay on same page', () => { it('should login when clicking enter key & stay on same page', () => {
cy.visit('/home'); cy.visit('/home');
// Open login menu in header & verify <ds-log-in> tag is visible // Open login menu in header & verify <ds-log-in> tag is visible
page.openLoginMenu(); page.openLoginMenu();
cy.get('.form-login').should('be.visible'); cy.get('.form-login').should('be.visible');
// Login, and the <ds-log-in> tag should no longer exist // Login, and the <ds-log-in> tag should no longer exist
page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
cy.get('.form-login').should('not.exist'); cy.get('.form-login').should('not.exist');
// Verify we are still on homepage // Verify we are still on homepage
cy.url().should('include', '/home'); cy.url().should('include', '/home');
// Open user menu, verify user menu & logout button now available // Open user menu, verify user menu & logout button now available
page.openUserMenu(); page.openUserMenu();
cy.get('ds-user-menu').should('be.visible'); cy.get('ds-user-menu').should('be.visible');
cy.get('ds-log-out').should('be.visible'); cy.get('ds-log-out').should('be.visible');
}); });
it('should support logout', () => { it('should support logout', () => {
// First authenticate & access homepage // First authenticate & access homepage
cy.login(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); cy.login(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
cy.visit('/'); cy.visit('/');
// Verify ds-log-in tag doesn't exist, but ds-log-out tag does exist // Verify ds-log-in tag doesn't exist, but ds-log-out tag does exist
cy.get('ds-log-in').should('not.exist'); cy.get('ds-log-in').should('not.exist');
cy.get('ds-log-out').should('exist'); cy.get('ds-log-out').should('exist');
// Click logout button // Click logout button
page.openUserMenu(); page.openUserMenu();
page.submitLogoutByPressingButton(); page.submitLogoutByPressingButton();
// Verify ds-log-in tag now exists // Verify ds-log-in tag now exists
cy.get('ds-log-in').should('exist'); cy.get('ds-log-in').should('exist');
cy.get('ds-log-out').should('not.exist'); cy.get('ds-log-out').should('not.exist');
}); });
it('should allow new user registration', () => { it('should allow new user registration', () => {
cy.visit('/'); cy.visit('/');
page.openLoginMenu(); page.openLoginMenu();
// Registration link should be visible // Registration link should be visible
cy.get('ds-themed-header [data-test="register"]').should('be.visible'); cy.get('ds-themed-header [data-test="register"]').should('be.visible');
// Click registration link & you should go to registration page // Click registration link & you should go to registration page
cy.get('ds-themed-header [data-test="register"]').click(); cy.get('ds-themed-header [data-test="register"]').click();
cy.location('pathname').should('eq', '/register'); cy.location('pathname').should('eq', '/register');
cy.get('ds-register-email').should('exist'); cy.get('ds-register-email').should('exist');
// Test accessibility of this page // Test accessibility of this page
testA11y('ds-register-email'); testA11y('ds-register-email');
}); });
it('should allow forgot password', () => { it('should allow forgot password', () => {
cy.visit('/'); cy.visit('/');
page.openLoginMenu(); page.openLoginMenu();
// Forgot password link should be visible // Forgot password link should be visible
cy.get('ds-themed-header [data-test="forgot"]').should('be.visible'); cy.get('ds-themed-header [data-test="forgot"]').should('be.visible');
// Click link & you should go to Forgot Password page // Click link & you should go to Forgot Password page
cy.get('ds-themed-header [data-test="forgot"]').click(); cy.get('ds-themed-header [data-test="forgot"]').click();
cy.location('pathname').should('eq', '/forgot'); cy.location('pathname').should('eq', '/forgot');
cy.get('ds-forgot-email').should('exist'); cy.get('ds-forgot-email').should('exist');
// Test accessibility of this page // Test accessibility of this page
testA11y('ds-forgot-email'); testA11y('ds-forgot-email');
}); });
it('should pass accessibility tests in menus', () => { it('should pass accessibility tests in menus', () => {
cy.visit('/'); cy.visit('/');
// Open login menu & verify accessibility // Open login menu & verify accessibility
page.openLoginMenu(); page.openLoginMenu();
cy.get('ds-log-in').should('exist'); cy.get('ds-log-in').should('exist');
testA11y('ds-log-in'); testA11y('ds-log-in');
// Now login // Now login
page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
cy.get('ds-log-in').should('not.exist'); cy.get('ds-log-in').should('not.exist');
// Open user menu, verify user menu accesibility // Open user menu, verify user menu accesibility
page.openUserMenu(); page.openUserMenu();
cy.get('ds-user-menu').should('be.visible'); cy.get('ds-user-menu').should('be.visible');
testA11y('ds-user-menu'); testA11y('ds-user-menu');
}); });
}); });

View File

@@ -1,134 +1,134 @@
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('My DSpace page', () => { describe('My DSpace page', () => {
it('should display recent submissions and pass accessibility tests', () => { it('should display recent submissions and pass accessibility tests', () => {
cy.visit('/mydspace'); cy.visit('/mydspace');
// 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_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
cy.get('ds-my-dspace-page').should('be.visible'); cy.get('ds-my-dspace-page').should('be.visible');
// At least one recent submission should be displayed // At least one recent submission should be displayed
cy.get('[data-test="list-object"]').should('be.visible'); cy.get('[data-test="list-object"]').should('be.visible');
// Click each filter toggle to open *every* filter // Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well) // (As we want to scan filter section for accessibility issues as well)
cy.get('.filter-toggle').click({ multiple: true }); cy.get('.filter-toggle').click({ multiple: true });
// Analyze <ds-my-dspace-page> for accessibility issues // Analyze <ds-my-dspace-page> for accessibility issues
testA11y('ds-my-dspace-page'); testA11y('ds-my-dspace-page');
});
it('should have a working detailed view that passes accessibility tests', () => {
cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
cy.get('ds-my-dspace-page').should('be.visible');
// Click button in sidebar to display detailed view
cy.get('ds-search-sidebar [data-test="detail-view"]').click();
cy.get('ds-object-detail').should('be.visible');
// Analyze <ds-my-dspace-page> for accessibility issues
testA11y('ds-my-dspace-page');
});
// NOTE: Deleting existing submissions is exercised by submission.spec.ts
it('should let you start a new submission & edit in-progress submissions', () => {
cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Open the New Submission dropdown
cy.get('button[data-test="submission-dropdown"]').click();
// Click on the "Item" type in that dropdown
cy.get('#entityControlsDropdownMenu button[title="none"]').click();
// This should display the <ds-create-item-parent-selector> (popup window)
cy.get('ds-create-item-parent-selector').should('be.visible');
// Type in a known Collection name in the search box
cy.get('ds-authorized-collection-selector input[type="search"]').type(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME'));
// Click on the button matching that known Collection name
cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')).concat('"]')).click();
// New URL should include /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
// The Submission edit form tag should be visible
cy.get('ds-submission-edit').should('be.visible');
// A Collection menu button should exist & its value should be the selected collection
cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME'));
// Now that we've created a submission, we'll test that we can go back and Edit it.
// Get our Submission URL, to parse out the ID of this new submission
cy.location().then(fullUrl => {
// This will be the full path (/workspaceitems/[id]/edit)
const path = fullUrl.pathname;
// Split on the slashes
const subpaths = path.split('/');
// Part 2 will be the [id] of the submission
const id = subpaths[2];
// Click the "Save for Later" button to save this submission
cy.get('ds-submission-form-footer [data-test="save-for-later"]').click();
// "Save for Later" should send us to MyDSpace
cy.url().should('include', '/mydspace');
// Close any open notifications, to make sure they don't get in the way of next steps
cy.get('[data-dismiss="alert"]').click({ multiple: true });
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// On MyDSpace, find the submission we just created via its ID
cy.get('[data-test="search-box"]').type(id);
cy.get('[data-test="search-button"]').click();
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// Click the Edit button for this in-progress submission
cy.get('#edit_' + id).click();
// Should send us back to the submission form
cy.url().should('include', '/workspaceitems/' + id + '/edit');
// Discard our new submission by clicking Discard in Submission form & confirming
cy.get('ds-submission-form-footer [data-test="discard"]').click();
cy.get('button#discard_submit').click();
// Discarding should send us back to MyDSpace
cy.url().should('include', '/mydspace');
}); });
});
it('should have a working detailed view that passes accessibility tests', () => { it('should let you import from external sources', () => {
cy.visit('/mydspace'); cy.visit('/mydspace');
// 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_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
cy.get('ds-my-dspace-page').should('be.visible'); // Open the New Import dropdown
cy.get('button[data-test="import-dropdown"]').click();
// Click on the "Item" type in that dropdown
cy.get('#importControlsDropdownMenu button[title="none"]').click();
// Click button in sidebar to display detailed view // New URL should include /import-external, as we've moved to the import page
cy.get('ds-search-sidebar [data-test="detail-view"]').click(); cy.url().should('include', '/import-external');
cy.get('ds-object-detail').should('be.visible'); // The external import searchbox should be visible
cy.get('ds-submission-import-external-searchbar').should('be.visible');
// Analyze <ds-my-dspace-page> for accessibility issues // Test for accessibility issues
testA11y('ds-my-dspace-page'); testA11y('ds-submission-import-external');
}); });
// NOTE: Deleting existing submissions is exercised by submission.spec.ts
it('should let you start a new submission & edit in-progress submissions', () => {
cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Open the New Submission dropdown
cy.get('button[data-test="submission-dropdown"]').click();
// Click on the "Item" type in that dropdown
cy.get('#entityControlsDropdownMenu button[title="none"]').click();
// This should display the <ds-create-item-parent-selector> (popup window)
cy.get('ds-create-item-parent-selector').should('be.visible');
// Type in a known Collection name in the search box
cy.get('ds-authorized-collection-selector input[type="search"]').type(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME'));
// Click on the button matching that known Collection name
cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')).concat('"]')).click();
// New URL should include /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
// The Submission edit form tag should be visible
cy.get('ds-submission-edit').should('be.visible');
// A Collection menu button should exist & its value should be the selected collection
cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME'));
// Now that we've created a submission, we'll test that we can go back and Edit it.
// Get our Submission URL, to parse out the ID of this new submission
cy.location().then(fullUrl => {
// This will be the full path (/workspaceitems/[id]/edit)
const path = fullUrl.pathname;
// Split on the slashes
const subpaths = path.split('/');
// Part 2 will be the [id] of the submission
const id = subpaths[2];
// Click the "Save for Later" button to save this submission
cy.get('ds-submission-form-footer [data-test="save-for-later"]').click();
// "Save for Later" should send us to MyDSpace
cy.url().should('include', '/mydspace');
// Close any open notifications, to make sure they don't get in the way of next steps
cy.get('[data-dismiss="alert"]').click({multiple: true});
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// On MyDSpace, find the submission we just created via its ID
cy.get('[data-test="search-box"]').type(id);
cy.get('[data-test="search-button"]').click();
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// Click the Edit button for this in-progress submission
cy.get('#edit_' + id).click();
// Should send us back to the submission form
cy.url().should('include', '/workspaceitems/' + id + '/edit');
// Discard our new submission by clicking Discard in Submission form & confirming
cy.get('ds-submission-form-footer [data-test="discard"]').click();
cy.get('button#discard_submit').click();
// Discarding should send us back to MyDSpace
cy.url().should('include', '/mydspace');
});
});
it('should let you import from external sources', () => {
cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Open the New Import dropdown
cy.get('button[data-test="import-dropdown"]').click();
// Click on the "Item" type in that dropdown
cy.get('#importControlsDropdownMenu button[title="none"]').click();
// New URL should include /import-external, as we've moved to the import page
cy.url().should('include', '/import-external');
// The external import searchbox should be visible
cy.get('ds-submission-import-external-searchbar').should('be.visible');
// Test for accessibility issues
testA11y('ds-submission-import-external');
});
}); });

View File

@@ -1,18 +1,18 @@
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('PageNotFound', () => { describe('PageNotFound', () => {
it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => { it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => {
// request an invalid page (UUIDs at root path aren't valid) // request an invalid page (UUIDs at root path aren't valid)
cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false }); cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false });
cy.get('ds-pagenotfound').should('be.visible'); cy.get('ds-pagenotfound').should('be.visible');
// Analyze <ds-pagenotfound> for accessibility issues // Analyze <ds-pagenotfound> for accessibility issues
testA11y('ds-pagenotfound'); testA11y('ds-pagenotfound');
}); });
it('should not contain element ds-pagenotfound when navigating to existing page', () => { it('should not contain element ds-pagenotfound when navigating to existing page', () => {
cy.visit('/home'); cy.visit('/home');
cy.get('ds-pagenotfound').should('not.exist'); cy.get('ds-pagenotfound').should('not.exist');
}); });
}); });

View File

@@ -1,64 +1,64 @@
const page = { const page = {
fillOutQueryInNavBar(query) { fillOutQueryInNavBar(query) {
// Click the magnifying glass // Click the magnifying glass
cy.get('ds-themed-header [data-test="header-search-icon"]').click(); cy.get('ds-themed-header [data-test="header-search-icon"]').click();
// Fill out a query in input that appears // Fill out a query in input that appears
cy.get('ds-themed-header [data-test="header-search-box"]').type(query); cy.get('ds-themed-header [data-test="header-search-box"]').type(query);
}, },
submitQueryByPressingEnter() { submitQueryByPressingEnter() {
cy.get('ds-themed-header [data-test="header-search-box"]').type('{enter}'); cy.get('ds-themed-header [data-test="header-search-box"]').type('{enter}');
}, },
submitQueryByPressingIcon() { submitQueryByPressingIcon() {
cy.get('ds-themed-header [data-test="header-search-icon"]').click(); cy.get('ds-themed-header [data-test="header-search-icon"]').click();
} },
}; };
describe('Search from Navigation Bar', () => { describe('Search from Navigation Bar', () => {
// NOTE: these tests currently assume this query will return results! // NOTE: these tests currently assume this query will return results!
const query = Cypress.env('DSPACE_TEST_SEARCH_TERM'); const query = Cypress.env('DSPACE_TEST_SEARCH_TERM');
it('should go to search page with correct query if submitted (from home)', () => { it('should go to search page with correct query if submitted (from home)', () => {
cy.visit('/'); cy.visit('/');
// This is the GET command that will actually run the search // This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search // Run the search
page.fillOutQueryInNavBar(query); page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter(); page.submitQueryByPressingEnter();
// New URL should include query param // New URL should include query param
cy.url().should('include', 'query='.concat(query)); cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command // Wait for search results to come back from the above GET command
cy.wait('@search-results'); cy.wait('@search-results');
// At least one search result should be displayed // At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible'); cy.get('[data-test="list-object"]').should('be.visible');
}); });
it('should go to search page with correct query if submitted (from search)', () => { it('should go to search page with correct query if submitted (from search)', () => {
cy.visit('/search'); cy.visit('/search');
// This is the GET command that will actually run the search // This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search // Run the search
page.fillOutQueryInNavBar(query); page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter(); page.submitQueryByPressingEnter();
// New URL should include query param // New URL should include query param
cy.url().should('include', 'query='.concat(query)); cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command // Wait for search results to come back from the above GET command
cy.wait('@search-results'); cy.wait('@search-results');
// At least one search result should be displayed // At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible'); cy.get('[data-test="list-object"]').should('be.visible');
}); });
it('should allow user to also submit query by clicking icon', () => { it('should allow user to also submit query by clicking icon', () => {
cy.visit('/'); cy.visit('/');
// This is the GET command that will actually run the search // This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search // Run the search
page.fillOutQueryInNavBar(query); page.fillOutQueryInNavBar(query);
page.submitQueryByPressingIcon(); page.submitQueryByPressingIcon();
// New URL should include query param // New URL should include query param
cy.url().should('include', 'query='.concat(query)); cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command // Wait for search results to come back from the above GET command
cy.wait('@search-results'); cy.wait('@search-results');
// At least one search result should be displayed // At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible'); cy.get('[data-test="list-object"]').should('be.visible');
}); });
}); });

View File

@@ -1,57 +1,57 @@
import { Options } from 'cypress-axe';
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
import { Options } from 'cypress-axe';
describe('Search Page', () => { describe('Search Page', () => {
// NOTE: these tests currently assume this query will return results! // NOTE: these tests currently assume this query will return results!
const query = Cypress.env('DSPACE_TEST_SEARCH_TERM'); const query = Cypress.env('DSPACE_TEST_SEARCH_TERM');
it('should redirect to the correct url when query was set and submit button was triggered', () => { it('should redirect to the correct url when query was set and submit button was triggered', () => {
const queryString = 'Another interesting query string'; const queryString = 'Another interesting query string';
cy.visit('/search'); cy.visit('/search');
// Type query in searchbox & click search button // Type query in searchbox & click search button
cy.get('[data-test="search-box"]').type(queryString); cy.get('[data-test="search-box"]').type(queryString);
cy.get('[data-test="search-button"]').click(); cy.get('[data-test="search-button"]').click();
cy.url().should('include', 'query=' + encodeURI(queryString)); cy.url().should('include', 'query=' + encodeURI(queryString));
}); });
it('should load results and pass accessibility tests', () => { it('should load results and pass accessibility tests', () => {
cy.visit('/search?query='.concat(query)); cy.visit('/search?query='.concat(query));
cy.get('[data-test="search-box"]').should('have.value', query); cy.get('[data-test="search-box"]').should('have.value', query);
// <ds-search-page> tag must be loaded // <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('be.visible'); cy.get('ds-search-page').should('be.visible');
// At least one search result should be displayed // At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible'); cy.get('[data-test="list-object"]').should('be.visible');
// Click each filter toggle to open *every* filter // Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well) // (As we want to scan filter section for accessibility issues as well)
cy.get('[data-test="filter-toggle"]').click({ multiple: true }); cy.get('[data-test="filter-toggle"]').click({ multiple: true });
// Analyze <ds-search-page> for accessibility issues // Analyze <ds-search-page> for accessibility issues
testA11y('ds-search-page'); testA11y('ds-search-page');
}); });
it('should have a working grid view that passes accessibility tests', () => { it('should have a working grid view that passes accessibility tests', () => {
cy.visit('/search?query='.concat(query)); cy.visit('/search?query='.concat(query));
// Click button in sidebar to display grid view // Click button in sidebar to display grid view
cy.get('ds-search-sidebar [data-test="grid-view"]').click(); cy.get('ds-search-sidebar [data-test="grid-view"]').click();
// <ds-search-page> tag must be loaded // <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('be.visible'); cy.get('ds-search-page').should('be.visible');
// At least one grid object (card) should be displayed // At least one grid object (card) should be displayed
cy.get('[data-test="grid-object"]').should('be.visible'); cy.get('[data-test="grid-object"]').should('be.visible');
// Analyze <ds-search-page> for accessibility issues // Analyze <ds-search-page> for accessibility issues
testA11y('ds-search-page', testA11y('ds-search-page',
{ {
rules: { rules: {
// Card titles fail this test currently // Card titles fail this test currently
'heading-order': { enabled: false } 'heading-order': { enabled: false },
} },
} as Options } as Options,
); );
}); });
}); });

View File

@@ -4,224 +4,224 @@ import { Options } from 'cypress-axe';
describe('New Submission page', () => { describe('New Submission page', () => {
// NOTE: We already test that new Item submissions can be started from MyDSpace in my-dspace.spec.ts // NOTE: We already test that new Item submissions can be started from MyDSpace in my-dspace.spec.ts
it('should create a new submission when using /submit path & pass accessibility', () => { it('should create a new submission when using /submit path & pass accessibility', () => {
// Test that calling /submit with collection & entityType will create a new submission // Test that calling /submit with collection & entityType will create a new submission
cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none')); cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none'));
// 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_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Should redirect to /workspaceitems, as we've started a new submission // Should redirect to /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems'); cy.url().should('include', '/workspaceitems');
// The Submission edit form tag should be visible // The Submission edit form tag should be visible
cy.get('ds-submission-edit').should('be.visible'); cy.get('ds-submission-edit').should('be.visible');
// A Collection menu button should exist & it's value should be the selected collection // A Collection menu button should exist & it's value should be the selected collection
cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')); cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME'));
// 4 sections should be visible by default // 4 sections should be visible by default
cy.get('div#section_traditionalpageone').should('be.visible'); cy.get('div#section_traditionalpageone').should('be.visible');
cy.get('div#section_traditionalpagetwo').should('be.visible'); cy.get('div#section_traditionalpagetwo').should('be.visible');
cy.get('div#section_upload').should('be.visible'); cy.get('div#section_upload').should('be.visible');
cy.get('div#section_license').should('be.visible'); cy.get('div#section_license').should('be.visible');
// Test entire page for accessibility // Test entire page for accessibility
testA11y('ds-submission-edit', testA11y('ds-submission-edit',
{ {
rules: { rules: {
// Author & Subject fields have invalid "aria-multiline" attrs. // Author & Subject fields have invalid "aria-multiline" attrs.
// See https://github.com/DSpace/dspace-angular/issues/1272 // See https://github.com/DSpace/dspace-angular/issues/1272
'aria-allowed-attr': { enabled: false }, 'aria-allowed-attr': { enabled: false },
// All panels are accordians & fail "aria-required-children" and "nested-interactive". // 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 // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
'aria-required-children': { enabled: false }, 'aria-required-children': { enabled: false },
'nested-interactive': { enabled: false }, 'nested-interactive': { enabled: false },
// All select boxes fail to have a name / aria-label. // All select boxes fail to have a name / aria-label.
// This is a bug in ng-dynamic-forms and may require https://github.com/DSpace/dspace-angular/issues/2216 // This is a bug in ng-dynamic-forms and may require https://github.com/DSpace/dspace-angular/issues/2216
'select-name': { enabled: false }, 'select-name': { enabled: false },
} },
} as Options } as Options,
); );
// Discard button should work // Discard button should work
// 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
cy.get('button#discard').click(); cy.get('button#discard').click();
cy.get('button#discard_submit').click(); cy.get('button#discard_submit').click();
});
it('should block submission & show errors if required fields are missing', () => {
// Create a new submission
cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none'));
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Attempt an immediate deposit without filling out any fields
cy.get('button#deposit').click();
// A warning alert should display.
cy.get('ds-notification div.alert-success').should('not.exist');
cy.get('ds-notification div.alert-warning').should('be.visible');
// First section should have an exclamation error in the header
// (as it has required fields)
cy.get('div#traditionalpageone-header i.fa-exclamation-circle').should('be.visible');
// Title field should have class "is-invalid" applied, as it's required
cy.get('input#dc_title').should('have.class', 'is-invalid');
// Date Year field should also have "is-valid" class
cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid');
// FINALLY, cleanup after ourselves. This also exercises the MyDSpace delete button.
// Get our Submission URL, to parse out the ID of this submission
cy.location().then(fullUrl => {
// This will be the full path (/workspaceitems/[id]/edit)
const path = fullUrl.pathname;
// Split on the slashes
const subpaths = path.split('/');
// Part 2 will be the [id] of the submission
const id = subpaths[2];
// Even though form is incomplete, the "Save for Later" button should still work
cy.get('button#saveForLater').click();
// "Save for Later" should send us to MyDSpace
cy.url().should('include', '/mydspace');
// A success alert should be visible
cy.get('ds-notification div.alert-success').should('be.visible');
// Now, dismiss any open alert boxes (may be multiple, as tests run quickly)
cy.get('[data-dismiss="alert"]').click({ multiple: true });
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// On MyDSpace, find the submission we just saved via its ID
cy.get('[data-test="search-box"]').type(id);
cy.get('[data-test="search-button"]').click();
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// Delete our created submission & confirm deletion
cy.get('button#delete_' + id).click();
cy.get('button#delete_confirm').click();
});
});
it('should allow for deposit if all required fields completed & file uploaded', () => {
// Create a new submission
cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none'));
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Fill out all required fields (Title, Date)
cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests');
cy.get('input#dc_date_issued_year').type('2022');
// Confirm the required license by checking checkbox
// (NOTE: requires "force:true" cause Cypress claims this checkbox is covered by its own <span>)
cy.get('input#granted').check( { force: true } );
// Before using Cypress drag & drop, we have to manually trigger the "dragover" event.
// This ensures our UI displays the dropzone that covers the entire submission page.
// (For some reason Cypress drag & drop doesn't trigger this even itself & upload won't work without this trigger)
cy.get('ds-uploader').trigger('dragover');
// This is the POST command that will upload the file
cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload');
// Upload our DSpace logo via drag & drop onto submission form
// cy.get('div#section_upload')
cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.png', {
action: 'drag-drop',
}); });
it('should block submission & show errors if required fields are missing', () => { // Wait for upload to complete before proceeding
// Create a new submission cy.wait('@upload');
cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none'));
// This page is restricted, so we will be shown the login form. Fill it out & submit. // Wait for deposit button to not be disabled & click it.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); cy.get('button#deposit').should('not.be.disabled').click();
// Attempt an immediate deposit without filling out any fields // No warnings should exist. Instead, just successful deposit alert is displayed
cy.get('button#deposit').click(); cy.get('ds-notification div.alert-warning').should('not.exist');
cy.get('ds-notification div.alert-success').should('be.visible');
});
// A warning alert should display. it('is possible to submit a new "Person" and that form passes accessibility', () => {
cy.get('ds-notification div.alert-success').should('not.exist'); // To submit a different entity type, we'll start from MyDSpace
cy.get('ds-notification div.alert-warning').should('be.visible'); cy.visit('/mydspace');
// First section should have an exclamation error in the header // This page is restricted, so we will be shown the login form. Fill it out & submit.
// (as it has required fields) // NOTE: At this time, we MUST login as admin to submit Person objects
cy.get('div#traditionalpageone-header i.fa-exclamation-circle').should('be.visible'); cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
// Title field should have class "is-invalid" applied, as it's required // Open the New Submission dropdown
cy.get('input#dc_title').should('have.class', 'is-invalid'); cy.get('button[data-test="submission-dropdown"]').click();
// Click on the "Person" type in that dropdown
cy.get('#entityControlsDropdownMenu button[title="Person"]').click();
// Date Year field should also have "is-valid" class // This should display the <ds-create-item-parent-selector> (popup window)
cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid'); cy.get('ds-create-item-parent-selector').should('be.visible');
// FINALLY, cleanup after ourselves. This also exercises the MyDSpace delete button. // Type in a known Collection name in the search box
// Get our Submission URL, to parse out the ID of this submission cy.get('ds-authorized-collection-selector input[type="search"]').type(Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME'));
cy.location().then(fullUrl => {
// This will be the full path (/workspaceitems/[id]/edit)
const path = fullUrl.pathname;
// Split on the slashes
const subpaths = path.split('/');
// Part 2 will be the [id] of the submission
const id = subpaths[2];
// Even though form is incomplete, the "Save for Later" button should still work // Click on the button matching that known Collection name
cy.get('button#saveForLater').click(); cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME')).concat('"]')).click();
// "Save for Later" should send us to MyDSpace // New URL should include /workspaceitems, as we've started a new submission
cy.url().should('include', '/mydspace'); cy.url().should('include', '/workspaceitems');
// A success alert should be visible // The Submission edit form tag should be visible
cy.get('ds-notification div.alert-success').should('be.visible'); cy.get('ds-submission-edit').should('be.visible');
// Now, dismiss any open alert boxes (may be multiple, as tests run quickly)
cy.get('[data-dismiss="alert"]').click({multiple: true});
// This is the GET command that will actually run the search // A Collection menu button should exist & its value should be the selected collection
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME'));
// On MyDSpace, find the submission we just saved via its ID
cy.get('[data-test="search-box"]').type(id);
cy.get('[data-test="search-button"]').click();
// Wait for search results to come back from the above GET command // 3 sections should be visible by default
cy.wait('@search-results'); cy.get('div#section_personStep').should('be.visible');
cy.get('div#section_upload').should('be.visible');
cy.get('div#section_license').should('be.visible');
// Delete our created submission & confirm deletion // Test entire page for accessibility
cy.get('button#delete_' + id).click(); testA11y('ds-submission-edit',
cy.get('button#delete_confirm').click();
});
});
it('should allow for deposit if all required fields completed & file uploaded', () => {
// Create a new submission
cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none'));
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Fill out all required fields (Title, Date)
cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests');
cy.get('input#dc_date_issued_year').type('2022');
// Confirm the required license by checking checkbox
// (NOTE: requires "force:true" cause Cypress claims this checkbox is covered by its own <span>)
cy.get('input#granted').check( {force: true} );
// Before using Cypress drag & drop, we have to manually trigger the "dragover" event.
// This ensures our UI displays the dropzone that covers the entire submission page.
// (For some reason Cypress drag & drop doesn't trigger this even itself & upload won't work without this trigger)
cy.get('ds-uploader').trigger('dragover');
// This is the POST command that will upload the file
cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload');
// Upload our DSpace logo via drag & drop onto submission form
// cy.get('div#section_upload')
cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.png', {
action: 'drag-drop'
});
// Wait for upload to complete before proceeding
cy.wait('@upload');
// Wait for deposit button to not be disabled & click it.
cy.get('button#deposit').should('not.be.disabled').click();
// No warnings should exist. Instead, just successful deposit alert is displayed
cy.get('ds-notification div.alert-warning').should('not.exist');
cy.get('ds-notification div.alert-success').should('be.visible');
});
it('is possible to submit a new "Person" and that form passes accessibility', () => {
// To submit a different entity type, we'll start from MyDSpace
cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
// NOTE: At this time, we MUST login as admin to submit Person objects
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
// Open the New Submission dropdown
cy.get('button[data-test="submission-dropdown"]').click();
// Click on the "Person" type in that dropdown
cy.get('#entityControlsDropdownMenu button[title="Person"]').click();
// This should display the <ds-create-item-parent-selector> (popup window)
cy.get('ds-create-item-parent-selector').should('be.visible');
// Type in a known Collection name in the search box
cy.get('ds-authorized-collection-selector input[type="search"]').type(Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME'));
// Click on the button matching that known Collection name
cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME')).concat('"]')).click();
// New URL should include /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
// The Submission edit form tag should be visible
cy.get('ds-submission-edit').should('be.visible');
// A Collection menu button should exist & its value should be the selected collection
cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME'));
// 3 sections should be visible by default
cy.get('div#section_personStep').should('be.visible');
cy.get('div#section_upload').should('be.visible');
cy.get('div#section_license').should('be.visible');
// Test entire page for accessibility
testA11y('ds-submission-edit',
{ {
rules: { rules: {
// All panels are accordians & fail "aria-required-children" and "nested-interactive". // 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 // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
'aria-required-children': { enabled: false }, 'aria-required-children': { enabled: false },
'nested-interactive': { enabled: false }, 'nested-interactive': { enabled: false },
} },
} as Options } as Options,
); );
// Click the lookup button next to "Publication" field // Click the lookup button next to "Publication" field
cy.get('button[data-test="lookup-button"]').click(); cy.get('button[data-test="lookup-button"]').click();
// A popup modal window should be visible // A popup modal window should be visible
cy.get('ds-dynamic-lookup-relation-modal').should('be.visible'); cy.get('ds-dynamic-lookup-relation-modal').should('be.visible');
// Popup modal should also pass accessibility tests // Popup modal should also pass accessibility tests
//testA11y('ds-dynamic-lookup-relation-modal'); //testA11y('ds-dynamic-lookup-relation-modal');
testA11y({ testA11y({
include: ['ds-dynamic-lookup-relation-modal'], include: ['ds-dynamic-lookup-relation-modal'],
exclude: [ exclude: [
['ul.nav-tabs'] // Tabs at top of model have several issues which seem to be caused by ng-bootstrap ['ul.nav-tabs'], // Tabs at top of model have several issues which seem to be caused by ng-bootstrap
], ],
});
// Close popup window
cy.get('ds-dynamic-lookup-relation-modal button.close').click();
// Back on the form, click the discard button to remove new submission
// Clicking it will display a confirmation, which we will confirm with another click
cy.get('button#discard').click();
cy.get('button#discard_submit').click();
}); });
// Close popup window
cy.get('ds-dynamic-lookup-relation-modal button.close').click();
// Back on the form, click the discard button to remove new submission
// Clicking it will display a confirmation, which we will confirm with another click
cy.get('button#discard').click();
cy.get('button#discard_submit').click();
});
}); });

View File

@@ -9,51 +9,51 @@ let REST_DOMAIN: string;
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress // Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
// For more info, visit https://on.cypress.io/plugins-api // For more info, visit https://on.cypress.io/plugins-api
module.exports = (on, config) => { module.exports = (on, config) => {
on('task', { on('task', {
// Define "log" and "table" tasks, used for logging accessibility errors during CI // Define "log" and "table" tasks, used for logging accessibility errors during CI
// Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file // Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
log(message: string) { log(message: string) {
console.log(message); console.log(message);
return null; return null;
}, },
table(message: string) { table(message: string) {
console.table(message); console.table(message);
return null; return null;
}, },
// Cypress doesn't have access to the running application in Node.js. // Cypress doesn't have access to the running application in Node.js.
// So, it's not possible to inject or load the AppConfig or environment of the Angular UI. // So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
// Instead, we'll read our running application's config.json, which contains the configs & // Instead, we'll read our running application's config.json, which contains the configs &
// is regenerated at runtime each time the Angular UI application starts up. // is regenerated at runtime each time the Angular UI application starts up.
readUIConfig() { readUIConfig() {
// Check if we have a config.json in the src/assets. If so, use that. // Check if we have a config.json in the src/assets. If so, use that.
// This is where it's written when running "ng e2e" or "yarn serve" // This is where it's written when running "ng e2e" or "yarn serve"
if (fs.existsSync('./src/assets/config.json')) { if (fs.existsSync('./src/assets/config.json')) {
return fs.readFileSync('./src/assets/config.json', 'utf8'); return fs.readFileSync('./src/assets/config.json', 'utf8');
// Otherwise, check the dist/browser/assets // Otherwise, check the dist/browser/assets
// This is where it's written when running "serve:ssr", which is what CI uses to start the frontend // This is where it's written when running "serve:ssr", which is what CI uses to start the frontend
} else if (fs.existsSync('./dist/browser/assets/config.json')) { } else if (fs.existsSync('./dist/browser/assets/config.json')) {
return fs.readFileSync('./dist/browser/assets/config.json', 'utf8'); return fs.readFileSync('./dist/browser/assets/config.json', 'utf8');
} }
return null; return null;
}, },
// Save value of REST Base URL, looked up before all tests. // Save value of REST Base URL, looked up before all tests.
// This allows other tests to use it easily via getRestBaseURL() below. // This allows other tests to use it easily via getRestBaseURL() below.
saveRestBaseURL(url: string) { saveRestBaseURL(url: string) {
return (REST_BASE_URL = url); return (REST_BASE_URL = url);
}, },
// Retrieve currently saved value of REST Base URL // Retrieve currently saved value of REST Base URL
getRestBaseURL() { getRestBaseURL() {
return REST_BASE_URL ; return REST_BASE_URL ;
}, },
// Save value of REST Domain, looked up before all tests. // Save value of REST Domain, looked up before all tests.
// This allows other tests to use it easily via getRestBaseDomain() below. // This allows other tests to use it easily via getRestBaseDomain() below.
saveRestBaseDomain(domain: string) { saveRestBaseDomain(domain: string) {
return (REST_DOMAIN = domain); return (REST_DOMAIN = domain);
}, },
// Retrieve currently saved value of REST Domain // Retrieve currently saved value of REST Domain
getRestBaseDomain() { getRestBaseDomain() {
return REST_DOMAIN ; return REST_DOMAIN ;
} },
}); });
}; };

View File

@@ -3,8 +3,14 @@
// See docs at https://docs.cypress.io/api/cypress-api/custom-commands // See docs at https://docs.cypress.io/api/cypress-api/custom-commands
// *********************************************** // ***********************************************
import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model'; import {
import { DSPACE_XSRF_COOKIE, XSRF_REQUEST_HEADER } from 'src/app/core/xsrf/xsrf.constants'; AuthTokenInfo,
TOKENITEM,
} from 'src/app/core/auth/models/auth-token-info.model';
import {
DSPACE_XSRF_COOKIE,
XSRF_REQUEST_HEADER,
} from 'src/app/core/xsrf/xsrf.constants';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
// Declare Cypress namespace to help with Intellisense & code completion in IDEs // Declare Cypress namespace to help with Intellisense & code completion in IDEs
@@ -57,33 +63,33 @@ declare global {
* @param password password to login as * @param password password to login as
*/ */
function login(email: string, password: string): void { function login(email: string, password: string): void {
// Create a fake CSRF cookie/token to use in POST // Create a fake CSRF cookie/token to use in POST
cy.createCSRFCookie().then((csrfToken: string) => { cy.createCSRFCookie().then((csrfToken: string) => {
// get our REST API's base URL, also needed for POST // get our REST API's base URL, also needed for POST
cy.task('getRestBaseURL').then((baseRestUrl: string) => { cy.task('getRestBaseURL').then((baseRestUrl: string) => {
// Now, send login POST request including that CSRF token // Now, send login POST request including that CSRF token
cy.request({ cy.request({
method: 'POST', method: 'POST',
url: baseRestUrl + '/api/authn/login', url: baseRestUrl + '/api/authn/login',
headers: { [XSRF_REQUEST_HEADER]: csrfToken}, headers: { [XSRF_REQUEST_HEADER]: csrfToken },
form: true, // indicates the body should be form urlencoded form: true, // indicates the body should be form urlencoded
body: { user: email, password: password } body: { user: email, password: password },
}).then((resp) => { }).then((resp) => {
// We expect a successful login // We expect a successful login
expect(resp.status).to.eq(200); expect(resp.status).to.eq(200);
// We expect to have a valid authorization header returned (with our auth token) // We expect to have a valid authorization header returned (with our auth token)
expect(resp.headers).to.have.property('authorization'); expect(resp.headers).to.have.property('authorization');
// Initialize our AuthTokenInfo object from the authorization header. // Initialize our AuthTokenInfo object from the authorization header.
const authheader = resp.headers.authorization as string; const authheader = resp.headers.authorization as string;
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader); const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
// Save our AuthTokenInfo object to our dsAuthInfo UI cookie // Save our AuthTokenInfo object to our dsAuthInfo UI cookie
// This ensures the UI will recognize we are logged in on next "visit()" // This ensures the UI will recognize we are logged in on next "visit()"
cy.setCookie(TOKENITEM, JSON.stringify(authinfo)); cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
}); });
});
}); });
});
} }
// Add as a Cypress command (i.e. assign to 'cy.login') // Add as a Cypress command (i.e. assign to 'cy.login')
Cypress.Commands.add('login', login); Cypress.Commands.add('login', login);
@@ -94,12 +100,12 @@ Cypress.Commands.add('login', login);
* @param password password to login as * @param password password to login as
*/ */
function loginViaForm(email: string, password: string): void { function loginViaForm(email: string, password: string): void {
// Enter email // Enter email
cy.get('ds-log-in [data-test="email"]').type(email); cy.get('ds-log-in [data-test="email"]').type(email);
// Enter password // Enter password
cy.get('ds-log-in [data-test="password"]').type(password); cy.get('ds-log-in [data-test="password"]').type(password);
// Click login button // Click login button
cy.get('ds-log-in [data-test="login-button"]').click(); cy.get('ds-log-in [data-test="login-button"]').click();
} }
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm') // Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
Cypress.Commands.add('loginViaForm', loginViaForm); Cypress.Commands.add('loginViaForm', loginViaForm);
@@ -117,29 +123,29 @@ Cypress.Commands.add('loginViaForm', loginViaForm);
* @param dsoType type of DSpace Object (e.g. "item", "collection", "community") * @param dsoType type of DSpace Object (e.g. "item", "collection", "community")
*/ */
function generateViewEvent(uuid: string, dsoType: string): void { function generateViewEvent(uuid: string, dsoType: string): void {
// Create a fake CSRF cookie/token to use in POST // Create a fake CSRF cookie/token to use in POST
cy.createCSRFCookie().then((csrfToken: string) => { cy.createCSRFCookie().then((csrfToken: string) => {
// get our REST API's base URL, also needed for POST // get our REST API's base URL, also needed for POST
cy.task('getRestBaseURL').then((baseRestUrl: string) => { cy.task('getRestBaseURL').then((baseRestUrl: string) => {
// Now, send 'statistics/viewevents' POST request including that fake CSRF token in required header // Now, send 'statistics/viewevents' POST request including that fake CSRF token in required header
cy.request({ cy.request({
method: 'POST', method: 'POST',
url: baseRestUrl + '/api/statistics/viewevents', url: baseRestUrl + '/api/statistics/viewevents',
headers: { headers: {
[XSRF_REQUEST_HEADER] : csrfToken, [XSRF_REQUEST_HEADER] : csrfToken,
// use a known public IP address to avoid being seen as a "bot" // use a known public IP address to avoid being seen as a "bot"
'X-Forwarded-For': '1.1.1.1', 'X-Forwarded-For': '1.1.1.1',
// Use a user-agent of a Firefox browser on Windows. This again avoids being seen as a "bot" // Use a user-agent of a Firefox browser on Windows. This again avoids being seen as a "bot"
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
}, },
//form: true, // indicates the body should be form urlencoded //form: true, // indicates the body should be form urlencoded
body: { targetId: uuid, targetType: dsoType }, body: { targetId: uuid, targetType: dsoType },
}).then((resp) => { }).then((resp) => {
// We expect a 201 (which means statistics event was created) // We expect a 201 (which means statistics event was created)
expect(resp.status).to.eq(201); expect(resp.status).to.eq(201);
}); });
});
}); });
});
} }
// Add as a Cypress command (i.e. assign to 'cy.generateViewEvent') // Add as a Cypress command (i.e. assign to 'cy.generateViewEvent')
Cypress.Commands.add('generateViewEvent', generateViewEvent); Cypress.Commands.add('generateViewEvent', generateViewEvent);
@@ -153,17 +159,17 @@ Cypress.Commands.add('generateViewEvent', generateViewEvent);
* @returns a Cypress Chainable which can be used to get the generated CSRF Token * @returns a Cypress Chainable which can be used to get the generated CSRF Token
*/ */
function createCSRFCookie(): Cypress.Chainable { function createCSRFCookie(): Cypress.Chainable {
// Generate a new token which is a random UUID // Generate a new token which is a random UUID
const csrfToken: string = uuidv4(); const csrfToken: string = uuidv4();
// Save it to our required cookie // Save it to our required cookie
cy.task('getRestBaseDomain').then((baseDomain: string) => { cy.task('getRestBaseDomain').then((baseDomain: string) => {
// Create a fake CSRF Token. Set it in the required server-side cookie // Create a fake CSRF Token. Set it in the required server-side cookie
cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain }); cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain });
}); });
// return the generated token wrapped in a chainable // return the generated token wrapped in a chainable
return cy.wrap(csrfToken); return cy.wrap(csrfToken);
} }
// Add as a Cypress command (i.e. assign to 'cy.createCSRFCookie') // Add as a Cypress command (i.e. assign to 'cy.createCSRFCookie')
Cypress.Commands.add('createCSRFCookie', createCSRFCookie); Cypress.Commands.add('createCSRFCookie', createCSRFCookie);

View File

@@ -15,10 +15,10 @@
// Import all custom Commands (from commands.ts) for all tests // Import all custom Commands (from commands.ts) for all tests
import './commands'; import './commands';
// Import Cypress Axe tools for all tests // Import Cypress Axe tools for all tests
// https://github.com/component-driven/cypress-axe // https://github.com/component-driven/cypress-axe
import 'cypress-axe'; import 'cypress-axe';
import { DSPACE_XSRF_COOKIE } from 'src/app/core/xsrf/xsrf.constants'; import { DSPACE_XSRF_COOKIE } from 'src/app/core/xsrf/xsrf.constants';
// Runs once before all tests // Runs once before all tests
@@ -34,18 +34,18 @@ before(() => {
// Find URL of our REST API & save to global variable via task // Find URL of our REST API & save to global variable via task
let baseRestUrl = FALLBACK_TEST_REST_BASE_URL; let baseRestUrl = FALLBACK_TEST_REST_BASE_URL;
if (!config.rest.baseUrl) { if (!config.rest.baseUrl) {
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL); console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
} else { } else {
baseRestUrl = config.rest.baseUrl; baseRestUrl = config.rest.baseUrl;
} }
cy.task('saveRestBaseURL', baseRestUrl); cy.task('saveRestBaseURL', baseRestUrl);
// Find domain of our REST API & save to global variable via task. // Find domain of our REST API & save to global variable via task.
let baseDomain = FALLBACK_TEST_REST_DOMAIN; let baseDomain = FALLBACK_TEST_REST_DOMAIN;
if (!config.rest.host) { if (!config.rest.host) {
console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN); console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN);
} else { } else {
baseDomain = config.rest.host; baseDomain = config.rest.host;
} }
cy.task('saveRestBaseDomain', baseDomain); cy.task('saveRestBaseDomain', baseDomain);
@@ -54,12 +54,12 @@ before(() => {
// Runs once before the first test in each "block" // Runs once before the first test in each "block"
beforeEach(() => { beforeEach(() => {
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie // Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
// This just ensures it doesn't get in the way of matching other objects in the page. // This just ensures it doesn't get in the way of matching other objects in the page.
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true%2C%22google-recaptcha%22:true}'); cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true%2C%22google-recaptcha%22:true}');
// Remove any CSRF cookies saved from prior tests // Remove any CSRF cookies saved from prior tests
cy.clearCookie(DSPACE_XSRF_COOKIE); cy.clearCookie(DSPACE_XSRF_COOKIE);
}); });
// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL // NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL

View File

@@ -5,26 +5,26 @@ import { Options } from 'cypress-axe';
// Uses 'log' and 'table' tasks defined in ../plugins/index.ts // Uses 'log' and 'table' tasks defined in ../plugins/index.ts
// Borrowed from https://github.com/component-driven/cypress-axe#in-your-spec-file // Borrowed from https://github.com/component-driven/cypress-axe#in-your-spec-file
function terminalLog(violations: Result[]) { function terminalLog(violations: Result[]) {
cy.task( cy.task(
'log', 'log',
`${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected` `${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected`,
); );
// pluck specific keys to keep the table readable // pluck specific keys to keep the table readable
const violationData = violations.map( const violationData = violations.map(
({ id, impact, description, helpUrl, nodes }) => ({ ({ id, impact, description, helpUrl, nodes }) => ({
id, id,
impact, impact,
description, description,
helpUrl, helpUrl,
nodes: nodes.length, nodes: nodes.length,
html: nodes.map(node => node.html) html: nodes.map(node => node.html),
}) }),
); );
// Print violations as an array, since 'node.html' above often breaks table alignment // Print violations as an array, since 'node.html' above often breaks table alignment
cy.task('log', violationData); cy.task('log', violationData);
// Optionally, uncomment to print as a table // Optionally, uncomment to print as a table
// cy.task('table', violationData); // cy.task('table', violationData);
} }
@@ -32,13 +32,13 @@ function terminalLog(violations: Result[]) {
// while also ensuring any violations are logged to the terminal (see terminalLog above) // while also ensuring any violations are logged to the terminal (see terminalLog above)
// This method MUST be called after cy.visit(), as cy.injectAxe() must be called after page load // This method MUST be called after cy.visit(), as cy.injectAxe() must be called after page load
export const testA11y = (context?: any, options?: Options) => { export const testA11y = (context?: any, options?: Options) => {
cy.injectAxe(); cy.injectAxe();
cy.configureAxe({ cy.configureAxe({
rules: [ rules: [
// Disable color contrast checks as they are inaccurate / result in a lot of false positives // Disable color contrast checks as they are inaccurate / result in a lot of false positives
// See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast // See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast
{ id: 'color-contrast', enabled: false }, { id: 'color-contrast', enabled: false },
] ],
}); });
cy.checkA11y(context, options, terminalLog); cy.checkA11y(context, options, terminalLog);
}; };

View File

@@ -4,10 +4,11 @@
"**/*.ts" "**/*.ts"
], ],
"compilerOptions": { "compilerOptions": {
"sourceMap": false,
"types": [ "types": [
"cypress", "cypress",
"cypress-axe", "cypress-axe",
"node" "node"
] ]
} }
} }

View File

@@ -20,7 +20,7 @@ the Docker compose scripts in this 'docker' folder.
### Dockerfile ### Dockerfile
This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular' 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:latest .
@@ -46,11 +46,11 @@ A default/demo version of this image is built *automatically*.
## 'docker' directory ## 'docker' directory
- docker-compose.yml - docker-compose.yml
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker. - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace REST instance will also be started in Docker.
- docker-compose-rest.yml - docker-compose-rest.yml
- Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes - Runs a published instance of the DSpace REST API - persists data in Docker volumes
- docker-compose-ci.yml - docker-compose-ci.yml
- Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup. - Runs a published instance of the DSpace REST API for CI testing. The database is re-populated from a SQL dump on each startup.
- cli.yml - cli.yml
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container. - Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
- cli.assetstore.yml - cli.assetstore.yml
@@ -71,7 +71,7 @@ docker-compose -f docker/docker-compose.yml build
This command provides a quick way to start both the frontend & backend from this single codebase This command provides a quick way to start both the frontend & backend from this single codebase
``` ```
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d docker-compose -p d8 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
``` ```
Keep in mind, you may also start the backend by cloning the 'DSpace/DSpace' GitHub repository separately. See the next section. Keep in mind, you may also start the backend by cloning the 'DSpace/DSpace' GitHub repository separately. See the next section.
@@ -86,14 +86,14 @@ _The system will be started in 2 steps. Each step shares the same docker network
From 'DSpace/DSpace' clone (build first as needed): From 'DSpace/DSpace' clone (build first as needed):
``` ```
docker-compose -p d7 up -d docker-compose -p d8 up -d
``` ```
NOTE: More detailed instructions on starting the backend via Docker can be found in the [Docker Compose instructions for the Backend](https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md). NOTE: More detailed instructions on starting the backend via Docker can be found in the [Docker Compose instructions for the Backend](https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md).
From 'DSpace/dspace-angular' clone (build first as needed) From 'DSpace/dspace-angular' clone (build first as needed)
``` ```
docker-compose -p d7 -f docker/docker-compose.yml up -d docker-compose -p d8 -f docker/docker-compose.yml up -d
``` ```
At this point, you should be able to access the UI from http://localhost:4000, At this point, you should be able to access the UI from http://localhost:4000,
@@ -107,19 +107,19 @@ This allows you to run the Angular UI in *production* mode, pointing it at the d
``` ```
docker-compose -f docker/docker-compose-dist.yml pull docker-compose -f docker/docker-compose-dist.yml pull
docker-compose -f docker/docker-compose-dist.yml build docker-compose -f docker/docker-compose-dist.yml build
docker-compose -p d7 -f docker/docker-compose-dist.yml up -d docker-compose -p d8 -f docker/docker-compose-dist.yml up -d
``` ```
## Ingest test data from AIPDIR ## Ingest test data from AIPDIR
Create an administrator Create an administrator
``` ```
docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en docker-compose -p d8 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en
``` ```
Load content from AIP files Load content from AIP files
``` ```
docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli docker-compose -p d8 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
``` ```
## Alternative Ingest - Use Entities dataset ## Alternative Ingest - Use Entities dataset
@@ -127,12 +127,12 @@ _Delete your docker volumes or use a unique project (-p) name_
Start DSpace with Database Content from a database dump Start DSpace with Database Content from a database dump
``` ```
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d docker-compose -p d8 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d
``` ```
Load assetstore content and trigger a re-index of the repository Load assetstore content and trigger a re-index of the repository
``` ```
docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli docker-compose -p d8 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
``` ```
## End to end testing of the REST API (runs in GitHub Actions CI). ## End to end testing of the REST API (runs in GitHub Actions CI).
@@ -140,5 +140,5 @@ _In this instance, only the REST api runs in Docker using the Entities dataset.
This command is only really useful for testing our Continuous Integration process. This command is only really useful for testing our Continuous Integration process.
``` ```
docker-compose -p d7ci -f docker/docker-compose-ci.yml up -d docker-compose -p d8ci -f docker/docker-compose-ci.yml up -d
``` ```

View File

@@ -33,6 +33,7 @@ services:
# Tell Statistics to commit all views immediately instead of waiting on Solr's autocommit. # Tell Statistics to commit all views immediately instead of waiting on Solr's autocommit.
# 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
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
depends_on: depends_on:
- dspacedb - dspacedb
@@ -60,15 +61,19 @@ 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}"
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
# 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
LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
PGDATA: /pgdata PGDATA: /pgdata
image: dspace/dspace-postgres-pgcrypto:loadsql POSTGRES_PASSWORD: dspace
networks: networks:
- dspacenet - dspacenet
ports:
- published: 5432
target: 5432
stdin_open: true stdin_open: true
tty: true tty: true
volumes: volumes:
@@ -105,6 +110,8 @@ services:
cp -r /opt/solr/server/solr/configsets/statistics/* statistics cp -r /opt/solr/server/solr/configsets/statistics/* statistics
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
exec solr -f exec solr -f
volumes: volumes:
assetstore: assetstore:

View File

@@ -29,8 +29,9 @@ services:
# __D__ => "-" (e.g. google__D__metadata => google-metadata) # __D__ => "-" (e.g. google__D__metadata => google-metadata)
# dspace.dir, dspace.server.url, dspace.ui.url and dspace.name # dspace.dir, dspace.server.url, dspace.ui.url and dspace.name
dspace__P__dir: /dspace dspace__P__dir: /dspace
dspace__P__server__P__url: http://localhost:8080/server # Uncomment to set a non-default value for dspace.server.url or dspace.ui.url
dspace__P__ui__P__url: http://localhost:4000 # dspace__P__server__P__url: http://localhost:8080/server
# dspace__P__ui__P__url: http://localhost:4000
dspace__P__name: 'DSpace Started with Docker Compose' dspace__P__name: 'DSpace Started with Docker Compose'
# db.url: Ensure we are using the 'dspacedb' image for our database # db.url: Ensure we are using the 'dspacedb' image for our database
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
@@ -39,6 +40,7 @@ services:
# proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests # proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests
# 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
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
depends_on: depends_on:
- dspacedb - dspacedb
@@ -50,6 +52,7 @@ services:
stdin_open: true stdin_open: true
tty: true tty: true
volumes: volumes:
# Keep DSpace assetstore directory between reboots
- assetstore:/dspace/assetstore - assetstore:/dspace/assetstore
# Ensure that the database is ready BEFORE starting tomcat # Ensure that the database is ready BEFORE starting tomcat
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep # 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
@@ -65,9 +68,11 @@ services:
# DSpace database container # DSpace database container
dspacedb: dspacedb:
container_name: dspacedb container_name: dspacedb
# Uses a custom Postgres image with pgcrypto installed
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
environment: environment:
PGDATA: /pgdata PGDATA: /pgdata
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}" POSTGRES_PASSWORD: dspace
networks: networks:
- dspacenet - dspacenet
ports: ports:
@@ -113,6 +118,8 @@ services:
cp -r /opt/solr/server/solr/configsets/statistics/* statistics cp -r /opt/solr/server/solr/configsets/statistics/* statistics
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
exec solr -f exec solr -f
volumes: volumes:
assetstore: assetstore:

View File

@@ -55,28 +55,28 @@
"ts-node": "10.2.1" "ts-node": "10.2.1"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^15.2.8", "@angular/animations": "^17.3.4",
"@angular/cdk": "^15.2.8", "@angular/cdk": "^17.3.4",
"@angular/common": "^15.2.8", "@angular/common": "^17.3.4",
"@angular/compiler": "^15.2.8", "@angular/compiler": "^17.3.4",
"@angular/core": "^15.2.8", "@angular/core": "^17.3.4",
"@angular/forms": "^15.2.8", "@angular/forms": "^17.3.4",
"@angular/localize": "15.2.8", "@angular/localize": "17.3.4",
"@angular/platform-browser": "^15.2.8", "@angular/platform-browser": "^17.3.4",
"@angular/platform-browser-dynamic": "^15.2.8", "@angular/platform-browser-dynamic": "^17.3.4",
"@angular/platform-server": "^15.2.8", "@angular/platform-server": "^17.3.4",
"@angular/router": "^15.2.8", "@angular/router": "^17.3.4",
"@angular/ssr": "^17.3.0",
"@babel/runtime": "7.21.0", "@babel/runtime": "7.21.0",
"@kolkov/ngx-gallery": "^2.0.1", "@kolkov/ngx-gallery": "^2.0.1",
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.11.3", "@material-ui/icons": "^4.11.3",
"@ng-bootstrap/ng-bootstrap": "^11.0.0", "@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@ng-dynamic-forms/core": "^15.0.0", "@ng-dynamic-forms/core": "^16.0.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0", "@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
"@ngrx/effects": "^15.4.0", "@ngrx/effects": "^17.1.1",
"@ngrx/router-store": "^15.4.0", "@ngrx/router-store": "^17.1.1",
"@ngrx/store": "^15.4.0", "@ngrx/store": "^17.1.1",
"@nguniversal/express-engine": "^15.2.1",
"@ngx-translate/core": "^14.0.0", "@ngx-translate/core": "^14.0.0",
"@nicky-lenaers/ngx-scroll-to": "^14.0.0", "@nicky-lenaers/ngx-scroll-to": "^14.0.0",
"@types/grecaptcha": "^3.0.4", "@types/grecaptcha": "^3.0.4",
@@ -94,7 +94,7 @@
"date-fns-tz": "^1.3.7", "date-fns-tz": "^1.3.7",
"deepmerge": "^4.3.1", "deepmerge": "^4.3.1",
"ejs": "^3.1.9", "ejs": "^3.1.9",
"express": "^4.18.2", "express": "^4.19.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",
@@ -110,17 +110,15 @@
"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",
"markdown-it-mathjax3": "^4.3.2",
"mirador": "^3.3.0", "mirador": "^3.3.0",
"mirador-dl-plugin": "^0.13.0", "mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.11.0", "mirador-share-plugin": "^0.11.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ng-mocks": "^14.10.0", "ng-mocks": "^14.10.0",
"ng2-file-upload": "1.4.0", "ng2-file-upload": "5.0.0",
"ng2-nouislider": "^2.0.0", "ng2-nouislider": "^2.0.0",
"ngx-infinite-scroll": "^15.0.0", "ngx-infinite-scroll": "^16.0.0",
"ngx-pagination": "6.0.3", "ngx-pagination": "6.0.3",
"ngx-sortablejs": "^11.1.0",
"ngx-ui-switch": "^14.1.0", "ngx-ui-switch": "^14.1.0",
"nouislider": "^15.7.1", "nouislider": "^15.7.1",
"pem": "1.14.7", "pem": "1.14.7",
@@ -132,24 +130,23 @@
"sortablejs": "1.15.0", "sortablejs": "1.15.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"webfontloader": "1.6.28", "webfontloader": "1.6.28",
"zone.js": "~0.11.5" "zone.js": "~0.14.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "~15.0.0", "@angular-builders/custom-webpack": "~17.0.1",
"@angular-devkit/build-angular": "^15.2.6", "@angular-devkit/build-angular": "^17.3.0",
"@angular-eslint/builder": "15.2.1", "@angular-eslint/builder": "17.2.1",
"@angular-eslint/eslint-plugin": "15.2.1", "@angular-eslint/eslint-plugin": "17.2.1",
"@angular-eslint/eslint-plugin-template": "15.2.1", "@angular-eslint/eslint-plugin-template": "17.2.1",
"@angular-eslint/schematics": "15.2.1", "@angular-eslint/schematics": "17.2.1",
"@angular-eslint/template-parser": "15.2.1", "@angular-eslint/template-parser": "17.2.1",
"@angular/cli": "^16.0.4", "@angular/cli": "^17.3.0",
"@angular/compiler-cli": "^15.2.8", "@angular/compiler-cli": "^17.3.4",
"@angular/language-service": "^15.2.8", "@angular/language-service": "^17.3.4",
"@cypress/schematic": "^1.5.0", "@cypress/schematic": "^1.5.0",
"@fortawesome/fontawesome-free": "^6.4.0", "@fortawesome/fontawesome-free": "^6.4.0",
"@ngrx/store-devtools": "^15.4.0", "@ngrx/store-devtools": "^17.1.1",
"@ngtools/webpack": "^15.2.6", "@ngtools/webpack": "^16.2.12",
"@nguniversal/builders": "^15.2.1",
"@types/deep-freeze": "0.1.2", "@types/deep-freeze": "0.1.2",
"@types/ejs": "^3.1.2", "@types/ejs": "^3.1.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
@@ -161,6 +158,7 @@
"@typescript-eslint/eslint-plugin": "^5.59.1", "@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1", "@typescript-eslint/parser": "^5.59.1",
"axe-core": "^4.7.2", "axe-core": "^4.7.2",
"browser-sync": "^3.0.0",
"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",
@@ -186,7 +184,7 @@
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "^1.5.0",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"ngx-mask": "^13.1.7", "ngx-mask": "14.2.4",
"nodemon": "^2.0.22", "nodemon": "^2.0.22",
"postcss": "^8.4", "postcss": "^8.4",
"postcss-apply": "0.12.0", "postcss-apply": "0.12.0",
@@ -202,10 +200,10 @@
"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": "~4.8.4", "typescript": "~5.3.3",
"webpack": "5.76.1", "webpack": "5.76.1",
"webpack-bundle-analyzer": "^4.8.0", "webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^4.2.0", "webpack-cli": "^4.2.0",
"webpack-dev-server": "^4.13.3" "webpack-dev-server": "^4.13.3"
} }
} }

201
server.ts
View File

@@ -17,7 +17,6 @@
import 'zone.js/node'; import 'zone.js/node';
import 'reflect-metadata'; import 'reflect-metadata';
import 'rxjs';
/* eslint-disable import/no-namespace */ /* eslint-disable import/no-namespace */
import * as morgan from 'morgan'; import * as morgan from 'morgan';
@@ -39,23 +38,26 @@ import { join } from 'path';
import { enableProdMode } from '@angular/core'; import { enableProdMode } from '@angular/core';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
import { environment } from './src/environments/environment'; import { environment } from './src/environments/environment';
import { createProxyMiddleware } from 'http-proxy-middleware'; import { createProxyMiddleware } from 'http-proxy-middleware';
import { hasNoValue, hasValue } from './src/app/shared/empty.util'; import { hasValue } from './src/app/shared/empty.util';
import { UIServerConfig } from './src/config/ui-server-config.interface'; import { UIServerConfig } from './src/config/ui-server-config.interface';
import bootstrap from './src/main.server';
import { ServerAppModule } from './src/main.server';
import { buildAppConfig } from './src/config/config.server'; import { buildAppConfig } from './src/config/config.server';
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface'; import {
APP_CONFIG,
AppConfig,
} from './src/config/app-config.interface';
import { extendEnvironmentWithAppConfig } from './src/config/config.util'; import { extendEnvironmentWithAppConfig } from './src/config/config.util';
import { logStartupMessage } from './startup-message'; import { logStartupMessage } from './startup-message';
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model'; import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
import { CommonEngine } from '@angular/ssr';
import { APP_BASE_HREF } from '@angular/common';
import {
REQUEST,
RESPONSE,
} from './src/express.tokens';
/* /*
* Set path for the browser application's dist folder * Set path for the browser application's dist folder
@@ -127,27 +129,6 @@ export function app() {
*/ */
server.use(json()); server.use(json());
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', (_, options, callback) =>
ngExpressEngine({
bootstrap: ServerAppModule,
providers: [
{
provide: REQUEST,
useValue: (options as any).req,
},
{
provide: RESPONSE,
useValue: (options as any).req.res,
},
{
provide: APP_CONFIG,
useValue: environment
}
]
})(_, (options as any), callback)
);
server.engine('ejs', ejs.renderFile); server.engine('ejs', ejs.renderFile);
/* /*
@@ -162,7 +143,7 @@ export function app() {
server.get('/robots.txt', (req, res) => { server.get('/robots.txt', (req, res) => {
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.render('assets/robots.txt.ejs', { res.render('assets/robots.txt.ejs', {
'origin': req.protocol + '://' + req.headers.host 'origin': req.protocol + '://' + req.headers.host,
}); });
}); });
@@ -177,7 +158,7 @@ export function app() {
router.use('/sitemap**', createProxyMiddleware({ router.use('/sitemap**', createProxyMiddleware({
target: `${environment.rest.baseUrl}/sitemaps`, target: `${environment.rest.baseUrl}/sitemaps`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true changeOrigin: true,
})); }));
/** /**
@@ -186,7 +167,7 @@ export function app() {
router.use('/signposting**', createProxyMiddleware({ router.use('/signposting**', createProxyMiddleware({
target: `${environment.rest.baseUrl}`, target: `${environment.rest.baseUrl}`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true changeOrigin: true,
})); }));
/** /**
@@ -197,7 +178,7 @@ export function app() {
const RateLimit = require('express-rate-limit'); const RateLimit = require('express-rate-limit');
const limiter = new RateLimit({ const limiter = new RateLimit({
windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs, windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs,
max: (environment.ui as UIServerConfig).rateLimiter.max max: (environment.ui as UIServerConfig).rateLimiter.max,
}); });
server.use(limiter); server.use(limiter);
} }
@@ -236,10 +217,10 @@ export function app() {
/* /*
* The callback function to serve server side angular * The callback function to serve server side angular
*/ */
function ngApp(req, res) { function ngApp(req, res, next) {
if (environment.universal.preboot) { if (environment.ssr.enabled) {
// Render the page to user via SSR (server side rendering) // Render the page to user via SSR (server side rendering)
serverSideRender(req, res); serverSideRender(req, res, next);
} else { } else {
// If preboot is disabled, just serve the client // If preboot is disabled, just serve the client
console.log('Universal off, serving for direct client-side rendering (CSR)'); console.log('Universal off, serving for direct client-side rendering (CSR)');
@@ -252,45 +233,66 @@ function ngApp(req, res) {
* returned to the user. * returned to the user.
* @param req current request * @param req current request
* @param res current response * @param res current response
* @param next the next function
* @param sendToUser if true (default), send the rendered content to the user. * @param sendToUser if true (default), send the rendered content to the user.
* If false, then only save this rendered content to the in-memory cache (to refresh cache). * If false, then only save this rendered content to the in-memory cache (to refresh cache).
*/ */
function serverSideRender(req, res, sendToUser: boolean = true) { function serverSideRender(req, res, next, sendToUser: boolean = true) {
const { protocol, originalUrl, baseUrl, headers } = req;
const commonEngine = new CommonEngine({ enablePerformanceProfiler: environment.ssr.enablePerformanceProfiler });
// Render the page via SSR (server side rendering) // Render the page via SSR (server side rendering)
res.render(indexHtml, { commonEngine
req, .render({
res, bootstrap,
preboot: environment.universal.preboot, documentFilePath: indexHtml,
async: environment.universal.async, inlineCriticalCss: environment.ssr.inlineCriticalCss,
time: environment.universal.time, url: `${protocol}://${headers.host}${originalUrl}`,
baseUrl: environment.ui.nameSpace, publicPath: DIST_FOLDER,
originUrl: environment.ui.baseUrl, providers: [
requestUrl: req.originalUrl, { provide: APP_BASE_HREF, useValue: baseUrl },
}, (err, data) => { {
if (hasNoValue(err) && hasValue(data)) { provide: REQUEST,
// save server side rendered page to cache (if any are enabled) useValue: req,
saveToCache(req, data); },
if (sendToUser) { {
res.locals.ssr = true; // mark response as SSR (enables text compression) provide: RESPONSE,
// send rendered page to user useValue: res,
res.send(data); },
{
provide: APP_CONFIG,
useValue: environment,
},
],
})
.then((html) => {
if (hasValue(html)) {
// save server side rendered page to cache (if any are enabled)
saveToCache(req, html);
if (sendToUser) {
res.locals.ssr = true; // mark response as SSR (enables text compression)
// send rendered page to user
res.send(html);
}
} }
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') { })
// When this error occurs we can't fall back to CSR because the response has already been .catch((err) => {
// sent. These errors occur for various reasons in universal, not all of which are in our if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
// control to solve. // When this error occurs we can't fall back to CSR because the response has already been
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client'); // sent. These errors occur for various reasons in universal, not all of which are in our
} else { // control to solve.
console.warn('Error in server-side rendering (SSR)'); console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
if (hasValue(err)) { } else {
console.warn('Error details : ', err); console.warn('Error in server-side rendering (SSR)');
if (hasValue(err)) {
console.warn('Error details : ', err);
}
if (sendToUser) {
console.warn('Falling back to serving direct client-side rendering (CSR).');
clientSideRender(req, res);
}
} }
if (sendToUser) { next(err);
console.warn('Falling back to serving direct client-side rendering (CSR).'); });
clientSideRender(req, res);
}
}
});
} }
/** /**
@@ -325,7 +327,7 @@ function initCache() {
botCache = new LRU( { botCache = new LRU( {
max: environment.cache.serverSide.botCache.max, max: environment.cache.serverSide.botCache.max,
ttl: environment.cache.serverSide.botCache.timeToLive, ttl: environment.cache.serverSide.botCache.timeToLive,
allowStale: environment.cache.serverSide.botCache.allowStale allowStale: environment.cache.serverSide.botCache.allowStale,
}); });
} }
@@ -337,7 +339,7 @@ function initCache() {
anonymousCache = new LRU( { anonymousCache = new LRU( {
max: environment.cache.serverSide.anonymousCache.max, max: environment.cache.serverSide.anonymousCache.max,
ttl: environment.cache.serverSide.anonymousCache.timeToLive, ttl: environment.cache.serverSide.anonymousCache.timeToLive,
allowStale: environment.cache.serverSide.anonymousCache.allowStale allowStale: environment.cache.serverSide.anonymousCache.allowStale,
}); });
} }
} }
@@ -348,7 +350,7 @@ function initCache() {
function botCacheEnabled(): boolean { function botCacheEnabled(): boolean {
// Caching is only enabled if SSR is enabled AND // Caching is only enabled if SSR is enabled AND
// "max" pages to cache is greater than zero // "max" pages to cache is greater than zero
return environment.universal.preboot && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0); return environment.ssr.enabled && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0);
} }
/** /**
@@ -357,7 +359,7 @@ function botCacheEnabled(): boolean {
function anonymousCacheEnabled(): boolean { function anonymousCacheEnabled(): boolean {
// Caching is only enabled if SSR is enabled AND // Caching is only enabled if SSR is enabled AND
// "max" pages to cache is greater than zero // "max" pages to cache is greater than zero
return environment.universal.preboot && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0); return environment.ssr.enabled && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0);
} }
/** /**
@@ -370,9 +372,9 @@ function cacheCheck(req, res, next) {
// If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page. // If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page.
if (botCacheEnabled() && isbot(req.get('user-agent'))) { if (botCacheEnabled() && isbot(req.get('user-agent'))) {
cachedCopy = checkCacheForRequest('bot', botCache, req, res); cachedCopy = checkCacheForRequest('bot', botCache, req, res, next);
} else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) { } else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) {
cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res); cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res, next);
} }
// If cached copy exists, return it to the user. // If cached copy exists, return it to the user.
@@ -408,14 +410,15 @@ function cacheCheck(req, res, next) {
* @param cache LRU cache to check * @param cache LRU cache to check
* @param req current request to look for in the cache * @param req current request to look for in the cache
* @param res current response * @param res current response
* @param next the next function
* @returns cached copy (if found) or undefined (if not found) * @returns cached copy (if found) or undefined (if not found)
*/ */
function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, res): any { function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, res, next): any {
// Get the cache key for this request // Get the cache key for this request
const key = getCacheKey(req); const key = getCacheKey(req);
// Check if this page is in our cache // Check if this page is in our cache
let cachedCopy = cache.get(key); const cachedCopy = cache.get(key);
if (cachedCopy) { if (cachedCopy) {
if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); } if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); }
@@ -426,7 +429,7 @@ function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, r
// Update cached copy by rerendering server-side // Update cached copy by rerendering server-side
// NOTE: In this scenario the currently cached copy will be returned to the current user. // NOTE: In this scenario the currently cached copy will be returned to the current user.
// This re-render is peformed behind the scenes to update cached copy for next user. // This re-render is peformed behind the scenes to update cached copy for next user.
serverSideRender(req, res, false); serverSideRender(req, res, next, false);
} }
} else { } else {
if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); } if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); }
@@ -529,20 +532,20 @@ function serverStarted() {
function createHttpsServer(keys) { function createHttpsServer(keys) {
const listener = createServer({ const listener = createServer({
key: keys.serviceKey, key: keys.serviceKey,
cert: keys.certificate cert: keys.certificate,
}, app).listen(environment.ui.port, environment.ui.host, () => { }, app()).listen(environment.ui.port, environment.ui.host, () => {
serverStarted(); serverStarted();
}); });
// Graceful shutdown when signalled // Graceful shutdown when signalled
const terminator = createHttpTerminator({server: listener}); const terminator = createHttpTerminator({ server: listener });
process.on('SIGINT', () => { process.on('SIGINT', () => {
void (async ()=> { void (async ()=> {
console.debug('Closing HTTPS server on signal'); console.debug('Closing HTTPS server on signal');
await terminator.terminate().catch(e => { console.error(e); }); await terminator.terminate().catch(e => { console.error(e); });
console.debug('HTTPS server closed'); console.debug('HTTPS server closed');
})(); })();
}); });
} }
/** /**
@@ -559,14 +562,14 @@ function run() {
}); });
// Graceful shutdown when signalled // Graceful shutdown when signalled
const terminator = createHttpTerminator({server: listener}); const terminator = createHttpTerminator({ server: listener });
process.on('SIGINT', () => { process.on('SIGINT', () => {
void (async () => { void (async () => {
console.debug('Closing HTTP server on signal'); console.debug('Closing HTTP server on signal');
await terminator.terminate().catch(e => { console.error(e); }); await terminator.terminate().catch(e => { console.error(e); });
console.debug('HTTP server closed.');return undefined; console.debug('HTTP server closed.');return undefined;
})(); })();
}); });
} }
function start() { function start() {
@@ -597,7 +600,7 @@ function start() {
if (serviceKey && certificate) { if (serviceKey && certificate) {
createHttpsServer({ createHttpsServer({
serviceKey: serviceKey, serviceKey: serviceKey,
certificate: certificate certificate: certificate,
}); });
} else { } else {
console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.'); console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.');
@@ -606,7 +609,7 @@ function start() {
createCertificate({ createCertificate({
days: 1, days: 1,
selfSigned: true selfSigned: true,
}, (error, keys) => { }, (error, keys) => {
createHttpsServer(keys); createHttpsServer(keys);
}); });
@@ -627,7 +630,7 @@ function healthCheck(req, res) {
}) })
.catch((error) => { .catch((error) => {
res.status(error.response.status).send({ res.status(error.response.status).send({
error: error.message error: error.message,
}); });
}); });
} }

View File

@@ -0,0 +1,117 @@
import { AbstractControl } from '@angular/forms';
import {
mapToCanActivate,
Route,
} from '@angular/router';
import {
DYNAMIC_ERROR_MESSAGES_MATCHER,
DynamicErrorMessagesMatcher,
} from '@ng-dynamic-forms/core';
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import {
EPERSON_PATH,
GROUP_PATH,
} from './access-control-routing-paths';
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
import { EPersonResolver } from './epeople-registry/eperson-resolver.service';
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
import { GroupPageGuard } from './group-registry/group-page.guard';
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
/**
* Condition for displaying error messages on email form field
*/
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
(control: AbstractControl, model: any, hasFocus: boolean) => {
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
};
const providers = [
{
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
useValue: ValidateEmailErrorStateMatcher,
},
];
export const ROUTES: Route[] = [
{
path: EPERSON_PATH,
component: EPeopleRegistryComponent,
resolve: {
breadcrumb: i18nBreadcrumbResolver,
},
providers,
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' },
canActivate: mapToCanActivate([SiteAdministratorGuard]),
},
{
path: `${EPERSON_PATH}/create`,
component: EPersonFormComponent,
resolve: {
breadcrumb: i18nBreadcrumbResolver,
},
providers,
data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' },
canActivate: mapToCanActivate([SiteAdministratorGuard]),
},
{
path: `${EPERSON_PATH}/:id/edit`,
component: EPersonFormComponent,
resolve: {
breadcrumb: i18nBreadcrumbResolver,
ePerson: EPersonResolver,
},
providers,
data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' },
canActivate: mapToCanActivate([SiteAdministratorGuard]),
},
{
path: GROUP_PATH,
component: GroupsRegistryComponent,
resolve: {
breadcrumb: i18nBreadcrumbResolver,
},
providers,
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' },
canActivate: mapToCanActivate([GroupAdministratorGuard]),
},
{
path: `${GROUP_PATH}/create`,
component: GroupFormComponent,
resolve: {
breadcrumb: i18nBreadcrumbResolver,
},
providers,
data: {
title: 'admin.access-control.groups.title.addGroup',
breadcrumbKey: 'admin.access-control.groups.addGroup',
},
canActivate: mapToCanActivate([GroupAdministratorGuard]),
},
{
path: `${GROUP_PATH}/:groupId/edit`,
component: GroupFormComponent,
resolve: {
breadcrumb: i18nBreadcrumbResolver,
},
providers,
data: {
title: 'admin.access-control.groups.title.singleGroup',
breadcrumbKey: 'admin.access-control.groups.singleGroup',
},
canActivate: mapToCanActivate([GroupPageGuard]),
},
{
path: 'bulk-access',
component: BulkAccessComponent,
resolve: {
breadcrumb: i18nBreadcrumbResolver,
},
data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' },
canActivate: mapToCanActivate([SiteAdministratorGuard]),
},
];

View File

@@ -1,94 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import {
EPERSON_PATH,
GROUP_PATH,
} from './access-control-routing-paths';
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
import { EPersonResolver } from './epeople-registry/eperson-resolver.service';
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
import { GroupPageGuard } from './group-registry/group-page.guard';
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
@NgModule({
imports: [
RouterModule.forChild([
{
path: EPERSON_PATH,
component: EPeopleRegistryComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver,
},
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' },
canActivate: [SiteAdministratorGuard],
},
{
path: `${EPERSON_PATH}/create`,
component: EPersonFormComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver,
},
data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' },
canActivate: [SiteAdministratorGuard],
},
{
path: `${EPERSON_PATH}/:id/edit`,
component: EPersonFormComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver,
ePerson: EPersonResolver,
},
data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' },
canActivate: [SiteAdministratorGuard],
},
{
path: GROUP_PATH,
component: GroupsRegistryComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver,
},
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' },
canActivate: [GroupAdministratorGuard],
},
{
path: `${GROUP_PATH}/create`,
component: GroupFormComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver,
},
data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup' },
canActivate: [GroupAdministratorGuard],
},
{
path: `${GROUP_PATH}/:groupId/edit`,
component: GroupFormComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver,
},
data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' },
canActivate: [GroupPageGuard],
},
{
path: 'bulk-access',
component: BulkAccessComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver,
},
data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' },
canActivate: [SiteAdministratorGuard],
},
]),
],
})
/**
* Routing module for the AccessControl section of the admin sidebar
*/
export class AccessControlRoutingModule {
}

View File

@@ -1,71 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
import {
DYNAMIC_ERROR_MESSAGES_MATCHER,
DynamicErrorMessagesMatcher,
} from '@ng-dynamic-forms/core';
import { AccessControlFormModule } from '../shared/access-control-form-container/access-control-form.module';
import { FormModule } from '../shared/form/form.module';
import { SearchModule } from '../shared/search/search.module';
import { SharedModule } from '../shared/shared.module';
import { AccessControlRoutingModule } from './access-control-routing.module';
import { BulkAccessBrowseComponent } from './bulk-access/browse/bulk-access-browse.component';
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
import { BulkAccessSettingsComponent } from './bulk-access/settings/bulk-access-settings.component';
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
import { MembersListComponent } from './group-registry/group-form/members-list/members-list.component';
import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component';
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
/**
* Condition for displaying error messages on email form field
*/
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
(control: AbstractControl, model: any, hasFocus: boolean) => {
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
};
@NgModule({
imports: [
CommonModule,
SharedModule,
RouterModule,
AccessControlRoutingModule,
FormModule,
NgbAccordionModule,
SearchModule,
AccessControlFormModule,
],
exports: [
MembersListComponent,
],
declarations: [
EPeopleRegistryComponent,
EPersonFormComponent,
GroupsRegistryComponent,
GroupFormComponent,
SubgroupsListComponent,
MembersListComponent,
BulkAccessComponent,
BulkAccessBrowseComponent,
BulkAccessSettingsComponent,
],
providers: [
{
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
useValue: ValidateEmailErrorStateMatcher,
},
],
})
/**
* This module handles all components related to the access control pages
*/
export class AccessControlModule {
}

View File

@@ -37,7 +37,6 @@
<ng-template ngbNavContent> <ng-template ngbNavContent>
<ds-pagination <ds-pagination
[paginationOptions]="(paginationOptions$ | async)" [paginationOptions]="(paginationOptions$ | async)"
[pageInfoState]="(objectsSelected$|async)?.payload.pageInfo"
[collectionSize]="(objectsSelected$|async)?.payload?.totalElements" [collectionSize]="(objectsSelected$|async)?.payload?.totalElements"
[objects]="(objectsSelected$|async)" [objects]="(objectsSelected$|async)"
[showPaginator]="false" [showPaginator]="false"

View File

@@ -13,9 +13,15 @@ import { of } from 'rxjs';
import { buildPaginatedList } from '../../../core/data/paginated-list.model'; import { buildPaginatedList } from '../../../core/data/paginated-list.model';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { getMockThemeService } from '../../../shared/mocks/theme-service.mock';
import { ListableObjectComponentLoaderComponent } from '../../../shared/object-collection/shared/listable-object/listable-object-component-loader.component';
import { SelectableListItemControlComponent } from '../../../shared/object-collection/shared/selectable-list-item-control/selectable-list-item-control.component';
import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service';
import { SelectableObject } from '../../../shared/object-list/selectable-list/selectable-list.service.spec'; import { SelectableObject } from '../../../shared/object-list/selectable-list/selectable-list.service.spec';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { ThemedSearchComponent } from '../../../shared/search/themed-search.component';
import { ThemeService } from '../../../shared/theme-support/theme.service';
import { BulkAccessBrowseComponent } from './bulk-access-browse.component'; import { BulkAccessBrowseComponent } from './bulk-access-browse.component';
describe('BulkAccessBrowseComponent', () => { describe('BulkAccessBrowseComponent', () => {
@@ -29,7 +35,7 @@ describe('BulkAccessBrowseComponent', () => {
const selected1 = new SelectableObject(value1); const selected1 = new SelectableObject(value1);
const selected2 = new SelectableObject(value2); const selected2 = new SelectableObject(value2);
const testSelection = { id: listID1, selection: [selected1, selected2] } ; const testSelection = { id: listID1, selection: [selected1, selected2] };
const selectableListService = jasmine.createSpyObj('SelectableListService', ['getSelectableList', 'deselectAll']); const selectableListService = jasmine.createSpyObj('SelectableListService', ['getSelectableList', 'deselectAll']);
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
@@ -38,13 +44,27 @@ describe('BulkAccessBrowseComponent', () => {
NgbAccordionModule, NgbAccordionModule,
NgbNavModule, NgbNavModule,
TranslateModule.forRoot(), TranslateModule.forRoot(),
BulkAccessBrowseComponent,
],
providers: [
{ provide: SelectableListService, useValue: selectableListService },
{ provide: ThemeService, useValue: getMockThemeService() },
], ],
declarations: [BulkAccessBrowseComponent],
providers: [ { provide: SelectableListService, useValue: selectableListService } ],
schemas: [ schemas: [
NO_ERRORS_SCHEMA, NO_ERRORS_SCHEMA,
], ],
}).compileComponents(); })
.overrideComponent(BulkAccessBrowseComponent, {
remove: {
imports: [
PaginationComponent,
ThemedSearchComponent,
SelectableListItemControlComponent,
ListableObjectComponentLoaderComponent,
],
},
})
.compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
@@ -79,7 +99,7 @@ describe('BulkAccessBrowseComponent', () => {
'totalElements': 2, 'totalElements': 2,
'totalPages': 1, 'totalPages': 1,
'currentPage': 1, 'currentPage': 1,
}), [selected1, selected2]) ; }), [selected1, selected2]);
const rd = createSuccessfulRemoteDataObject(list); const rd = createSuccessfulRemoteDataObject(list);
expect(component.objectsSelected$.value).toEqual(rd); expect(component.objectsSelected$.value).toEqual(rd);

View File

@@ -1,9 +1,20 @@
import {
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
} from '@angular/core'; } from '@angular/core';
import {
NgbAccordionModule,
NgbNavModule,
} from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { NgxPaginationModule } from 'ngx-pagination';
import { import {
BehaviorSubject, BehaviorSubject,
Subscription, Subscription,
@@ -20,13 +31,18 @@ import {
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-configuration.service';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { ListableObjectComponentLoaderComponent } from '../../../shared/object-collection/shared/listable-object/listable-object-component-loader.component';
import { SelectableListItemControlComponent } from '../../../shared/object-collection/shared/selectable-list-item-control/selectable-list-item-control.component';
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 { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { ThemedSearchComponent } from '../../../shared/search/themed-search.component';
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
@Component({ @Component({
selector: 'ds-bulk-access-browse', selector: 'ds-bulk-access-browse',
@@ -38,6 +54,21 @@ import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.ut
useClass: SearchConfigurationService, useClass: SearchConfigurationService,
}, },
], ],
imports: [
PaginationComponent,
AsyncPipe,
NgbAccordionModule,
TranslateModule,
NgIf,
NgbNavModule,
ThemedSearchComponent,
BrowserOnlyPipe,
NgForOf,
NgxPaginationModule,
SelectableListItemControlComponent,
ListableObjectComponentLoaderComponent,
],
standalone: true,
}) })
export class BulkAccessBrowseComponent implements OnInit, OnDestroy { export class BulkAccessBrowseComponent implements OnInit, OnDestroy {

View File

@@ -9,12 +9,15 @@ import { of } from 'rxjs';
import { Process } from '../../process-page/processes/process.model'; import { Process } from '../../process-page/processes/process.model';
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 { getMockThemeService } from '../../shared/mocks/theme-service.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
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 { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { ThemeService } from '../../shared/theme-support/theme.service';
import { BulkAccessComponent } from './bulk-access.component'; import { BulkAccessComponent } from './bulk-access.component';
import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component';
describe('BulkAccessComponent', () => { describe('BulkAccessComponent', () => {
let component: BulkAccessComponent; let component: BulkAccessComponent;
@@ -74,15 +77,23 @@ describe('BulkAccessComponent', () => {
imports: [ imports: [
RouterTestingModule, RouterTestingModule,
TranslateModule.forRoot(), TranslateModule.forRoot(),
BulkAccessComponent,
], ],
declarations: [ BulkAccessComponent ],
providers: [ providers: [
{ provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock }, { provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock },
{ provide: NotificationsService, useValue: NotificationsServiceStub }, { provide: NotificationsService, useValue: NotificationsServiceStub },
{ provide: SelectableListService, useValue: selectableListServiceMock }, { provide: SelectableListService, useValue: selectableListServiceMock },
{ provide: ThemeService, useValue: getMockThemeService() },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}) })
.overrideComponent(BulkAccessComponent, {
remove: {
imports: [
BulkAccessSettingsComponent,
],
},
})
.compileComponents(); .compileComponents();
}); });

View File

@@ -3,6 +3,7 @@ import {
OnInit, OnInit,
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { import {
BehaviorSubject, BehaviorSubject,
Subscription, Subscription,
@@ -15,12 +16,19 @@ import {
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 { 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 { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component'; import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component';
@Component({ @Component({
selector: 'ds-bulk-access', selector: 'ds-bulk-access',
templateUrl: './bulk-access.component.html', templateUrl: './bulk-access.component.html',
styleUrls: ['./bulk-access.component.scss'], styleUrls: ['./bulk-access.component.scss'],
imports: [
TranslateModule,
BulkAccessSettingsComponent,
BulkAccessBrowseComponent,
],
standalone: true,
}) })
export class BulkAccessComponent implements OnInit { export class BulkAccessComponent implements OnInit {

View File

@@ -6,6 +6,7 @@ import {
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { AccessControlFormContainerComponent } from '../../../shared/access-control-form-container/access-control-form-container.component';
import { BulkAccessSettingsComponent } from './bulk-access-settings.component'; import { BulkAccessSettingsComponent } from './bulk-access-settings.component';
describe('BulkAccessSettingsComponent', () => { describe('BulkAccessSettingsComponent', () => {
@@ -45,10 +46,13 @@ describe('BulkAccessSettingsComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NgbAccordionModule, TranslateModule.forRoot()], imports: [NgbAccordionModule, TranslateModule.forRoot(), BulkAccessSettingsComponent],
declarations: [BulkAccessSettingsComponent],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); })
.overrideComponent(BulkAccessSettingsComponent, {
remove: { imports: [AccessControlFormContainerComponent] },
})
.compileComponents();
}); });
beforeEach(() => { beforeEach(() => {

View File

@@ -1,7 +1,10 @@
import { NgIf } from '@angular/common';
import { import {
Component, Component,
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { AccessControlFormContainerComponent } from '../../../shared/access-control-form-container/access-control-form-container.component'; import { AccessControlFormContainerComponent } from '../../../shared/access-control-form-container/access-control-form-container.component';
@@ -10,6 +13,13 @@ import { AccessControlFormContainerComponent } from '../../../shared/access-cont
templateUrl: 'bulk-access-settings.component.html', templateUrl: 'bulk-access-settings.component.html',
styleUrls: ['./bulk-access-settings.component.scss'], styleUrls: ['./bulk-access-settings.component.scss'],
exportAs: 'dsBulkSettings', exportAs: 'dsBulkSettings',
imports: [
NgbAccordionModule,
TranslateModule,
NgIf,
AccessControlFormContainerComponent,
],
standalone: true,
}) })
export class BulkAccessSettingsComponent { export class BulkAccessSettingsComponent {

View File

@@ -45,7 +45,6 @@
<ds-pagination <ds-pagination
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (searching$ | async) !== true" *ngIf="(pageInfoState$ | async)?.totalElements > 0 && (searching$ | async) !== true"
[paginationOptions]="config" [paginationOptions]="config"
[pageInfoState]="pageInfoState$"
[collectionSize]="(pageInfoState$ | async)?.totalElements" [collectionSize]="(pageInfoState$ | async)?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">

View File

@@ -19,6 +19,7 @@ import {
By, By,
} from '@angular/platform-browser'; } from '@angular/platform-browser';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { import {
NgbModal, NgbModal,
NgbModule, NgbModule,
@@ -42,8 +43,11 @@ 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 { 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 { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock';
import { RouterMock } from '../../shared/mocks/router.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { import {
EPersonMock, EPersonMock,
@@ -51,8 +55,8 @@ import {
} from '../../shared/testing/eperson.mock'; } from '../../shared/testing/eperson.mock';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { RouterStub } from '../../shared/testing/router.stub';
import { EPeopleRegistryComponent } from './epeople-registry.component'; import { EPeopleRegistryComponent } from './epeople-registry.component';
import { EPersonFormComponent } from './eperson-form/eperson-form.component';
describe('EPeopleRegistryComponent', () => { describe('EPeopleRegistryComponent', () => {
let component: EPeopleRegistryComponent; let component: EPeopleRegistryComponent;
@@ -145,22 +149,30 @@ describe('EPeopleRegistryComponent', () => {
builderService = getMockFormBuilderService(); builderService = getMockFormBuilderService();
paginationService = new PaginationServiceStub(); paginationService = new PaginationServiceStub();
await TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule.withRoutes([]),
TranslateModule.forRoot(), TranslateModule.forRoot(), EPeopleRegistryComponent],
],
declarations: [EPeopleRegistryComponent],
providers: [ providers: [
{ provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: AuthorizationDataService, useValue: authorizationService }, { provide: AuthorizationDataService, useValue: authorizationService },
{ provide: FormBuilderService, useValue: builderService }, { provide: FormBuilderService, useValue: builderService },
{ provide: Router, useValue: new RouterStub() }, { provide: Router, useValue: new RouterMock() },
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) }, { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); })
.overrideComponent(EPeopleRegistryComponent, {
remove: {
imports: [
EPersonFormComponent,
ThemedLoadingComponent,
PaginationComponent,
],
},
})
.compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {

View File

@@ -1,12 +1,27 @@
import {
AsyncPipe,
NgClass,
NgForOf,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
OnDestroy, OnDestroy,
OnInit, OnInit,
} from '@angular/core'; } from '@angular/core';
import { UntypedFormBuilder } from '@angular/forms'; import {
import { Router } from '@angular/router'; ReactiveFormsModule,
UntypedFormBuilder,
} from '@angular/forms';
import {
Router,
RouterModule,
} from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core'; import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { import {
BehaviorSubject, BehaviorSubject,
combineLatest, combineLatest,
@@ -40,16 +55,32 @@ import {
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
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 { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { import {
getEPersonEditRoute, getEPersonEditRoute,
getEPersonsRoute, getEPersonsRoute,
} from '../access-control-routing-paths'; } from '../access-control-routing-paths';
import { EPersonFormComponent } from './eperson-form/eperson-form.component';
@Component({ @Component({
selector: 'ds-epeople-registry', selector: 'ds-epeople-registry',
templateUrl: './epeople-registry.component.html', templateUrl: './epeople-registry.component.html',
imports: [
TranslateModule,
RouterModule,
AsyncPipe,
NgIf,
EPersonFormComponent,
ReactiveFormsModule,
ThemedLoadingComponent,
PaginationComponent,
NgClass,
NgForOf,
],
standalone: true,
}) })
/** /**
* A component used for managing all existing epeople within the repository. * A component used for managing all existing epeople within the repository.

View File

@@ -52,7 +52,6 @@
<ds-pagination <ds-pagination
*ngIf="(groups$ | async)?.payload?.totalElements > 0" *ngIf="(groups$ | async)?.payload?.totalElements > 0"
[paginationOptions]="config" [paginationOptions]="config"
[pageInfoState]="groupsPageInfoState$"
[collectionSize]="(groups$ | async)?.payload?.totalElements" [collectionSize]="(groups$ | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true" [hidePagerWhenSinglePage]="true"

View File

@@ -46,9 +46,12 @@ 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 { 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 { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
import { AuthServiceStub } from '../../../shared/testing/auth-service.stub'; import { AuthServiceStub } from '../../../shared/testing/auth-service.stub';
@@ -231,8 +234,6 @@ describe('EPersonFormComponent', () => {
useClass: TranslateLoaderMock, useClass: TranslateLoaderMock,
}, },
}), }),
],
declarations: [
EPersonFormComponent, EPersonFormComponent,
HasNoValuePipe, HasNoValuePipe,
], ],
@@ -251,7 +252,11 @@ describe('EPersonFormComponent', () => {
EPeopleRegistryComponent, EPeopleRegistryComponent,
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); })
.overrideComponent(EPersonFormComponent, {
remove: { imports: [ ThemedLoadingComponent, PaginationComponent,FormComponent] },
})
.compileComponents();
})); }));
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {

View File

@@ -1,3 +1,9 @@
import {
AsyncPipe,
NgClass,
NgFor,
NgIf,
} from '@angular/common';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
@@ -10,6 +16,7 @@ import { UntypedFormGroup } from '@angular/forms';
import { import {
ActivatedRoute, ActivatedRoute,
Router, Router,
RouterLink,
} from '@angular/router'; } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { import {
@@ -18,7 +25,10 @@ import {
DynamicFormLayout, DynamicFormLayout,
DynamicInputModel, DynamicInputModel,
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { TranslateService } from '@ngx-translate/core'; import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { import {
combineLatest as observableCombineLatest, combineLatest as observableCombineLatest,
Observable, Observable,
@@ -58,15 +68,32 @@ import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email
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';
import { FormComponent } from '../../../shared/form/form.component';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
import { HasNoValuePipe } from '../../../shared/utils/has-no-value.pipe';
import { getEPersonsRoute } from '../../access-control-routing-paths'; import { getEPersonsRoute } from '../../access-control-routing-paths';
import { ValidateEmailNotTaken } from './validators/email-taken.validator'; import { ValidateEmailNotTaken } from './validators/email-taken.validator';
@Component({ @Component({
selector: 'ds-eperson-form', selector: 'ds-eperson-form',
templateUrl: './eperson-form.component.html', templateUrl: './eperson-form.component.html',
imports: [
FormComponent,
NgIf,
NgFor,
AsyncPipe,
TranslateModule,
NgClass,
ThemedLoadingComponent,
PaginationComponent,
RouterLink,
HasNoValuePipe,
],
standalone: true,
}) })
/** /**
* A form used for creating and editing EPeople * A form used for creating and editing EPeople

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@@ -27,7 +26,7 @@ export const EPERSON_EDIT_FOLLOW_LINKS: FollowLinkConfig<EPerson>[] = [
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class EPersonResolver implements Resolve<RemoteData<EPerson>> { export class EPersonResolver {
constructor( constructor(
protected ePersonService: EPersonDataService, protected ePersonService: EPersonDataService,

View File

@@ -53,7 +53,11 @@ import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
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 { UUIDService } from '../../../core/shared/uuid.service'; import { UUIDService } from '../../../core/shared/uuid.service';
import { XSRFService } from '../../../core/xsrf/xsrf.service';
import { AlertComponent } from '../../../shared/alert/alert.component';
import { ContextHelpDirective } from '../../../shared/context-help.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 { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
import { RouterMock } from '../../../shared/mocks/router.mock'; import { RouterMock } from '../../../shared/mocks/router.mock';
@@ -67,6 +71,8 @@ import {
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock'; import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock';
import { GroupFormComponent } from './group-form.component'; import { GroupFormComponent } from './group-form.component';
import { MembersListComponent } from './members-list/members-list.component';
import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component';
import { ValidateGroupExists } from './validators/group-exists.validator'; import { ValidateGroupExists } from './validators/group-exists.validator';
describe('GroupFormComponent', () => { describe('GroupFormComponent', () => {
@@ -227,9 +233,7 @@ describe('GroupFormComponent', () => {
provide: TranslateLoader, provide: TranslateLoader,
useClass: TranslateLoaderMock, useClass: TranslateLoaderMock,
}, },
}), }), GroupFormComponent],
],
declarations: [GroupFormComponent],
providers: [ providers: [
{ provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: DSONameService, useValue: new DSONameServiceMock() },
{ provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: EPersonDataService, useValue: ePersonDataServiceStub },
@@ -241,6 +245,7 @@ describe('GroupFormComponent', () => {
{ provide: HttpClient, useValue: {} }, { provide: HttpClient, useValue: {} },
{ provide: ObjectCacheService, useValue: {} }, { provide: ObjectCacheService, useValue: {} },
{ provide: UUIDService, useValue: {} }, { provide: UUIDService, useValue: {} },
{ provide: XSRFService, useValue: {} },
{ provide: Store, useValue: {} }, { provide: Store, useValue: {} },
{ provide: RemoteDataBuildService, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} },
{ provide: HALEndpointService, useValue: {} }, { provide: HALEndpointService, useValue: {} },
@@ -252,7 +257,17 @@ describe('GroupFormComponent', () => {
{ provide: AuthorizationDataService, useValue: authorizationService }, { provide: AuthorizationDataService, useValue: authorizationService },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); })
.overrideComponent(GroupFormComponent, {
remove: { imports: [
FormComponent,
AlertComponent,
ContextHelpDirective,
MembersListComponent,
SubgroupsListComponent,
] },
})
.compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {

View File

@@ -1,3 +1,7 @@
import {
AsyncPipe,
NgIf,
} from '@angular/common';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
@@ -19,7 +23,10 @@ import {
DynamicInputModel, DynamicInputModel,
DynamicTextAreaModel, DynamicTextAreaModel,
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { TranslateService } from '@ngx-translate/core'; import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { import {
combineLatest as observableCombineLatest, combineLatest as observableCombineLatest,
@@ -59,25 +66,41 @@ import {
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { AlertComponent } from '../../../shared/alert/alert.component';
import { AlertType } from '../../../shared/alert/alert-type'; import { AlertType } from '../../../shared/alert/alert-type';
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
import { ContextHelpDirective } from '../../../shared/context-help.directive';
import { import {
hasValue, hasValue,
hasValueOperator, hasValueOperator,
isNotEmpty, isNotEmpty,
} from '../../../shared/empty.util'; } from '../../../shared/empty.util';
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 { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
import { import {
getGroupEditRoute, getGroupEditRoute,
getGroupsRoute, getGroupsRoute,
} from '../../access-control-routing-paths'; } from '../../access-control-routing-paths';
import { MembersListComponent } from './members-list/members-list.component';
import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component';
import { ValidateGroupExists } from './validators/group-exists.validator'; import { ValidateGroupExists } from './validators/group-exists.validator';
@Component({ @Component({
selector: 'ds-group-form', selector: 'ds-group-form',
templateUrl: './group-form.component.html', templateUrl: './group-form.component.html',
imports: [
FormComponent,
AlertComponent,
NgIf,
AsyncPipe,
TranslateModule,
ContextHelpDirective,
MembersListComponent,
SubgroupsListComponent,
],
standalone: true,
}) })
/** /**
* A form used for creating and editing groups * A form used for creating and editing groups

View File

@@ -5,7 +5,6 @@
<ds-pagination *ngIf="(ePeopleMembersOfGroup | async)?.totalElements > 0" <ds-pagination *ngIf="(ePeopleMembersOfGroup | async)?.totalElements > 0"
[paginationOptions]="config" [paginationOptions]="config"
[pageInfoState]="(ePeopleMembersOfGroup | async)"
[collectionSize]="(ePeopleMembersOfGroup | async)?.totalElements" [collectionSize]="(ePeopleMembersOfGroup | async)?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">
@@ -86,7 +85,6 @@
<ds-pagination *ngIf="(ePeopleSearch | async)?.totalElements > 0" <ds-pagination *ngIf="(ePeopleSearch | async)?.totalElements > 0"
[paginationOptions]="configSearch" [paginationOptions]="configSearch"
[pageInfoState]="(ePeopleSearch | async)"
[collectionSize]="(ePeopleSearch | async)?.totalElements" [collectionSize]="(ePeopleSearch | async)?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">

View File

@@ -20,7 +20,10 @@ import {
BrowserModule, BrowserModule,
By, By,
} from '@angular/platform-browser'; } from '@angular/platform-browser';
import { Router } from '@angular/router'; import {
ActivatedRoute,
Router,
} from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { import {
TranslateLoader, TranslateLoader,
@@ -45,13 +48,16 @@ import { EPerson } from '../../../../core/eperson/models/eperson.model';
import { Group } from '../../../../core/eperson/models/group.model'; import { Group } from '../../../../core/eperson/models/group.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 { ContextHelpDirective } from '../../../../shared/context-help.directive';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock';
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
import { RouterMock } from '../../../../shared/mocks/router.mock'; import { RouterMock } from '../../../../shared/mocks/router.mock';
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub';
import { import {
EPersonMock, EPersonMock,
EPersonMock2, EPersonMock2,
@@ -155,9 +161,7 @@ describe('MembersListComponent', () => {
provide: TranslateLoader, provide: TranslateLoader,
useClass: TranslateLoaderMock, useClass: TranslateLoaderMock,
}, },
}), }), MembersListComponent],
],
declarations: [MembersListComponent],
providers: [MembersListComponent, providers: [MembersListComponent,
{ provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub },
@@ -166,9 +170,16 @@ describe('MembersListComponent', () => {
{ provide: Router, useValue: new RouterMock() }, { provide: Router, useValue: new RouterMock() },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: DSONameService, useValue: new DSONameServiceMock() },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); })
.overrideComponent(MembersListComponent, {
remove: {
imports: [PaginationComponent, ContextHelpDirective],
},
})
.compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {

View File

@@ -1,12 +1,27 @@
import {
AsyncPipe,
NgClass,
NgForOf,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
} from '@angular/core'; } from '@angular/core';
import { UntypedFormBuilder } from '@angular/forms'; import {
import { Router } from '@angular/router'; ReactiveFormsModule,
import { TranslateService } from '@ngx-translate/core'; UntypedFormBuilder,
} from '@angular/forms';
import {
Router,
RouterLink,
} from '@angular/router';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { import {
BehaviorSubject, BehaviorSubject,
Observable, Observable,
@@ -31,7 +46,9 @@ import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
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 { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { getEPersonEditRoute } from '../../../access-control-routing-paths'; import { getEPersonEditRoute } from '../../../access-control-routing-paths';
@@ -78,6 +95,18 @@ export interface EPersonListActionConfig {
@Component({ @Component({
selector: 'ds-members-list', selector: 'ds-members-list',
templateUrl: './members-list.component.html', templateUrl: './members-list.component.html',
imports: [
TranslateModule,
ContextHelpDirective,
ReactiveFormsModule,
PaginationComponent,
NgIf,
AsyncPipe,
RouterLink,
NgClass,
NgForOf,
],
standalone: true,
}) })
/** /**
* The list of members in the edit group page * The list of members in the edit group page

View File

@@ -5,7 +5,6 @@
<ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0" <ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0"
[paginationOptions]="config" [paginationOptions]="config"
[pageInfoState]="(subGroups$ | async)?.payload"
[collectionSize]="(subGroups$ | async)?.payload?.totalElements" [collectionSize]="(subGroups$ | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">
@@ -84,7 +83,6 @@
<ds-pagination *ngIf="(searchResults$ | async)?.payload?.totalElements > 0" <ds-pagination *ngIf="(searchResults$ | async)?.payload?.totalElements > 0"
[paginationOptions]="configSearch" [paginationOptions]="configSearch"
[pageInfoState]="(searchResults$ | async)?.payload"
[collectionSize]="(searchResults$ | async)?.payload?.totalElements" [collectionSize]="(searchResults$ | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">

View File

@@ -19,7 +19,10 @@ import {
BrowserModule, BrowserModule,
By, By,
} from '@angular/platform-browser'; } from '@angular/platform-browser';
import { Router } from '@angular/router'; import {
ActivatedRoute,
Router,
} from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { import {
TranslateLoader, TranslateLoader,
@@ -43,13 +46,16 @@ import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { Group } from '../../../../core/eperson/models/group.model'; import { Group } from '../../../../core/eperson/models/group.model';
import { 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 { ContextHelpDirective } from '../../../../shared/context-help.directive';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock';
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
import { RouterMock } from '../../../../shared/mocks/router.mock'; import { RouterMock } from '../../../../shared/mocks/router.mock';
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub';
import { import {
GroupMock, GroupMock,
GroupMock2, GroupMock2,
@@ -119,7 +125,9 @@ describe('SubgroupsListComponent', () => {
if (query === '') { if (query === '') {
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupNonMembers)); return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupNonMembers));
} }
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); return createSuccessfulRemoteDataObject$(
buildPaginatedList(new PageInfo(), []),
);
}, },
addSubGroupToGroup(parentGroup, subgroupToAdd: Group): Observable<RestResponse> { addSubGroupToGroup(parentGroup, subgroupToAdd: Group): Observable<RestResponse> {
// Add group to list of subgroups // Add group to list of subgroups
@@ -153,28 +161,44 @@ describe('SubgroupsListComponent', () => {
routerStub = new RouterMock(); routerStub = new RouterMock();
builderService = getMockFormBuilderService(); builderService = getMockFormBuilderService();
translateService = getMockTranslateService(); translateService = getMockTranslateService();
paginationService = new PaginationServiceStub(); paginationService = new PaginationServiceStub();
return TestBed.configureTestingModule({ return TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, imports: [
CommonModule,
NgbModule,
FormsModule,
ReactiveFormsModule,
BrowserModule,
// ContextHelpDirective,
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
useClass: TranslateLoaderMock, useClass: TranslateLoaderMock,
}, },
}), }),
SubgroupsListComponent,
], ],
declarations: [SubgroupsListComponent], providers: [
providers: [SubgroupsListComponent, SubgroupsListComponent,
{ provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: DSONameService, useValue: new DSONameServiceMock() },
{ provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, {
provide: NotificationsService,
useValue: new NotificationsServiceStub(),
},
{ provide: FormBuilderService, useValue: builderService }, { provide: FormBuilderService, useValue: builderService },
{ provide: Router, useValue: routerStub }, { provide: Router, useValue: routerStub },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); })
.overrideComponent(SubgroupsListComponent, {
remove: {
imports: [ContextHelpDirective, PaginationComponent],
},
})
.compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {

View File

@@ -1,12 +1,26 @@
import {
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
} from '@angular/core'; } from '@angular/core';
import { UntypedFormBuilder } from '@angular/forms'; import {
import { Router } from '@angular/router'; ReactiveFormsModule,
import { TranslateService } from '@ngx-translate/core'; UntypedFormBuilder,
} from '@angular/forms';
import {
Router,
RouterLink,
} from '@angular/router';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { import {
BehaviorSubject, BehaviorSubject,
Observable, Observable,
@@ -30,7 +44,9 @@ import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
} 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 { 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 { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../../shared/utils/follow-link-config.model';
@@ -46,6 +62,17 @@ enum SubKey {
@Component({ @Component({
selector: 'ds-subgroups-list', selector: 'ds-subgroups-list',
templateUrl: './subgroups-list.component.html', templateUrl: './subgroups-list.component.html',
imports: [
RouterLink,
AsyncPipe,
NgForOf,
ContextHelpDirective,
TranslateModule,
ReactiveFormsModule,
PaginationComponent,
NgIf,
],
standalone: true,
}) })
/** /**
* The list of subgroups in the edit group page * The list of subgroups in the edit group page

View File

@@ -37,7 +37,6 @@
<ds-pagination <ds-pagination
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (loading$ | async) !== true" *ngIf="(pageInfoState$ | async)?.totalElements > 0 && (loading$ | async) !== true"
[paginationOptions]="config" [paginationOptions]="config"
[pageInfoState]="pageInfoState$"
[collectionSize]="(pageInfoState$ | async)?.totalElements" [collectionSize]="(pageInfoState$ | async)?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">

View File

@@ -16,8 +16,12 @@ import {
BrowserModule, BrowserModule,
By, By,
} from '@angular/platform-browser'; } from '@angular/platform-browser';
import { Router } from '@angular/router'; import {
ActivatedRoute,
Router,
} from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { provideMockStore } from '@ngrx/store/testing';
import { import {
TranslateLoader, TranslateLoader,
TranslateModule, TranslateModule,
@@ -25,9 +29,12 @@ import {
import { import {
Observable, Observable,
of as observableOf, of as observableOf,
of,
} from 'rxjs'; } from 'rxjs';
import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../core/data/feature-authorization/feature-id';
@@ -53,6 +60,7 @@ import {
import { RouterMock } from '../../shared/mocks/router.mock'; import { RouterMock } from '../../shared/mocks/router.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { import {
EPersonMock, EPersonMock,
EPersonMock2, EPersonMock2,
@@ -74,6 +82,7 @@ describe('GroupsRegistryComponent', () => {
let groupsDataServiceStub: any; let groupsDataServiceStub: any;
let dsoDataServiceStub: any; let dsoDataServiceStub: any;
let authorizationService: AuthorizationDataService; let authorizationService: AuthorizationDataService;
let configurationDataService: jasmine.SpyObj<ConfigurationDataService>;
let mockGroups; let mockGroups;
let mockEPeople; let mockEPeople;
@@ -191,6 +200,10 @@ describe('GroupsRegistryComponent', () => {
}, },
}; };
configurationDataService = jasmine.createSpyObj('ConfigurationDataService', {
findByPropertyName: of({ payload: { value: 'test' } }),
});
authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']); authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
setIsAuthorized(true, true); setIsAuthorized(true, true);
paginationService = new PaginationServiceStub(); paginationService = new PaginationServiceStub();
@@ -201,20 +214,22 @@ describe('GroupsRegistryComponent', () => {
provide: TranslateLoader, provide: TranslateLoader,
useClass: TranslateLoaderMock, useClass: TranslateLoaderMock,
}, },
}), }), GroupsRegistryComponent],
],
declarations: [GroupsRegistryComponent],
providers: [GroupsRegistryComponent, providers: [GroupsRegistryComponent,
{ provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: DSONameService, useValue: new DSONameServiceMock() },
{ provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub },
{ provide: DSpaceObjectDataService, useValue: dsoDataServiceStub }, { provide: DSpaceObjectDataService, useValue: dsoDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: ConfigurationDataService, useValue: configurationDataService },
{ provide: RouteService, useValue: routeServiceStub }, { provide: RouteService, useValue: routeServiceStub },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
{ provide: Router, useValue: new RouterMock() }, { provide: Router, useValue: new RouterMock() },
{ provide: AuthorizationDataService, useValue: authorizationService }, { provide: AuthorizationDataService, useValue: authorizationService },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) }, { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) },
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
provideMockStore(),
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); }).compileComponents();

View File

@@ -1,11 +1,28 @@
import {
AsyncPipe,
NgForOf,
NgIf,
NgSwitch,
NgSwitchCase,
} from '@angular/common';
import { import {
Component, Component,
OnDestroy, OnDestroy,
OnInit, OnInit,
} from '@angular/core'; } from '@angular/core';
import { UntypedFormBuilder } from '@angular/forms'; import {
import { Router } from '@angular/router'; ReactiveFormsModule,
import { TranslateService } from '@ngx-translate/core'; UntypedFormBuilder,
} from '@angular/forms';
import {
Router,
RouterLink,
} from '@angular/router';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { import {
BehaviorSubject, BehaviorSubject,
combineLatest as observableCombineLatest, combineLatest as observableCombineLatest,
@@ -49,13 +66,29 @@ import {
} 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 { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { followLink } from '../../shared/utils/follow-link-config.model'; import { followLink } from '../../shared/utils/follow-link-config.model';
@Component({ @Component({
selector: 'ds-groups-registry', selector: 'ds-groups-registry',
templateUrl: './groups-registry.component.html', templateUrl: './groups-registry.component.html',
imports: [
ThemedLoadingComponent,
TranslateModule,
RouterLink,
ReactiveFormsModule,
AsyncPipe,
NgIf,
PaginationComponent,
NgSwitch,
NgSwitchCase,
NgbTooltipModule,
NgForOf,
],
standalone: true,
}) })
/** /**
* A component used for managing all existing groups within the repository. * A component used for managing all existing groups within the repository.

View File

@@ -6,6 +6,7 @@ import {
} from '@angular/core/testing'; } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { CurationFormComponent } from '../../curation-form/curation-form.component';
import { AdminCurationTasksComponent } from './admin-curation-tasks.component'; import { AdminCurationTasksComponent } from './admin-curation-tasks.component';
describe('AdminCurationTasksComponent', () => { describe('AdminCurationTasksComponent', () => {
@@ -14,10 +15,15 @@ describe('AdminCurationTasksComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()], imports: [TranslateModule.forRoot(), AdminCurationTasksComponent],
declarations: [AdminCurationTasksComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents(); })
.overrideComponent(AdminCurationTasksComponent, {
remove: {
imports: [CurationFormComponent],
},
})
.compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {

View File

@@ -1,4 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { CurationFormComponent } from '../../curation-form/curation-form.component';
/** /**
* Component responsible for rendering the system wide Curation Task UI * Component responsible for rendering the system wide Curation Task UI
@@ -6,6 +9,11 @@ import { Component } from '@angular/core';
@Component({ @Component({
selector: 'ds-admin-curation-task', selector: 'ds-admin-curation-task',
templateUrl: './admin-curation-tasks.component.html', templateUrl: './admin-curation-tasks.component.html',
imports: [
CurationFormComponent,
TranslateModule,
],
standalone: true,
}) })
export class AdminCurationTasksComponent { export class AdminCurationTasksComponent {

View File

@@ -23,6 +23,7 @@ import {
createSuccessfulRemoteDataObject$, createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils'; } from '../../shared/remote-data.utils';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component';
import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive'; import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive';
import { FileValidator } from '../../shared/utils/require-file.validator'; import { FileValidator } from '../../shared/utils/require-file.validator';
import { BatchImportPageComponent } from './batch-import-page.component'; import { BatchImportPageComponent } from './batch-import-page.component';
@@ -58,8 +59,8 @@ describe('BatchImportPageComponent', () => {
FormsModule, FormsModule,
TranslateModule.forRoot(), TranslateModule.forRoot(),
RouterTestingModule.withRoutes([]), RouterTestingModule.withRoutes([]),
BatchImportPageComponent, FileValueAccessorDirective, FileValidator,
], ],
declarations: [BatchImportPageComponent, FileValueAccessorDirective, FileValidator],
providers: [ providers: [
{ provide: NotificationsService, useValue: notificationService }, { provide: NotificationsService, useValue: notificationService },
{ provide: ScriptDataService, useValue: scriptService }, { provide: ScriptDataService, useValue: scriptService },
@@ -67,7 +68,13 @@ describe('BatchImportPageComponent', () => {
{ provide: Location, useValue: locationStub }, { provide: Location, useValue: locationStub },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); })
.overrideComponent(BatchImportPageComponent, {
remove: {
imports: [FileDropzoneNoUploaderComponent],
},
})
.compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {

View File

@@ -1,8 +1,16 @@
import { Location } from '@angular/common'; import {
Location,
NgIf,
} from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core'; import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { UiSwitchModule } from 'ngx-ui-switch';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
@@ -22,10 +30,19 @@ import {
isNotEmpty, isNotEmpty,
} from '../../shared/empty.util'; } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component';
@Component({ @Component({
selector: 'ds-batch-import-page', selector: 'ds-batch-import-page',
templateUrl: './batch-import-page.component.html', templateUrl: './batch-import-page.component.html',
imports: [
NgIf,
TranslateModule,
FormsModule,
UiSwitchModule,
FileDropzoneNoUploaderComponent,
],
standalone: true,
}) })
export class BatchImportPageComponent { export class BatchImportPageComponent {
/** /**

View File

@@ -23,6 +23,7 @@ import {
createSuccessfulRemoteDataObject$, createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils'; } from '../../shared/remote-data.utils';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component';
import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive'; import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive';
import { FileValidator } from '../../shared/utils/require-file.validator'; import { FileValidator } from '../../shared/utils/require-file.validator';
import { MetadataImportPageComponent } from './metadata-import-page.component'; import { MetadataImportPageComponent } from './metadata-import-page.component';
@@ -58,8 +59,8 @@ describe('MetadataImportPageComponent', () => {
FormsModule, FormsModule,
TranslateModule.forRoot(), TranslateModule.forRoot(),
RouterTestingModule.withRoutes([]), RouterTestingModule.withRoutes([]),
MetadataImportPageComponent, FileValueAccessorDirective, FileValidator,
], ],
declarations: [MetadataImportPageComponent, FileValueAccessorDirective, FileValidator],
providers: [ providers: [
{ provide: NotificationsService, useValue: notificationService }, { provide: NotificationsService, useValue: notificationService },
{ provide: ScriptDataService, useValue: scriptService }, { provide: ScriptDataService, useValue: scriptService },
@@ -67,7 +68,13 @@ describe('MetadataImportPageComponent', () => {
{ provide: Location, useValue: locationStub }, { provide: Location, useValue: locationStub },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); })
.overrideComponent(MetadataImportPageComponent, {
remove: {
imports: [FileDropzoneNoUploaderComponent],
},
})
.compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {

View File

@@ -1,7 +1,11 @@
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { import {
METADATA_IMPORT_SCRIPT_NAME, METADATA_IMPORT_SCRIPT_NAME,
@@ -14,10 +18,17 @@ import { Process } from '../../process-page/processes/process.model';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model'; import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component';
@Component({ @Component({
selector: 'ds-metadata-import-page', selector: 'ds-metadata-import-page',
templateUrl: './metadata-import-page.component.html', templateUrl: './metadata-import-page.component.html',
imports: [
TranslateModule,
FormsModule,
FileDropzoneNoUploaderComponent,
],
standalone: true,
}) })
/** /**

View File

@@ -0,0 +1,38 @@
import { Routes } from '@angular/router';
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { navigationBreadcrumbResolver } from '../../core/breadcrumbs/navigation-breadcrumb.resolver';
import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component';
import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component';
const moduleRoutes: Routes = [
{
path: '',
pathMatch: 'full',
component: LdnServicesOverviewComponent,
resolve: { breadcrumb: i18nBreadcrumbResolver },
data: { title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new' },
},
{
path: 'new',
resolve: { breadcrumb: navigationBreadcrumbResolver },
component: LdnServiceFormComponent,
data: { title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service' },
},
{
path: 'edit/:serviceId',
resolve: { breadcrumb: navigationBreadcrumbResolver },
component: LdnServiceFormComponent,
data: { title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service' },
},
];
export const ROUTES = moduleRoutes.map(route => {
return { ...route, data: {
...route.data,
relatedRoutes: moduleRoutes.filter(relatedRoute => relatedRoute.path !== route.path)
.map((relatedRoute) => {
return { path: relatedRoute.path, data: relatedRoute.data };
}),
} };
});

View File

@@ -1,50 +0,0 @@
import { NgModule } from '@angular/core';
import {
RouterModule,
Routes,
} from '@angular/router';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { NavigationBreadcrumbResolver } from '../../core/breadcrumbs/navigation-breadcrumb.resolver';
import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component';
import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component';
const moduleRoutes: Routes = [
{
path: '',
pathMatch: 'full',
component: LdnServicesOverviewComponent,
resolve: { breadcrumb: I18nBreadcrumbResolver },
data: { title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new' },
},
{
path: 'new',
resolve: { breadcrumb: NavigationBreadcrumbResolver },
component: LdnServiceFormComponent,
data: { title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service' },
},
{
path: 'edit/:serviceId',
resolve: { breadcrumb: NavigationBreadcrumbResolver },
component: LdnServiceFormComponent,
data: { title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service' },
},
];
@NgModule({
imports: [
RouterModule.forChild(moduleRoutes.map(route => {
return { ...route, data: {
...route.data,
relatedRoutes: moduleRoutes.filter(relatedRoute => relatedRoute.path !== route.path)
.map((relatedRoute) => {
return { path: relatedRoute.path, data: relatedRoute.data };
}),
} };
})),
],
})
export class AdminLdnServicesRoutingModule {
}

View File

@@ -1,25 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SharedModule } from '../../shared/shared.module';
import { AdminLdnServicesRoutingModule } from './admin-ldn-services-routing.module';
import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component';
import { LdnItemfiltersService } from './ldn-services-data/ldn-itemfilters-data.service';
import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component';
@NgModule({
imports: [
CommonModule,
SharedModule,
AdminLdnServicesRoutingModule,
FormsModule,
],
declarations: [
LdnServicesOverviewComponent,
LdnServiceFormComponent,
],
providers: [LdnItemfiltersService],
})
export class AdminLdnServicesModule {
}

View File

@@ -114,8 +114,7 @@ describe('LdnServiceFormEditComponent', () => {
activatedRoute = new MockActivatedRoute(routeParams, routeUrlSegments); activatedRoute = new MockActivatedRoute(routeParams, routeUrlSegments);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ReactiveFormsModule, TranslateModule.forRoot(), NgbDropdownModule], imports: [ReactiveFormsModule, TranslateModule.forRoot(), NgbDropdownModule, LdnServiceFormComponent],
declarations: [LdnServiceFormComponent],
providers: [ providers: [
{ provide: LdnServicesService, useValue: ldnServicesService }, { provide: LdnServicesService, useValue: ldnServicesService },
{ provide: LdnItemfiltersService, useValue: ldnItemfiltersService }, { provide: LdnItemfiltersService, useValue: ldnItemfiltersService },

View File

@@ -5,6 +5,11 @@ import {
transition, transition,
trigger, trigger,
} from '@angular/animations'; } from '@angular/animations';
import {
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
@@ -17,14 +22,21 @@ import {
FormArray, FormArray,
FormBuilder, FormBuilder,
FormGroup, FormGroup,
ReactiveFormsModule,
Validators, Validators,
} from '@angular/forms'; } from '@angular/forms';
import { import {
ActivatedRoute, ActivatedRoute,
Router, Router,
} from '@angular/router'; } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import {
import { TranslateService } from '@ngx-translate/core'; NgbDropdownModule,
NgbModal,
} from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { import {
combineLatestWith, combineLatestWith,
@@ -54,6 +66,7 @@ import { notifyPatterns } from '../ldn-services-patterns/ldn-service-coar-patter
selector: 'ds-ldn-service-form', selector: 'ds-ldn-service-form',
templateUrl: './ldn-service-form.component.html', templateUrl: './ldn-service-form.component.html',
styleUrls: ['./ldn-service-form.component.scss'], styleUrls: ['./ldn-service-form.component.scss'],
standalone: true,
animations: [ animations: [
trigger('toggleAnimation', [ trigger('toggleAnimation', [
state('true', style({})), state('true', style({})),
@@ -61,6 +74,14 @@ import { notifyPatterns } from '../ldn-services-patterns/ldn-service-coar-patter
transition('true <=> false', animate('300ms ease-in')), transition('true <=> false', animate('300ms ease-in')),
]), ]),
], ],
imports: [
ReactiveFormsModule,
TranslateModule,
NgIf,
NgbDropdownModule,
NgForOf,
AsyncPipe,
],
}) })
export class LdnServiceFormComponent implements OnInit, OnDestroy { export class LdnServiceFormComponent implements OnInit, OnDestroy {
formModel: FormGroup; formModel: FormGroup;

View File

@@ -3,7 +3,6 @@ import { Observable } from 'rxjs';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { dataService } from '../../../core/data/base/data-service.decorator';
import { import {
FindAllData, FindAllData,
FindAllDataImpl, FindAllDataImpl,
@@ -16,14 +15,12 @@ import { RequestService } from '../../../core/data/request.service';
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { LDN_SERVICE_CONSTRAINT_FILTERS } from '../ldn-services-model/ldn-service.resource-type';
import { Itemfilter } from '../ldn-services-model/ldn-service-itemfilters'; import { Itemfilter } from '../ldn-services-model/ldn-service-itemfilters';
/** /**
* A service responsible for fetching/sending data from/to the REST API on the itemfilters endpoint * A service responsible for fetching/sending data from/to the REST API on the itemfilters endpoint
*/ */
@Injectable() @Injectable({ providedIn: 'root' })
@dataService(LDN_SERVICE_CONSTRAINT_FILTERS)
export class LdnItemfiltersService extends IdentifiableDataService<Itemfilter> implements FindAllData<Itemfilter> { export class LdnItemfiltersService extends IdentifiableDataService<Itemfilter> implements FindAllData<Itemfilter> {
private findAllData: FindAllDataImpl<Itemfilter>; private findAllData: FindAllDataImpl<Itemfilter>;

View File

@@ -13,7 +13,6 @@ import {
CreateData, CreateData,
CreateDataImpl, CreateDataImpl,
} from '../../../core/data/base/create-data'; } from '../../../core/data/base/create-data';
import { dataService } from '../../../core/data/base/data-service.decorator';
import { import {
DeleteData, DeleteData,
DeleteDataImpl, DeleteDataImpl,
@@ -42,7 +41,6 @@ import { URLCombiner } from '../../../core/url-combiner/url-combiner';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { LdnServiceConstrain } from '../ldn-services-model/ldn-service.constrain.model'; import { LdnServiceConstrain } from '../ldn-services-model/ldn-service.constrain.model';
import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type';
import { LdnService } from '../ldn-services-model/ldn-services.model'; import { LdnService } from '../ldn-services-model/ldn-services.model';
/** /**
@@ -56,8 +54,7 @@ import { LdnService } from '../ldn-services-model/ldn-services.model';
* @implements {PatchData<LdnService>} * @implements {PatchData<LdnService>}
* @implements {CreateData<LdnService>} * @implements {CreateData<LdnService>}
*/ */
@Injectable() @Injectable({ providedIn: 'root' })
@dataService(LDN_SERVICE)
export class LdnServicesService extends IdentifiableDataService<LdnService> implements FindAllData<LdnService>, DeleteData<LdnService>, PatchData<LdnService>, CreateData<LdnService> { export class LdnServicesService extends IdentifiableDataService<LdnService> implements FindAllData<LdnService>, DeleteData<LdnService>, PatchData<LdnService>, CreateData<LdnService> {
createData: CreateDataImpl<LdnService>; createData: CreateDataImpl<LdnService>;
private findAllData: FindAllDataImpl<LdnService>; private findAllData: FindAllDataImpl<LdnService>;

View File

@@ -10,7 +10,6 @@
[collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements" [collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true" [hidePagerWhenSinglePage]="true"
[pageInfoState]="(ldnServicesRD$ | async)?.payload"
[paginationOptions]="pageConfig"> [paginationOptions]="pageConfig">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">

View File

@@ -9,6 +9,7 @@ import {
TestBed, TestBed,
tick, tick,
} from '@angular/core/testing'; } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { import {
TranslateModule, TranslateModule,
@@ -20,10 +21,14 @@ import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { createPaginatedList } from '../../../shared/testing/utils.test'; import { createPaginatedList } from '../../../shared/testing/utils.test';
import { TruncatableComponent } from '../../../shared/truncatable/truncatable.component';
import { TruncatablePartComponent } from '../../../shared/truncatable/truncatable-part/truncatable-part.component';
import { LdnServicesService } from '../ldn-services-data/ldn-services-data.service'; import { LdnServicesService } from '../ldn-services-data/ldn-services-data.service';
import { LdnService } from '../ldn-services-model/ldn-services.model'; import { LdnService } from '../ldn-services-model/ldn-services.model';
import { LdnServicesOverviewComponent } from './ldn-services-directory.component'; import { LdnServicesOverviewComponent } from './ldn-services-directory.component';
@@ -50,8 +55,7 @@ describe('LdnServicesOverviewComponent', () => {
'patch': createSuccessfulRemoteDataObject$({}), 'patch': createSuccessfulRemoteDataObject$({}),
}); });
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()], imports: [TranslateModule.forRoot(), LdnServicesOverviewComponent],
declarations: [LdnServicesOverviewComponent],
providers: [ providers: [
{ {
provide: LdnServicesService, provide: LdnServicesService,
@@ -60,16 +64,28 @@ describe('LdnServicesOverviewComponent', () => {
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ {
provide: NgbModal, useValue: { provide: NgbModal, useValue: {
open: () => { /*comment*/ open: () => {
//
}, },
}, },
}, },
{ provide: ChangeDetectorRef, useValue: {} }, { provide: ChangeDetectorRef, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: TranslateService, useValue: translateServiceStub }, { provide: TranslateService, useValue: translateServiceStub },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); })
.overrideComponent(LdnServicesOverviewComponent, {
remove: {
imports: [
PaginationComponent,
TruncatableComponent,
TruncatablePartComponent,
],
},
})
.compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
@@ -107,11 +123,9 @@ describe('LdnServicesOverviewComponent', () => {
component.ldnServicesRD$ = createSuccessfulRemoteDataObject$(mockLdnServicesRD); component.ldnServicesRD$ = createSuccessfulRemoteDataObject$(mockLdnServicesRD);
fixture.detectChanges(); fixture.detectChanges();
const tableRows = fixture.debugElement.nativeElement.querySelectorAll('tbody tr'); component.ldnServicesRD$.subscribe((rd) => {
expect(tableRows.length).toBe(testData.length); expect(rd.payload.page).toEqual(mockLdnServicesRD.page);
const firstRowContent = tableRows[0].textContent; });
expect(firstRowContent).toContain('Service 1');
expect(firstRowContent).toContain('Description 1');
})); }));
}); });

View File

@@ -1,3 +1,9 @@
import {
AsyncPipe,
NgClass,
NgFor,
NgIf,
} from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
@@ -7,8 +13,12 @@ import {
TemplateRef, TemplateRef,
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { RouterLink } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core'; import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { import {
Observable, Observable,
@@ -27,7 +37,10 @@ import { RemoteData } from '../../../core/data/remote-data';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { TruncatableComponent } from '../../../shared/truncatable/truncatable.component';
import { TruncatablePartComponent } from '../../../shared/truncatable/truncatable-part/truncatable-part.component';
import { LdnService } from '../ldn-services-model/ldn-services.model'; import { LdnService } from '../ldn-services-model/ldn-services.model';
/** /**
@@ -40,6 +53,18 @@ import { LdnService } from '../ldn-services-model/ldn-services.model';
templateUrl: './ldn-services-directory.component.html', templateUrl: './ldn-services-directory.component.html',
styleUrls: ['./ldn-services-directory.component.scss'], styleUrls: ['./ldn-services-directory.component.scss'],
changeDetection: ChangeDetectionStrategy.Default, changeDetection: ChangeDetectionStrategy.Default,
imports: [
NgIf,
NgFor,
TranslateModule,
AsyncPipe,
PaginationComponent,
TruncatableComponent,
TruncatablePartComponent,
NgClass,
RouterLink,
],
standalone: true,
}) })
export class LdnServicesOverviewComponent implements OnInit, OnDestroy { export class LdnServicesOverviewComponent implements OnInit, OnDestroy {

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -17,8 +16,8 @@ export interface NotificationsSuggestionTargetsPageParams {
/** /**
* This class represents a resolver that retrieve the route data before the route is activated. * This class represents a resolver that retrieve the route data before the route is activated.
*/ */
@Injectable() @Injectable({ providedIn: 'root' })
export class NotificationsSuggestionTargetsPageResolver implements Resolve<NotificationsSuggestionTargetsPageParams> { export class NotificationsSuggestionTargetsPageResolver {
/** /**
* Method for resolving the parameters in the current route. * Method for resolving the parameters in the current route.

View File

@@ -1,37 +1,40 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { import {
async,
ComponentFixture, ComponentFixture,
TestBed, TestBed,
waitForAsync,
} from '@angular/core/testing'; } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { NotificationsSuggestionTargetsPageComponent } from '../../../quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component'; import { PublicationClaimComponent } from '../../../notifications/suggestion-targets/publication-claim/publication-claim.component';
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page.component';
describe('NotificationsSuggestionTargetsPageComponent', () => { describe('AdminNotificationsPublicationClaimPageComponent', () => {
let component: NotificationsSuggestionTargetsPageComponent; let component: AdminNotificationsPublicationClaimPageComponent;
let fixture: ComponentFixture<NotificationsSuggestionTargetsPageComponent>; let fixture: ComponentFixture<AdminNotificationsPublicationClaimPageComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
CommonModule, CommonModule,
TranslateModule.forRoot(), TranslateModule.forRoot(),
], AdminNotificationsPublicationClaimPageComponent,
declarations: [
NotificationsSuggestionTargetsPageComponent,
], ],
providers: [ providers: [
NotificationsSuggestionTargetsPageComponent, AdminNotificationsPublicationClaimPageComponent,
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).overrideComponent(AdminNotificationsPublicationClaimPageComponent, {
remove: {
imports: [PublicationClaimComponent],
},
}) })
.compileComponents(); .compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(NotificationsSuggestionTargetsPageComponent); fixture = TestBed.createComponent(AdminNotificationsPublicationClaimPageComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -1,9 +1,15 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { PublicationClaimComponent } from '../../../notifications/suggestion-targets/publication-claim/publication-claim.component';
@Component({ @Component({
selector: 'ds-admin-notifications-publication-claim-page', selector: 'ds-admin-notifications-publication-claim-page',
templateUrl: './admin-notifications-publication-claim-page.component.html', templateUrl: './admin-notifications-publication-claim-page.component.html',
styleUrls: ['./admin-notifications-publication-claim-page.component.scss'], styleUrls: ['./admin-notifications-publication-claim-page.component.scss'],
imports: [
PublicationClaimComponent,
],
standalone: true,
}) })
export class AdminNotificationsPublicationClaimPageComponent { export class AdminNotificationsPublicationClaimPageComponent {

View File

@@ -0,0 +1,98 @@
import { Route } from '@angular/router';
import { authenticatedGuard } from '../../core/auth/authenticated.guard';
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { qualityAssuranceBreadcrumbResolver } from '../../core/breadcrumbs/quality-assurance-breadcrumb.resolver';
import { AdminNotificationsPublicationClaimPageResolver } from '../../quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service';
import { QualityAssuranceEventsPageComponent } from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.component';
import { qualityAssuranceEventsPageResolver } from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver';
import { qualityAssuranceSourceDataResolver } from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver';
import { QualityAssuranceSourcePageComponent } from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page.component';
import { QualityAssuranceSourcePageResolver } from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page-resolver.service';
import { QualityAssuranceTopicsPageComponent } from '../../quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page.component';
import { QualityAssuranceTopicsPageResolver } from '../../quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page-resolver.service';
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component';
import {
PUBLICATION_CLAIMS_PATH,
QUALITY_ASSURANCE_EDIT_PATH,
} from './admin-notifications-routing-paths';
export const ROUTES: Route[] = [
{
canActivate: [ authenticatedGuard ],
path: `${PUBLICATION_CLAIMS_PATH}`,
component: AdminNotificationsPublicationClaimPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: i18nBreadcrumbResolver,
suggestionTargetParams: AdminNotificationsPublicationClaimPageResolver,
},
data: {
title: 'admin.notifications.publicationclaim.page.title',
breadcrumbKey: 'admin.notifications.publicationclaim',
showBreadcrumbsFluid: false,
},
},
{
canActivate: [authenticatedGuard],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
component: QualityAssuranceTopicsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: qualityAssuranceBreadcrumbResolver,
openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver,
},
data: {
title: 'admin.quality-assurance.page.title',
breadcrumbKey: 'admin.quality-assurance',
showBreadcrumbsFluid: false,
},
},
{
canActivate: [ authenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/target/:targetId`,
component: QualityAssuranceTopicsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: i18nBreadcrumbResolver,
openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver,
},
data: {
title: 'admin.quality-assurance.page.title',
breadcrumbKey: 'admin.quality-assurance',
showBreadcrumbsFluid: false,
},
},
{
canActivate: [authenticatedGuard],
path: `${QUALITY_ASSURANCE_EDIT_PATH}`,
component: QualityAssuranceSourcePageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: i18nBreadcrumbResolver,
openaireQualityAssuranceSourceParams: QualityAssuranceSourcePageResolver,
sourceData: qualityAssuranceSourceDataResolver,
},
data: {
title: 'admin.notifications.source.breadcrumbs',
breadcrumbKey: 'admin.notifications.source',
showBreadcrumbsFluid: false,
},
},
{
canActivate: [authenticatedGuard],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/:topicId`,
component: QualityAssuranceEventsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: qualityAssuranceBreadcrumbResolver,
openaireQualityAssuranceEventsParams: qualityAssuranceEventsPageResolver,
},
data: {
title: 'admin.notifications.event.page.title',
breadcrumbKey: 'admin.notifications.event',
showBreadcrumbsFluid: false,
},
},
];

View File

@@ -1,123 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AuthenticatedGuard } from '../../core/auth/authenticated.guard';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
import { QualityAssuranceBreadcrumbResolver } from '../../core/breadcrumbs/quality-assurance-breadcrumb.resolver';
import { QualityAssuranceBreadcrumbService } from '../../core/breadcrumbs/quality-assurance-breadcrumb.service';
import { SiteAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { AdminNotificationsPublicationClaimPageResolver } from '../../quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service';
import { QualityAssuranceEventsPageComponent } from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.component';
import { QualityAssuranceEventsPageResolver } from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver';
import { SourceDataResolver } from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver';
import { QualityAssuranceSourcePageComponent } from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page.component';
import { QualityAssuranceSourcePageResolver } from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page-resolver.service';
import { QualityAssuranceTopicsPageComponent } from '../../quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page.component';
import { QualityAssuranceTopicsPageResolver } from '../../quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page-resolver.service';
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component';
import {
PUBLICATION_CLAIMS_PATH,
QUALITY_ASSURANCE_EDIT_PATH,
} from './admin-notifications-routing-paths';
@NgModule({
imports: [
RouterModule.forChild([
{
canActivate: [ AuthenticatedGuard ],
path: `${PUBLICATION_CLAIMS_PATH}`,
component: AdminNotificationsPublicationClaimPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: I18nBreadcrumbResolver,
suggestionTargetParams: AdminNotificationsPublicationClaimPageResolver,
},
data: {
title: 'admin.notifications.publicationclaim.page.title',
breadcrumbKey: 'admin.notifications.publicationclaim',
showBreadcrumbsFluid: false,
},
},
{
canActivate: [ AuthenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
component: QualityAssuranceTopicsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: QualityAssuranceBreadcrumbResolver,
openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver,
},
data: {
title: 'admin.quality-assurance.page.title',
breadcrumbKey: 'admin.quality-assurance',
showBreadcrumbsFluid: false,
},
},
{
canActivate: [ AuthenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/target/:targetId`,
component: QualityAssuranceTopicsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: I18nBreadcrumbResolver,
openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver,
},
data: {
title: 'admin.quality-assurance.page.title',
breadcrumbKey: 'admin.quality-assurance',
showBreadcrumbsFluid: false,
},
},
{
canActivate: [ SiteAdministratorGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}`,
component: QualityAssuranceSourcePageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: I18nBreadcrumbResolver,
openaireQualityAssuranceSourceParams: QualityAssuranceSourcePageResolver,
sourceData: SourceDataResolver,
},
data: {
title: 'admin.notifications.source.breadcrumbs',
breadcrumbKey: 'admin.notifications.source',
showBreadcrumbsFluid: false,
},
},
{
canActivate: [ AuthenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/:topicId`,
component: QualityAssuranceEventsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: QualityAssuranceBreadcrumbResolver,
openaireQualityAssuranceEventsParams: QualityAssuranceEventsPageResolver,
},
data: {
title: 'admin.notifications.event.page.title',
breadcrumbKey: 'admin.notifications.event',
showBreadcrumbsFluid: false,
},
},
]),
],
providers: [
I18nBreadcrumbResolver,
I18nBreadcrumbsService,
AdminNotificationsPublicationClaimPageResolver,
SourceDataResolver,
QualityAssuranceSourcePageResolver,
QualityAssuranceTopicsPageResolver,
QualityAssuranceEventsPageResolver,
QualityAssuranceSourcePageResolver,
QualityAssuranceBreadcrumbResolver,
QualityAssuranceBreadcrumbService,
],
})
/**
* Routing module for the Notifications section of the admin sidebar
*/
export class AdminNotificationsRoutingModule {
}

View File

@@ -1,28 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { CoreModule } from '../../core/core.module';
import { NotificationsModule } from '../../notifications/notifications.module';
import { SharedModule } from '../../shared/shared.module';
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component';
import { AdminNotificationsRoutingModule } from './admin-notifications-routing.module';
@NgModule({
imports: [
CommonModule,
SharedModule,
CoreModule.forRoot(),
AdminNotificationsRoutingModule,
NotificationsModule,
],
declarations: [
AdminNotificationsPublicationClaimPageComponent,
],
entryComponents: [],
})
/**
* This module handles all components related to the notifications pages
*/
export class AdminNotificationsModule {
}

View File

@@ -0,0 +1,51 @@
import {
mapToCanActivate,
Route,
} from '@angular/router';
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { notifyInfoGuard } from '../../core/coar-notify/notify-info/notify-info.guard';
import { SiteAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component';
import { AdminNotifyIncomingComponent } from './admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component';
import { AdminNotifyOutgoingComponent } from './admin-notify-logs/admin-notify-outgoing/admin-notify-outgoing.component';
export const ROUTES: Route[] = [
{
canActivate: [...mapToCanActivate([SiteAdministratorGuard]), notifyInfoGuard],
path: '',
resolve: {
breadcrumb: i18nBreadcrumbResolver,
},
component: AdminNotifyDashboardComponent,
pathMatch: 'full',
data: {
title: 'admin.notify.dashboard.page.title',
breadcrumbKey: 'admin.notify.dashboard',
},
},
{
path: 'inbound',
resolve: {
breadcrumb: i18nBreadcrumbResolver,
},
component: AdminNotifyIncomingComponent,
canActivate: [...mapToCanActivate([SiteAdministratorGuard]), notifyInfoGuard],
data: {
title: 'admin.notify.dashboard.page.title',
breadcrumbKey: 'admin.notify.dashboard',
},
},
{
path: 'outbound',
resolve: {
breadcrumb: i18nBreadcrumbResolver,
},
component: AdminNotifyOutgoingComponent,
canActivate: [...mapToCanActivate([SiteAdministratorGuard]), notifyInfoGuard],
data: {
title: 'admin.notify.dashboard.page.title',
breadcrumbKey: 'admin.notify.dashboard',
},
},
];

View File

@@ -1,59 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { NotifyInfoGuard } from '../../core/coar-notify/notify-info/notify-info.guard';
import { SiteAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component';
import { AdminNotifyIncomingComponent } from './admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component';
import { AdminNotifyOutgoingComponent } from './admin-notify-logs/admin-notify-outgoing/admin-notify-outgoing.component';
@NgModule({
imports: [
RouterModule.forChild([
{
canActivate: [SiteAdministratorGuard, NotifyInfoGuard],
path: '',
resolve: {
breadcrumb: I18nBreadcrumbResolver,
},
component: AdminNotifyDashboardComponent,
pathMatch: 'full',
data: {
title: 'admin.notify.dashboard.page.title',
breadcrumbKey: 'admin.notify.dashboard',
},
},
{
path: 'inbound',
resolve: {
breadcrumb: I18nBreadcrumbResolver,
},
component: AdminNotifyIncomingComponent,
canActivate: [SiteAdministratorGuard, NotifyInfoGuard],
data: {
title: 'admin.notify.dashboard.page.title',
breadcrumbKey: 'admin.notify.dashboard',
},
},
{
path: 'outbound',
resolve: {
breadcrumb: I18nBreadcrumbResolver,
},
component: AdminNotifyOutgoingComponent,
canActivate: [SiteAdministratorGuard, NotifyInfoGuard],
data: {
title: 'admin.notify.dashboard.page.title',
breadcrumbKey: 'admin.notify.dashboard',
},
},
]),
],
})
/**
* Routing module for the Notifications section of the admin sidebar
*/
export class AdminNotifyDashboardRoutingModule {
}

View File

@@ -1,14 +1,18 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { import {
ComponentFixture, ComponentFixture,
TestBed, TestBed,
} from '@angular/core/testing'; } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { buildPaginatedList } from '../../core/data/paginated-list.model'; import { buildPaginatedList } from '../../core/data/paginated-list.model';
import { SearchService } from '../../core/shared/search/search.service'; import { SearchService } from '../../core/shared/search/search.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component'; import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component';
import { AdminNotifyMetricsComponent } from './admin-notify-metrics/admin-notify-metrics.component';
import { AdminNotifyMessage } from './models/admin-notify-message.model'; import { AdminNotifyMessage } from './models/admin-notify-message.model';
import { AdminNotifySearchResult } from './models/admin-notify-message-search-result.model'; import { AdminNotifySearchResult } from './models/admin-notify-message-search-result.model';
@@ -39,9 +43,16 @@ describe('AdminNotifyDashboardComponent', () => {
results = buildPaginatedList(undefined, [searchResult1, searchResult2, searchResult3]); results = buildPaginatedList(undefined, [searchResult1, searchResult2, searchResult3]);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NgbNavModule], imports: [TranslateModule.forRoot(), NgbNavModule, AdminNotifyDashboardComponent],
declarations: [ AdminNotifyDashboardComponent ], providers: [
providers: [{ provide: SearchService, useValue: { search: () => createSuccessfulRemoteDataObject$(results) } }], { provide: SearchService, useValue: { search: () => createSuccessfulRemoteDataObject$(results) } },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
],
schemas: [NO_ERRORS_SCHEMA],
}).overrideComponent(AdminNotifyDashboardComponent, {
remove: {
imports: [AdminNotifyMetricsComponent],
},
}) })
.compileComponents(); .compileComponents();

View File

@@ -1,7 +1,13 @@
import {
AsyncPipe,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
OnInit, OnInit,
} from '@angular/core'; } from '@angular/core';
import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { import {
forkJoin, forkJoin,
Observable, Observable,
@@ -13,10 +19,11 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { SearchService } from '../../core/shared/search/search.service'; import { SearchService } from '../../core/shared/search/search.service';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-configuration.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
import { SearchObjects } from '../../shared/search/models/search-objects.model'; import { SearchObjects } from '../../shared/search/models/search-objects.model';
import { AdminNotifyMetricsComponent } from './admin-notify-metrics/admin-notify-metrics.component';
import { import {
AdminNotifyMetricsBox, AdminNotifyMetricsBox,
AdminNotifyMetricsRow, AdminNotifyMetricsRow,
@@ -31,6 +38,14 @@ import {
useClass: SearchConfigurationService, useClass: SearchConfigurationService,
}, },
], ],
standalone: true,
imports: [
AdminNotifyMetricsComponent,
RouterLink,
NgIf,
TranslateModule,
AsyncPipe,
],
}) })
/** /**

View File

@@ -1,55 +0,0 @@
import {
CommonModule,
DatePipe,
} from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { SearchPageModule } from '../../search-page/search-page.module';
import { SearchModule } from '../../shared/search/search.module';
import { SharedModule } from '../../shared/shared.module';
import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component';
import { AdminNotifyDashboardRoutingModule } from './admin-notify-dashboard-routing.module';
import { AdminNotifyDetailModalComponent } from './admin-notify-detail-modal/admin-notify-detail-modal.component';
import { AdminNotifyIncomingComponent } from './admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component';
import { AdminNotifyLogsResultComponent } from './admin-notify-logs/admin-notify-logs-result/admin-notify-logs-result.component';
import { AdminNotifyOutgoingComponent } from './admin-notify-logs/admin-notify-outgoing/admin-notify-outgoing.component';
import { AdminNotifyMetricsComponent } from './admin-notify-metrics/admin-notify-metrics.component';
import { AdminNotifySearchResultComponent } from './admin-notify-search-result/admin-notify-search-result.component';
import { AdminNotifyMessagesService } from './services/admin-notify-messages.service';
const ENTRY_COMPONENTS = [
AdminNotifySearchResultComponent,
];
@NgModule({
imports: [
CommonModule,
RouterModule,
SharedModule,
AdminNotifyDashboardRoutingModule,
SearchModule,
SearchPageModule,
],
providers: [
AdminNotifyMessagesService,
DatePipe,
],
declarations: [
...ENTRY_COMPONENTS,
AdminNotifyDashboardComponent,
AdminNotifyMetricsComponent,
AdminNotifyIncomingComponent,
AdminNotifyOutgoingComponent,
AdminNotifyDetailModalComponent,
AdminNotifySearchResultComponent,
AdminNotifyLogsResultComponent,
],
})
export class AdminNotifyDashboardModule {
static withEntryComponents() {
return {
ngModule: AdminNotifyDashboardModule,
providers: ENTRY_COMPONENTS.map((component) => ({ provide: component })),
};
}
}

View File

@@ -15,8 +15,7 @@ describe('AdminNotifyDetailModalComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()], imports: [TranslateModule.forRoot(), AdminNotifyDetailModalComponent],
declarations: [ AdminNotifyDetailModalComponent ],
providers: [{ provide: NgbActiveModal, useValue: modalStub }], providers: [{ provide: NgbActiveModal, useValue: modalStub }],
}) })
.compileComponents(); .compileComponents();

View File

@@ -1,3 +1,7 @@
import {
NgForOf,
NgIf,
} from '@angular/common';
import { import {
Component, Component,
EventEmitter, EventEmitter,
@@ -5,7 +9,10 @@ import {
Output, Output,
} from '@angular/core'; } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core'; import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { fadeIn } from '../../../shared/animations/fade'; import { fadeIn } from '../../../shared/animations/fade';
import { MissingTranslationHelper } from '../../../shared/translate/missing-translation.helper'; import { MissingTranslationHelper } from '../../../shared/translate/missing-translation.helper';
@@ -17,6 +24,12 @@ import { AdminNotifyMessage } from '../models/admin-notify-message.model';
animations: [ animations: [
fadeIn, fadeIn,
], ],
standalone: true,
imports: [
NgForOf,
TranslateModule,
NgIf,
],
}) })
/** /**
* Component for detailed view of LDN messages displayed in search result in AdminNotifyDashboardComponent * Component for detailed view of LDN messages displayed in search result in AdminNotifyDashboardComponent

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