Merge remote-tracking branch 'origin/main' into fix-versioning-button

This commit is contained in:
Yury Bondarenko
2024-04-12 11:48:37 +02:00
2842 changed files with 75920 additions and 52931 deletions

View File

@@ -8,7 +8,10 @@
"eslint-plugin-deprecation",
"unused-imports",
"eslint-plugin-lodash",
"eslint-plugin-jsonc"
"eslint-plugin-jsonc",
"eslint-plugin-rxjs",
"eslint-plugin-simple-import-sort",
"eslint-plugin-import-newlines"
],
"overrides": [
{
@@ -27,17 +30,29 @@
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
"plugin:@angular-eslint/template/process-inline-templates",
"plugin:rxjs/recommended"
],
"rules": {
"indent": [
"error",
2,
{
"SwitchCase": 1
}
],
"max-classes-per-file": [
"error",
1
],
"comma-dangle": [
"off",
"error",
"always-multiline"
],
"object-curly-spacing": [
"error",
"always"
],
"eol-last": [
"error",
"always"
@@ -104,15 +119,13 @@
"allowTernary": true
}
],
"prefer-const": "off", // todo: re-enable & fix errors (more strict than it used to be in TSLint)
"prefer-const": "error",
"no-case-declarations": "error",
"no-extra-boolean-cast": "error",
"prefer-spread": "off",
"no-underscore-dangle": "off",
// todo: disabled rules from eslint:recommended, consider re-enabling & fixing
"no-prototype-builtins": "off",
"no-useless-escape": "off",
"no-case-declarations": "off",
"no-extra-boolean-cast": "off",
"@angular-eslint/directive-selector": [
"error",
@@ -139,7 +152,6 @@
}
],
"@angular-eslint/no-attribute-decorator": "error",
"@angular-eslint/no-forward-ref": "error",
"@angular-eslint/no-output-native": "warn",
"@angular-eslint/no-output-on-prefix": "warn",
"@angular-eslint/no-conflicting-lifecycle": "warn",
@@ -183,7 +195,7 @@
],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unified-signatures": "error",
"@typescript-eslint/ban-types": "warn", // todo: deal with {} type issues & re-enable
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/no-misused-promises": "warn",
"@typescript-eslint/restrict-plus-operands": "warn",
@@ -203,14 +215,45 @@
"deprecation/deprecation": "warn",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"import/order": "off",
"import/first": "error",
"import/newline-after-import": "error",
"import/no-duplicates": "error",
"import/no-deprecated": "warn",
"import/no-namespace": "error",
"import-newlines/enforce": [
"error",
{
"items": 1,
"semi": true,
"forceSingleLine": true
}
],
"unused-imports/no-unused-imports": "error",
"lodash/import-scope": [
"error",
"method"
]
],
"rxjs/no-nested-subscribe": "off" // todo: go over _all_ cases
}
},
{
"files": [
"*.spec.ts"
],
"parserOptions": {
"project": [
"./tsconfig.json",
"./cypress/tsconfig.json"
],
"createDefaultProgram": true
},
"rules": {
"prefer-const": "off"
}
},
{
@@ -219,12 +262,7 @@
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {
// todo: re-enable & fix errors
"@angular-eslint/template/no-negated-async": "off",
"@angular-eslint/template/eqeqeq": "off"
}
]
},
{
"files": [

View File

@@ -33,12 +33,12 @@ jobs:
#CHROME_VERSION: "90.0.4430.212-1"
# Bump Node heap size (OOM in CI after upgrading to Angular 15)
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'
strategy:
# Create a matrix of Node versions to test against (in parallel)
matrix:
node-version: [16.x, 18.x]
node-version: [18.x, 20.x]
# Do NOT exit immediately if one matrix job fails
fail-fast: false
# These are the actual CI steps to perform per job
@@ -74,7 +74,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Cache Yarn dependencies
uses: actions/cache@v3
uses: actions/cache@v4
with:
# Cache entire Yarn cache directory (see previous step)
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)
# 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
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: matrix.node-version == '18.x'
with:
name: dspace-angular coverage report
name: coverage-report-${{ matrix.node-version }}
path: 'coverage/dspace-angular/lcov.info'
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
- name: Start DSpace REST Backend via Docker (for e2e tests)
run: |
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/docker-compose-ci.yml up -d
docker compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
docker container ls
# 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)
# Save those in an Artifact
- name: Upload e2e test videos to Artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: always()
with:
name: e2e-test-videos
name: e2e-test-videos-${{ matrix.node-version }}
path: cypress/videos
# If e2e tests fail, Cypress creates a screenshot of what happened
# Save those in an Artifact
- name: Upload e2e test failure screenshots to Artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: e2e-test-screenshots
name: e2e-test-screenshots-${{ matrix.node-version }}
path: cypress/screenshots
- name: Stop app (in case it stays up after e2e tests)
@@ -182,7 +182,7 @@ jobs:
run: kill -9 $(lsof -t -i:4000)
- 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
# 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
- name: Download coverage artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
# 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.
@@ -207,11 +207,12 @@ jobs:
- name: Upload coverage to Codecov.io
uses: Wandalen/wretry.action@v1.3.0
with:
action: codecov/codecov-action@v3
action: codecov/codecov-action@v4
# Ensure codecov-action throws an error when it fails to upload
# This allows us to auto-restart the action if an error is thrown
with: |
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
# Try re-running action 5 times max
attempt_limit: 5
# 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
uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main
with:
build_id: dspace-angular
build_id: dspace-angular-dev
image_name: dspace/dspace-angular
dockerfile_path: ./Dockerfile
secrets:

View File

@@ -16,7 +16,7 @@ jobs:
# 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
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.
# 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

View File

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

View File

@@ -17,6 +17,13 @@ ui:
# Trust X-FORWARDED-* headers from proxies (default = true)
useProxies: true
universal:
# Whether to inline "critical" styles into the server-side rendered HTML.
# Determining which styles are critical is a relatively expensive operation;
# this option can be disabled to boost server performance at the expense of
# loading smoothness.
inlineCriticalCss: true
# The REST API server settings
# 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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,13 +2,13 @@ import { testA11y } from 'cypress/support/utils';
describe('Collection Page', () => {
it('should pass accessibility tests', () => {
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
it('should pass accessibility tests', () => {
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
// <ds-collection-page> tag must be loaded
cy.get('ds-collection-page').should('be.visible');
// <ds-collection-page> tag must be loaded
cy.get('ds-collection-page').should('be.visible');
// Analyze <ds-collection-page> for accessibility issues
testA11y('ds-collection-page');
});
// Analyze <ds-collection-page> for accessibility issues
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';
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', () => {
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.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
});
it('should load if you click on "Statistics" from a Collection page', () => {
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.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
});
it('should contain a "Total visits" section', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible');
});
it('should contain a "Total visits" section', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
// 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');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
// 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');
});
it('should pass accessibility tests', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
it('should pass accessibility tests', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
// <ds-collection-statistics-page> tag must be loaded
cy.get('ds-collection-statistics-page').should('be.visible');
// <ds-collection-statistics-page> tag must be loaded
cy.get('ds-collection-statistics-page').should('be.visible');
// 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)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
// 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)
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
testA11y('ds-collection-statistics-page');
});
// Analyze <ds-collection-statistics-page> for accessibility issues
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');
beforeEach(() => {
// All tests start with visiting the Edit Community Page
cy.visit(COMMUNITY_EDIT_PAGE);
// All tests start with visiting the Edit Community Page
cy.visit(COMMUNITY_EDIT_PAGE);
// 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'));
// 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'));
});
describe('Edit Community > Edit Metadata tab', () => {
it('should pass accessibility tests', () => {
// <ds-edit-community> tag must be loaded
cy.get('ds-edit-community').should('be.visible');
it('should pass accessibility tests', () => {
// <ds-edit-community> tag must be loaded
cy.get('ds-edit-community').should('be.visible');
// Analyze <ds-edit-community> for accessibility issues
testA11y('ds-edit-community');
});
// Analyze <ds-edit-community> for accessibility issues
testA11y('ds-edit-community');
});
});
describe('Edit Community > Assign Roles tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="roles"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="roles"]').click();
// <ds-community-roles> tag must be loaded
cy.get('ds-community-roles').should('be.visible');
// <ds-community-roles> tag must be loaded
cy.get('ds-community-roles').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-community-roles');
});
// Analyze for accessibility issues
testA11y('ds-community-roles');
});
});
describe('Edit Community > Curate tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="curate"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="curate"]').click();
// <ds-community-curate> tag must be loaded
cy.get('ds-community-curate').should('be.visible');
// <ds-community-curate> tag must be loaded
cy.get('ds-community-curate').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-community-curate');
});
// Analyze for accessibility issues
testA11y('ds-community-curate');
});
});
describe('Edit Community > Access Control tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="access-control"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="access-control"]').click();
// <ds-community-access-control> tag must be loaded
cy.get('ds-community-access-control').should('be.visible');
// <ds-community-access-control> tag must be loaded
cy.get('ds-community-access-control').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-community-access-control');
});
// Analyze for accessibility issues
testA11y('ds-community-access-control');
});
});
describe('Edit Community > Authorizations tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="authorizations"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="authorizations"]').click();
// <ds-community-authorizations> tag must be loaded
cy.get('ds-community-authorizations').should('be.visible');
// <ds-community-authorizations> tag must be loaded
cy.get('ds-community-authorizations').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-community-authorizations');
});
// Analyze for accessibility issues
testA11y('ds-community-authorizations');
});
});
describe('Edit Community > Delete page', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="delete-button"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="delete-button"]').click();
// <ds-delete-community> tag must be loaded
cy.get('ds-delete-community').should('be.visible');
// <ds-delete-community> tag must be loaded
cy.get('ds-delete-community').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-delete-community');
});
// Analyze for accessibility issues
testA11y('ds-delete-community');
});
});

View File

@@ -2,16 +2,16 @@ import { testA11y } from 'cypress/support/utils';
describe('Community List Page', () => {
it('should pass accessibility tests', () => {
cy.visit('/community-list');
it('should pass accessibility tests', () => {
cy.visit('/community-list');
// <ds-community-list-page> tag must be loaded
cy.get('ds-community-list-page').should('be.visible');
// <ds-community-list-page> tag must be loaded
cy.get('ds-community-list-page').should('be.visible');
// Open every expand button on page, so that we can scan sub-elements as well
cy.get('[data-test="expand-button"]').click({ multiple: true });
// Open every expand button on page, so that we can scan sub-elements as well
cy.get('[data-test="expand-button"]').click({ multiple: true });
// Analyze <ds-community-list-page> for accessibility issues
testA11y('ds-community-list-page');
});
// Analyze <ds-community-list-page> for accessibility issues
testA11y('ds-community-list-page');
});
});

View File

@@ -2,13 +2,13 @@ import { testA11y } from 'cypress/support/utils';
describe('Community Page', () => {
it('should pass accessibility tests', () => {
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
it('should pass accessibility tests', () => {
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
// <ds-community-page> tag must be loaded
cy.get('ds-community-page').should('be.visible');
// <ds-community-page> tag must be loaded
cy.get('ds-community-page').should('be.visible');
// Analyze <ds-community-page> for accessibility issues
testA11y('ds-community-page');
});
// Analyze <ds-community-page> for accessibility issues
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';
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', () => {
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.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
});
it('should load if you click on "Statistics" from a Community page', () => {
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.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
});
it('should contain a "Total visits" section', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible');
});
it('should contain a "Total visits" section', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
// 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');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
// 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');
});
it('should pass accessibility tests', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
it('should pass accessibility tests', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
// <ds-community-statistics-page> tag must be loaded
cy.get('ds-community-statistics-page').should('be.visible');
// <ds-community-statistics-page> tag must be loaded
cy.get('ds-community-statistics-page').should('be.visible');
// 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)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
// 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)
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
testA11y('ds-community-statistics-page');
});
// Analyze <ds-community-statistics-page> for accessibility issues
testA11y('ds-community-statistics-page');
});
});

View File

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

View File

@@ -1,13 +1,13 @@
import { testA11y } from 'cypress/support/utils';
describe('Header', () => {
it('should pass accessibility tests', () => {
cy.visit('/');
it('should pass accessibility tests', () => {
cy.visit('/');
// Header must first be visible
cy.get('ds-header').should('be.visible');
// Header must first be visible
cy.get('ds-header').should('be.visible');
// Analyze <ds-header> for accessibility
testA11y('ds-header');
});
// Analyze <ds-header> for accessibility
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 { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
import { testA11y } from 'cypress/support/utils';
describe('Site Statistics Page', () => {
it('should load if you click on "Statistics" from homepage', () => {
cy.visit('/');
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', '/statistics');
});
it('should load if you click on "Statistics" from homepage', () => {
cy.visit('/');
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', '/statistics');
});
it('should pass accessibility tests', () => {
// 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');
it('should pass accessibility tests', () => {
// 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.visit('/statistics');
cy.visit('/statistics');
// <ds-site-statistics-page> tag must be visable
cy.get('ds-site-statistics-page').should('be.visible');
// <ds-site-statistics-page> tag must be visable
cy.get('ds-site-statistics-page').should('be.visible');
// 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)
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.
cy.wait(500);
// 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)
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.
cy.wait(500);
// Analyze <ds-site-statistics-page> for accessibility issues
testA11y('ds-site-statistics-page');
});
// Analyze <ds-site-statistics-page> for accessibility issues
testA11y('ds-site-statistics-page');
});
});

View File

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

View File

@@ -1,32 +1,32 @@
import { testA11y } from 'cypress/support/utils';
describe('Item Page', () => {
const ITEMPAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
const ENTITYPAGE = '/entities/publication/'.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'));
// 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', () => {
cy.visit(ITEMPAGE);
cy.location('pathname').should('eq', ENTITYPAGE);
});
// 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', () => {
cy.visit(ITEMPAGE);
cy.location('pathname').should('eq', ENTITYPAGE);
});
it('should pass accessibility tests', () => {
cy.visit(ENTITYPAGE);
it('should pass accessibility tests', () => {
cy.visit(ENTITYPAGE);
// <ds-item-page> tag must be loaded
cy.get('ds-item-page').should('be.visible');
// <ds-item-page> tag must be loaded
cy.get('ds-item-page').should('be.visible');
// Analyze <ds-item-page> for accessibility issues
testA11y('ds-item-page');
});
// Analyze <ds-item-page> for accessibility issues
testA11y('ds-item-page');
});
it('should pass accessibility tests on full item page', () => {
cy.visit(ENTITYPAGE + '/full');
it('should pass accessibility tests on full item page', () => {
cy.visit(ENTITYPAGE + '/full');
// <ds-full-item-page> tag must be loaded
cy.get('ds-full-item-page').should('be.visible');
// <ds-full-item-page> tag must be loaded
cy.get('ds-full-item-page').should('be.visible');
// Analyze <ds-full-item-page> for accessibility issues
testA11y('ds-full-item-page');
});
// Analyze <ds-full-item-page> for accessibility issues
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';
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', () => {
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
});
it('should load if you click on "Statistics" from an Item/Entity page', () => {
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
});
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
cy.visit(ITEMSTATISTICSPAGE);
cy.get('ds-item-statistics-page').should('be.visible');
cy.get('ds-item-page').should('not.exist');
});
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
cy.visit(ITEMSTATISTICSPAGE);
cy.get('ds-item-statistics-page').should('be.visible');
cy.get('ds-item-page').should('not.exist');
});
it('should contain a "Total visits" section', () => {
cy.visit(ITEMSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible');
});
it('should contain a "Total visits" section', () => {
cy.visit(ITEMSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(ITEMSTATISTICSPAGE);
// 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');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(ITEMSTATISTICSPAGE);
// 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');
});
it('should pass accessibility tests', () => {
cy.visit(ITEMSTATISTICSPAGE);
it('should pass accessibility tests', () => {
cy.visit(ITEMSTATISTICSPAGE);
// <ds-item-statistics-page> tag must be loaded
cy.get('ds-item-statistics-page').should('be.visible');
// <ds-item-statistics-page> tag must be loaded
cy.get('ds-item-statistics-page').should('be.visible');
// 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)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
// 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)
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
testA11y('ds-item-statistics-page');
});
// Analyze <ds-item-statistics-page> for accessibility issues
testA11y('ds-item-statistics-page');
});
});

View File

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

View File

@@ -1,134 +1,134 @@
import { testA11y } from 'cypress/support/utils';
describe('My DSpace page', () => {
it('should display recent submissions and pass accessibility tests', () => {
cy.visit('/mydspace');
it('should display recent submissions and pass 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'));
// 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');
cy.get('ds-my-dspace-page').should('be.visible');
// At least one recent submission should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
// At least one recent submission should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
// Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well)
cy.get('.filter-toggle').click({ multiple: true });
// Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well)
cy.get('.filter-toggle').click({ multiple: true });
// Analyze <ds-my-dspace-page> for accessibility issues
testA11y('ds-my-dspace-page');
// Analyze <ds-my-dspace-page> for accessibility issues
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', () => {
cy.visit('/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'));
// 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');
// 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
cy.get('ds-search-sidebar [data-test="detail-view"]').click();
// New URL should include /import-external, as we've moved to the import page
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
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 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');
});
// Test for accessibility issues
testA11y('ds-submission-import-external');
});
});

View File

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

View File

@@ -1,64 +1,64 @@
const page = {
fillOutQueryInNavBar(query) {
// Click the magnifying glass
cy.get('ds-themed-header [data-test="header-search-icon"]').click();
// Fill out a query in input that appears
cy.get('ds-themed-header [data-test="header-search-box"]').type(query);
},
submitQueryByPressingEnter() {
cy.get('ds-themed-header [data-test="header-search-box"]').type('{enter}');
},
submitQueryByPressingIcon() {
cy.get('ds-themed-header [data-test="header-search-icon"]').click();
}
fillOutQueryInNavBar(query) {
// Click the magnifying glass
cy.get('ds-themed-header [data-test="header-search-icon"]').click();
// Fill out a query in input that appears
cy.get('ds-themed-header [data-test="header-search-box"]').type(query);
},
submitQueryByPressingEnter() {
cy.get('ds-themed-header [data-test="header-search-box"]').type('{enter}');
},
submitQueryByPressingIcon() {
cy.get('ds-themed-header [data-test="header-search-icon"]').click();
},
};
describe('Search from Navigation Bar', () => {
// NOTE: these tests currently assume this query will return results!
const query = Cypress.env('DSPACE_TEST_SEARCH_TERM');
// NOTE: these tests currently assume this query will return results!
const query = Cypress.env('DSPACE_TEST_SEARCH_TERM');
it('should go to search page with correct query if submitted (from home)', () => {
cy.visit('/');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter();
// New URL should include query param
cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
});
it('should go to search page with correct query if submitted (from home)', () => {
cy.visit('/');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter();
// New URL should include query param
cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
});
it('should go to search page with correct query if submitted (from search)', () => {
cy.visit('/search');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter();
// New URL should include query param
cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
});
it('should go to search page with correct query if submitted (from search)', () => {
cy.visit('/search');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter();
// New URL should include query param
cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
});
it('should allow user to also submit query by clicking icon', () => {
cy.visit('/');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingIcon();
// New URL should include query param
cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
});
it('should allow user to also submit query by clicking icon', () => {
cy.visit('/');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingIcon();
// New URL should include query param
cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed
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 { Options } from 'cypress-axe';
describe('Search Page', () => {
// NOTE: these tests currently assume this query will return results!
const query = Cypress.env('DSPACE_TEST_SEARCH_TERM');
// NOTE: these tests currently assume this query will return results!
const query = Cypress.env('DSPACE_TEST_SEARCH_TERM');
it('should redirect to the correct url when query was set and submit button was triggered', () => {
const queryString = 'Another interesting query string';
cy.visit('/search');
// Type query in searchbox & click search button
cy.get('[data-test="search-box"]').type(queryString);
cy.get('[data-test="search-button"]').click();
cy.url().should('include', 'query=' + encodeURI(queryString));
});
it('should redirect to the correct url when query was set and submit button was triggered', () => {
const queryString = 'Another interesting query string';
cy.visit('/search');
// Type query in searchbox & click search button
cy.get('[data-test="search-box"]').type(queryString);
cy.get('[data-test="search-button"]').click();
cy.url().should('include', 'query=' + encodeURI(queryString));
});
it('should load results and pass accessibility tests', () => {
cy.visit('/search?query='.concat(query));
cy.get('[data-test="search-box"]').should('have.value', query);
it('should load results and pass accessibility tests', () => {
cy.visit('/search?query='.concat(query));
cy.get('[data-test="search-box"]').should('have.value', query);
// <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('be.visible');
// <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('be.visible');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
// Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well)
cy.get('[data-test="filter-toggle"]').click({ multiple: true });
// Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well)
cy.get('[data-test="filter-toggle"]').click({ multiple: true });
// Analyze <ds-search-page> for accessibility issues
testA11y('ds-search-page');
});
// Analyze <ds-search-page> for accessibility issues
testA11y('ds-search-page');
});
it('should have a working grid view that passes accessibility tests', () => {
cy.visit('/search?query='.concat(query));
it('should have a working grid view that passes accessibility tests', () => {
cy.visit('/search?query='.concat(query));
// Click button in sidebar to display grid view
cy.get('ds-search-sidebar [data-test="grid-view"]').click();
// Click button in sidebar to display grid view
cy.get('ds-search-sidebar [data-test="grid-view"]').click();
// <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('be.visible');
// <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('be.visible');
// At least one grid object (card) should be displayed
cy.get('[data-test="grid-object"]').should('be.visible');
// At least one grid object (card) should be displayed
cy.get('[data-test="grid-object"]').should('be.visible');
// Analyze <ds-search-page> for accessibility issues
testA11y('ds-search-page',
// Analyze <ds-search-page> for accessibility issues
testA11y('ds-search-page',
{
rules: {
// Card titles fail this test currently
'heading-order': { enabled: false }
}
} as Options
);
});
rules: {
// Card titles fail this test currently
'heading-order': { enabled: false },
},
} as Options,
);
});
});

View File

@@ -4,224 +4,224 @@ import { Options } from 'cypress-axe';
describe('New Submission page', () => {
// 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', () => {
// 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'));
// 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', () => {
// 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'));
// 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'));
// 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'));
// Should redirect to /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
// Should redirect to /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');
// The Submission edit form tag 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
cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME'));
// 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'));
// 4 sections should be visible by default
cy.get('div#section_traditionalpageone').should('be.visible');
cy.get('div#section_traditionalpagetwo').should('be.visible');
cy.get('div#section_upload').should('be.visible');
cy.get('div#section_license').should('be.visible');
// 4 sections should be visible by default
cy.get('div#section_traditionalpageone').should('be.visible');
cy.get('div#section_traditionalpagetwo').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',
// Test entire page for accessibility
testA11y('ds-submission-edit',
{
rules: {
// Author & Subject fields have invalid "aria-multiline" attrs.
// See https://github.com/DSpace/dspace-angular/issues/1272
'aria-allowed-attr': { enabled: false },
// All panels are accordians & fail "aria-required-children" and "nested-interactive".
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
'aria-required-children': { enabled: false },
'nested-interactive': { enabled: false },
// 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
'select-name': { enabled: false },
}
rules: {
// Author & Subject fields have invalid "aria-multiline" attrs.
// See https://github.com/DSpace/dspace-angular/issues/1272
'aria-allowed-attr': { enabled: false },
// All panels are accordians & fail "aria-required-children" and "nested-interactive".
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
'aria-required-children': { enabled: false },
'nested-interactive': { enabled: false },
// 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
'select-name': { enabled: false },
},
} as Options
);
} as Options,
);
// Discard button should work
// Clicking it will display a confirmation, which we will confirm with another click
cy.get('button#discard').click();
cy.get('button#discard_submit').click();
// Discard button should work
// Clicking it will display a confirmation, which we will confirm with another click
cy.get('button#discard').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', () => {
// Create a new submission
cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none'));
// Wait for upload to complete before proceeding
cy.wait('@upload');
// 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'));
// Wait for deposit button to not be disabled & click it.
cy.get('button#deposit').should('not.be.disabled').click();
// Attempt an immediate deposit without filling out any fields
cy.get('button#deposit').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');
});
// 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');
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');
// 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');
// 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'));
// Title field should have class "is-invalid" applied, as it's required
cy.get('input#dc_title').should('have.class', 'is-invalid');
// 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();
// Date Year field should also have "is-valid" class
cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid');
// This should display the <ds-create-item-parent-selector> (popup window)
cy.get('ds-create-item-parent-selector').should('be.visible');
// 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];
// 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'));
// Even though form is incomplete, the "Save for Later" button should still work
cy.get('button#saveForLater').click();
// 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();
// "Save for Later" should send us to MyDSpace
cy.url().should('include', '/mydspace');
// New URL should include /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
// 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});
// The Submission edit form tag should be visible
cy.get('ds-submission-edit').should('be.visible');
// 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();
// 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'));
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// 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');
// 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'
});
// 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',
// Test entire page for accessibility
testA11y('ds-submission-edit',
{
rules: {
// All panels are accordians & fail "aria-required-children" and "nested-interactive".
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
'aria-required-children': { enabled: false },
'nested-interactive': { enabled: false },
}
rules: {
// All panels are accordians & fail "aria-required-children" and "nested-interactive".
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
'aria-required-children': { enabled: false },
'nested-interactive': { enabled: false },
},
} as Options
);
} as Options,
);
// Click the lookup button next to "Publication" field
cy.get('button[data-test="lookup-button"]').click();
// Click the lookup button next to "Publication" field
cy.get('button[data-test="lookup-button"]').click();
// A popup modal window should be visible
cy.get('ds-dynamic-lookup-relation-modal').should('be.visible');
// A popup modal window should be visible
cy.get('ds-dynamic-lookup-relation-modal').should('be.visible');
// Popup modal should also pass accessibility tests
//testA11y('ds-dynamic-lookup-relation-modal');
testA11y({
include: ['ds-dynamic-lookup-relation-modal'],
exclude: [
['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();
// Popup modal should also pass accessibility tests
//testA11y('ds-dynamic-lookup-relation-modal');
testA11y({
include: ['ds-dynamic-lookup-relation-modal'],
exclude: [
['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();
});
});

View File

@@ -9,51 +9,51 @@ let REST_DOMAIN: string;
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
// For more info, visit https://on.cypress.io/plugins-api
module.exports = (on, config) => {
on('task', {
// 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
log(message: string) {
console.log(message);
return null;
},
table(message: string) {
console.table(message);
return null;
},
// 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.
// 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.
readUIConfig() {
// 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"
if (fs.existsSync('./src/assets/config.json')) {
return fs.readFileSync('./src/assets/config.json', 'utf8');
// 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
} else if (fs.existsSync('./dist/browser/assets/config.json')) {
return fs.readFileSync('./dist/browser/assets/config.json', 'utf8');
}
on('task', {
// 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
log(message: string) {
console.log(message);
return null;
},
table(message: string) {
console.table(message);
return null;
},
// 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.
// 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.
readUIConfig() {
// 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"
if (fs.existsSync('./src/assets/config.json')) {
return fs.readFileSync('./src/assets/config.json', 'utf8');
// 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
} else if (fs.existsSync('./dist/browser/assets/config.json')) {
return fs.readFileSync('./dist/browser/assets/config.json', 'utf8');
}
return null;
},
// Save value of REST Base URL, looked up before all tests.
// This allows other tests to use it easily via getRestBaseURL() below.
saveRestBaseURL(url: string) {
return (REST_BASE_URL = url);
},
// Retrieve currently saved value of REST Base URL
getRestBaseURL() {
return REST_BASE_URL ;
},
// Save value of REST Domain, looked up before all tests.
// This allows other tests to use it easily via getRestBaseDomain() below.
saveRestBaseDomain(domain: string) {
return (REST_DOMAIN = domain);
},
// Retrieve currently saved value of REST Domain
getRestBaseDomain() {
return REST_DOMAIN ;
}
});
return null;
},
// Save value of REST Base URL, looked up before all tests.
// This allows other tests to use it easily via getRestBaseURL() below.
saveRestBaseURL(url: string) {
return (REST_BASE_URL = url);
},
// Retrieve currently saved value of REST Base URL
getRestBaseURL() {
return REST_BASE_URL ;
},
// Save value of REST Domain, looked up before all tests.
// This allows other tests to use it easily via getRestBaseDomain() below.
saveRestBaseDomain(domain: string) {
return (REST_DOMAIN = domain);
},
// Retrieve currently saved value of REST Domain
getRestBaseDomain() {
return REST_DOMAIN ;
},
});
};

View File

@@ -3,8 +3,14 @@
// 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 { DSPACE_XSRF_COOKIE, XSRF_REQUEST_HEADER } from 'src/app/core/xsrf/xsrf.constants';
import {
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';
// Declare Cypress namespace to help with Intellisense & code completion in IDEs
@@ -57,33 +63,33 @@ declare global {
* @param password password to login as
*/
function login(email: string, password: string): void {
// Create a fake CSRF cookie/token to use in POST
cy.createCSRFCookie().then((csrfToken: string) => {
// get our REST API's base URL, also needed for POST
cy.task('getRestBaseURL').then((baseRestUrl: string) => {
// Now, send login POST request including that CSRF token
cy.request({
method: 'POST',
url: baseRestUrl + '/api/authn/login',
headers: { [XSRF_REQUEST_HEADER]: csrfToken},
form: true, // indicates the body should be form urlencoded
body: { user: email, password: password }
}).then((resp) => {
// We expect a successful login
expect(resp.status).to.eq(200);
// We expect to have a valid authorization header returned (with our auth token)
expect(resp.headers).to.have.property('authorization');
// Create a fake CSRF cookie/token to use in POST
cy.createCSRFCookie().then((csrfToken: string) => {
// get our REST API's base URL, also needed for POST
cy.task('getRestBaseURL').then((baseRestUrl: string) => {
// Now, send login POST request including that CSRF token
cy.request({
method: 'POST',
url: baseRestUrl + '/api/authn/login',
headers: { [XSRF_REQUEST_HEADER]: csrfToken },
form: true, // indicates the body should be form urlencoded
body: { user: email, password: password },
}).then((resp) => {
// We expect a successful login
expect(resp.status).to.eq(200);
// We expect to have a valid authorization header returned (with our auth token)
expect(resp.headers).to.have.property('authorization');
// Initialize our AuthTokenInfo object from the authorization header.
const authheader = resp.headers.authorization as string;
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
// Initialize our AuthTokenInfo object from the authorization header.
const authheader = resp.headers.authorization as string;
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
// Save our AuthTokenInfo object to our dsAuthInfo UI cookie
// This ensures the UI will recognize we are logged in on next "visit()"
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
});
});
// Save our AuthTokenInfo object to our dsAuthInfo UI cookie
// This ensures the UI will recognize we are logged in on next "visit()"
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
});
});
});
}
// Add as a Cypress command (i.e. assign to 'cy.login')
Cypress.Commands.add('login', login);
@@ -94,12 +100,12 @@ Cypress.Commands.add('login', login);
* @param password password to login as
*/
function loginViaForm(email: string, password: string): void {
// Enter email
cy.get('ds-log-in [data-test="email"]').type(email);
// Enter password
cy.get('ds-log-in [data-test="password"]').type(password);
// Click login button
cy.get('ds-log-in [data-test="login-button"]').click();
// Enter email
cy.get('ds-log-in [data-test="email"]').type(email);
// Enter password
cy.get('ds-log-in [data-test="password"]').type(password);
// Click login button
cy.get('ds-log-in [data-test="login-button"]').click();
}
// Add as a Cypress command (i.e. assign to 'cy.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")
*/
function generateViewEvent(uuid: string, dsoType: string): void {
// Create a fake CSRF cookie/token to use in POST
cy.createCSRFCookie().then((csrfToken: string) => {
// get our REST API's base URL, also needed for POST
cy.task('getRestBaseURL').then((baseRestUrl: string) => {
// Now, send 'statistics/viewevents' POST request including that fake CSRF token in required header
cy.request({
method: 'POST',
url: baseRestUrl + '/api/statistics/viewevents',
headers: {
[XSRF_REQUEST_HEADER] : csrfToken,
// use a known public IP address to avoid being seen as a "bot"
'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"
'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
body: { targetId: uuid, targetType: dsoType },
}).then((resp) => {
// We expect a 201 (which means statistics event was created)
expect(resp.status).to.eq(201);
});
});
// Create a fake CSRF cookie/token to use in POST
cy.createCSRFCookie().then((csrfToken: string) => {
// get our REST API's base URL, also needed for POST
cy.task('getRestBaseURL').then((baseRestUrl: string) => {
// Now, send 'statistics/viewevents' POST request including that fake CSRF token in required header
cy.request({
method: 'POST',
url: baseRestUrl + '/api/statistics/viewevents',
headers: {
[XSRF_REQUEST_HEADER] : csrfToken,
// use a known public IP address to avoid being seen as a "bot"
'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"
'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
body: { targetId: uuid, targetType: dsoType },
}).then((resp) => {
// We expect a 201 (which means statistics event was created)
expect(resp.status).to.eq(201);
});
});
});
}
// Add as a Cypress command (i.e. assign to 'cy.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
*/
function createCSRFCookie(): Cypress.Chainable {
// Generate a new token which is a random UUID
const csrfToken: string = uuidv4();
// Generate a new token which is a random UUID
const csrfToken: string = uuidv4();
// Save it to our required cookie
cy.task('getRestBaseDomain').then((baseDomain: string) => {
// Create a fake CSRF Token. Set it in the required server-side cookie
cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain });
});
// Save it to our required cookie
cy.task('getRestBaseDomain').then((baseDomain: string) => {
// Create a fake CSRF Token. Set it in the required server-side cookie
cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain });
});
// return the generated token wrapped in a chainable
return cy.wrap(csrfToken);
// return the generated token wrapped in a chainable
return cy.wrap(csrfToken);
}
// Add as a Cypress command (i.e. assign to 'cy.createCSRFCookie')
Cypress.Commands.add('createCSRFCookie', createCSRFCookie);

View File

@@ -15,12 +15,11 @@
// Import all custom Commands (from commands.ts) for all tests
import './commands';
// Import Cypress Axe tools for all tests
// https://github.com/component-driven/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
before(() => {
@@ -35,18 +34,18 @@ before(() => {
// Find URL of our REST API & save to global variable via task
let baseRestUrl = FALLBACK_TEST_REST_BASE_URL;
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 {
baseRestUrl = config.rest.baseUrl;
baseRestUrl = config.rest.baseUrl;
}
cy.task('saveRestBaseURL', baseRestUrl);
// Find domain of our REST API & save to global variable via task.
let baseDomain = FALLBACK_TEST_REST_DOMAIN;
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 {
baseDomain = config.rest.host;
baseDomain = config.rest.host;
}
cy.task('saveRestBaseDomain', baseDomain);
@@ -55,12 +54,12 @@ before(() => {
// Runs once before the first test in each "block"
beforeEach(() => {
// 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.
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}');
// 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.
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
cy.clearCookie(DSPACE_XSRF_COOKIE);
// Remove any CSRF cookies saved from prior tests
cy.clearCookie(DSPACE_XSRF_COOKIE);
});
// 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
// Borrowed from https://github.com/component-driven/cypress-axe#in-your-spec-file
function terminalLog(violations: Result[]) {
cy.task(
'log',
`${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected`
);
// pluck specific keys to keep the table readable
const violationData = violations.map(
({ id, impact, description, helpUrl, nodes }) => ({
id,
impact,
description,
helpUrl,
nodes: nodes.length,
html: nodes.map(node => node.html)
})
);
cy.task(
'log',
`${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected`,
);
// pluck specific keys to keep the table readable
const violationData = violations.map(
({ id, impact, description, helpUrl, nodes }) => ({
id,
impact,
description,
helpUrl,
nodes: nodes.length,
html: nodes.map(node => node.html),
}),
);
// Print violations as an array, since 'node.html' above often breaks table alignment
cy.task('log', violationData);
// Optionally, uncomment to print as a table
// cy.task('table', violationData);
// Print violations as an array, since 'node.html' above often breaks table alignment
cy.task('log', violationData);
// Optionally, uncomment to print as a table
// 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)
// This method MUST be called after cy.visit(), as cy.injectAxe() must be called after page load
export const testA11y = (context?: any, options?: Options) => {
cy.injectAxe();
cy.configureAxe({
rules: [
// 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
{ id: 'color-contrast', enabled: false },
]
});
cy.checkA11y(context, options, terminalLog);
cy.injectAxe();
cy.configureAxe({
rules: [
// 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
{ id: 'color-contrast', enabled: false },
],
});
cy.checkA11y(context, options, terminalLog);
};

View File

@@ -55,28 +55,28 @@
"ts-node": "10.2.1"
},
"dependencies": {
"@angular/animations": "^15.2.8",
"@angular/cdk": "^15.2.8",
"@angular/common": "^15.2.8",
"@angular/compiler": "^15.2.8",
"@angular/core": "^15.2.8",
"@angular/forms": "^15.2.8",
"@angular/localize": "15.2.8",
"@angular/platform-browser": "^15.2.8",
"@angular/platform-browser-dynamic": "^15.2.8",
"@angular/platform-server": "^15.2.8",
"@angular/router": "^15.2.8",
"@angular/animations": "^16.2.12",
"@angular/cdk": "^16.2.12",
"@angular/common": "^16.2.12",
"@angular/compiler": "^16.2.12",
"@angular/core": "^16.2.12",
"@angular/forms": "^16.2.12",
"@angular/localize": "16.2.12",
"@angular/platform-browser": "^16.2.12",
"@angular/platform-browser-dynamic": "^16.2.12",
"@angular/platform-server": "^16.2.12",
"@angular/router": "^16.2.12",
"@babel/runtime": "7.21.0",
"@kolkov/ngx-gallery": "^2.0.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.11.3",
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@ng-dynamic-forms/core": "^15.0.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0",
"@ngrx/effects": "^15.4.0",
"@ngrx/router-store": "^15.4.0",
"@ngrx/store": "^15.4.0",
"@nguniversal/express-engine": "^15.2.1",
"@ng-dynamic-forms/core": "^16.0.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
"@ngrx/effects": "^16.3.0",
"@ngrx/router-store": "^16.3.0",
"@ngrx/store": "^16.3.0",
"@nguniversal/express-engine": "^16.2.0",
"@ngx-translate/core": "^14.0.0",
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
"@types/grecaptcha": "^3.0.4",
@@ -94,7 +94,7 @@
"date-fns-tz": "^1.3.7",
"deepmerge": "^4.3.1",
"ejs": "^3.1.9",
"express": "^4.18.2",
"express": "^4.19.2",
"express-rate-limit": "^5.1.3",
"fast-json-patch": "^3.1.1",
"filesize": "^6.1.0",
@@ -110,17 +110,15 @@
"lodash": "^4.17.21",
"lru-cache": "^7.14.1",
"markdown-it": "^13.0.1",
"markdown-it-mathjax3": "^4.3.2",
"mirador": "^3.3.0",
"mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.11.0",
"morgan": "^1.10.0",
"ng-mocks": "^14.10.0",
"ng2-file-upload": "1.4.0",
"ng2-file-upload": "5.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-sortablejs": "^11.1.0",
"ngx-ui-switch": "^14.1.0",
"nouislider": "^15.7.1",
"pem": "1.14.7",
@@ -132,24 +130,24 @@
"sortablejs": "1.15.0",
"uuid": "^8.3.2",
"webfontloader": "1.6.28",
"zone.js": "~0.11.5"
"zone.js": "~0.13.3"
},
"devDependencies": {
"@angular-builders/custom-webpack": "~15.0.0",
"@angular-devkit/build-angular": "^15.2.6",
"@angular-eslint/builder": "15.2.1",
"@angular-eslint/eslint-plugin": "15.2.1",
"@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "15.2.1",
"@angular/cli": "^16.0.4",
"@angular/compiler-cli": "^15.2.8",
"@angular/language-service": "^15.2.8",
"@angular-builders/custom-webpack": "~16.0.0",
"@angular-devkit/build-angular": "^16.2.12",
"@angular-eslint/builder": "16.3.1",
"@angular-eslint/eslint-plugin": "16.3.1",
"@angular-eslint/eslint-plugin-template": "16.3.1",
"@angular-eslint/schematics": "16.3.1",
"@angular-eslint/template-parser": "16.3.1",
"@angular/cli": "^16.2.12",
"@angular/compiler-cli": "^16.2.12",
"@angular/language-service": "^16.2.12",
"@cypress/schematic": "^1.5.0",
"@fortawesome/fontawesome-free": "^6.4.0",
"@ngrx/store-devtools": "^15.4.0",
"@ngtools/webpack": "^15.2.6",
"@nguniversal/builders": "^15.2.1",
"@ngrx/store-devtools": "^16.3.0",
"@ngtools/webpack": "^16.2.12",
"@nguniversal/builders": "^16.2.0",
"@types/deep-freeze": "0.1.2",
"@types/ejs": "^3.1.2",
"@types/express": "^4.17.17",
@@ -170,9 +168,12 @@
"eslint": "^8.39.0",
"eslint-plugin-deprecation": "^1.4.1",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-import-newlines": "^1.3.1",
"eslint-plugin-jsdoc": "^45.0.0",
"eslint-plugin-jsonc": "^2.6.0",
"eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-rxjs": "^5.0.3",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"express-static-gzip": "^2.1.7",
"jasmine-core": "^3.8.0",
@@ -183,7 +184,7 @@
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"karma-mocha-reporter": "2.2.5",
"ngx-mask": "^13.1.7",
"ngx-mask": "14.2.4",
"nodemon": "^2.0.22",
"postcss": "^8.4",
"postcss-apply": "0.12.0",
@@ -199,7 +200,7 @@
"sass-loader": "^12.6.0",
"sass-resources-loader": "^2.2.5",
"ts-node": "^8.10.2",
"typescript": "~4.8.4",
"typescript": "~4.9.3",
"webpack": "5.76.1",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^4.2.0",

View File

@@ -48,7 +48,7 @@ import { hasNoValue, hasValue } from './src/app/shared/empty.util';
import { UIServerConfig } from './src/config/ui-server-config.interface';
import { ServerAppModule } from './src/main.server';
import bootstrap from './src/main.server';
import { buildAppConfig } from './src/config/config.server';
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
@@ -130,7 +130,8 @@ export function app() {
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', (_, options, callback) =>
ngExpressEngine({
bootstrap: ServerAppModule,
bootstrap,
inlineCriticalCss: environment.universal.inlineCriticalCss,
providers: [
{
provide: REQUEST,
@@ -142,10 +143,10 @@ export function app() {
},
{
provide: APP_CONFIG,
useValue: environment
}
]
})(_, (options as any), callback)
useValue: environment,
},
],
})(_, (options as any), callback),
);
server.engine('ejs', ejs.renderFile);
@@ -162,7 +163,7 @@ export function app() {
server.get('/robots.txt', (req, res) => {
res.setHeader('content-type', 'text/plain');
res.render('assets/robots.txt.ejs', {
'origin': req.protocol + '://' + req.headers.host
'origin': req.protocol + '://' + req.headers.host,
});
});
@@ -177,7 +178,7 @@ export function app() {
router.use('/sitemap**', createProxyMiddleware({
target: `${environment.rest.baseUrl}/sitemaps`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true
changeOrigin: true,
}));
/**
@@ -186,7 +187,7 @@ export function app() {
router.use('/signposting**', createProxyMiddleware({
target: `${environment.rest.baseUrl}`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true
changeOrigin: true,
}));
/**
@@ -197,7 +198,7 @@ export function app() {
const RateLimit = require('express-rate-limit');
const limiter = new RateLimit({
windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs,
max: (environment.ui as UIServerConfig).rateLimiter.max
max: (environment.ui as UIServerConfig).rateLimiter.max,
});
server.use(limiter);
}
@@ -325,7 +326,7 @@ function initCache() {
botCache = new LRU( {
max: environment.cache.serverSide.botCache.max,
ttl: environment.cache.serverSide.botCache.timeToLive,
allowStale: environment.cache.serverSide.botCache.allowStale
allowStale: environment.cache.serverSide.botCache.allowStale,
});
}
@@ -337,7 +338,7 @@ function initCache() {
anonymousCache = new LRU( {
max: environment.cache.serverSide.anonymousCache.max,
ttl: environment.cache.serverSide.anonymousCache.timeToLive,
allowStale: environment.cache.serverSide.anonymousCache.allowStale
allowStale: environment.cache.serverSide.anonymousCache.allowStale,
});
}
}
@@ -415,7 +416,7 @@ function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, r
const key = getCacheKey(req);
// Check if this page is in our cache
let cachedCopy = cache.get(key);
const cachedCopy = cache.get(key);
if (cachedCopy) {
if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); }
@@ -529,20 +530,20 @@ function serverStarted() {
function createHttpsServer(keys) {
const listener = createServer({
key: keys.serviceKey,
cert: keys.certificate
cert: keys.certificate,
}, app).listen(environment.ui.port, environment.ui.host, () => {
serverStarted();
});
// Graceful shutdown when signalled
const terminator = createHttpTerminator({server: listener});
const terminator = createHttpTerminator({ server: listener });
process.on('SIGINT', () => {
void (async ()=> {
console.debug('Closing HTTPS server on signal');
await terminator.terminate().catch(e => { console.error(e); });
console.debug('HTTPS server closed');
})();
});
void (async ()=> {
console.debug('Closing HTTPS server on signal');
await terminator.terminate().catch(e => { console.error(e); });
console.debug('HTTPS server closed');
})();
});
}
/**
@@ -559,14 +560,14 @@ function run() {
});
// Graceful shutdown when signalled
const terminator = createHttpTerminator({server: listener});
const terminator = createHttpTerminator({ server: listener });
process.on('SIGINT', () => {
void (async () => {
console.debug('Closing HTTP server on signal');
await terminator.terminate().catch(e => { console.error(e); });
console.debug('HTTP server closed.');return undefined;
})();
});
void (async () => {
console.debug('Closing HTTP server on signal');
await terminator.terminate().catch(e => { console.error(e); });
console.debug('HTTP server closed.');return undefined;
})();
});
}
function start() {
@@ -597,7 +598,7 @@ function start() {
if (serviceKey && certificate) {
createHttpsServer({
serviceKey: serviceKey,
certificate: certificate
certificate: certificate,
});
} 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.');
@@ -606,7 +607,7 @@ function start() {
createCertificate({
days: 1,
selfSigned: true
selfSigned: true,
}, (error, keys) => {
createHttpsServer(keys);
});
@@ -627,7 +628,7 @@ function healthCheck(req, res) {
})
.catch((error) => {
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,5 +1,5 @@
import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getAccessControlModuleRoute } from '../app-routing-paths';
import { URLCombiner } from '../core/url-combiner/url-combiner';
export const EPERSON_PATH = 'epeople';

View File

@@ -1,94 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
import { EPERSON_PATH, GROUP_PATH } from './access-control-routing-paths';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { GroupPageGuard } from './group-registry/group-page.guard';
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 { BulkAccessComponent } from './bulk-access/bulk-access.component';
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
import { EPersonResolver } from './epeople-registry/eperson-resolver.service';
@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,67 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { AccessControlRoutingModule } from './access-control-routing.module';
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';
import { FormModule } from '../shared/form/form.module';
import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
import { AbstractControl } from '@angular/forms';
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
import { BulkAccessBrowseComponent } from './bulk-access/browse/bulk-access-browse.component';
import { BulkAccessSettingsComponent } from './bulk-access/settings/bulk-access-settings.component';
import { SearchModule } from '../shared/search/search.module';
import { AccessControlFormModule } from '../shared/access-control-form-container/access-control-form.module';
/**
* 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

@@ -1,16 +1,28 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { of } from 'rxjs';
import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
import {
ComponentFixture,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import {
NgbAccordionModule,
NgbNavModule,
} from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs';
import { BulkAccessBrowseComponent } from './bulk-access-browse.component';
import { buildPaginatedList } from '../../../core/data/paginated-list.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 { SelectableObject } from '../../../shared/object-list/selectable-list/selectable-list.service.spec';
import { PageInfo } from '../../../core/shared/page-info.model';
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
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';
describe('BulkAccessBrowseComponent', () => {
let component: BulkAccessBrowseComponent;
@@ -23,7 +35,7 @@ describe('BulkAccessBrowseComponent', () => {
const selected1 = new SelectableObject(value1);
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']);
beforeEach(waitForAsync(() => {
@@ -31,14 +43,28 @@ describe('BulkAccessBrowseComponent', () => {
imports: [
NgbAccordionModule,
NgbNavModule,
TranslateModule.forRoot()
TranslateModule.forRoot(),
BulkAccessBrowseComponent,
],
providers: [
{ provide: SelectableListService, useValue: selectableListService },
{ provide: ThemeService, useValue: getMockThemeService() },
],
declarations: [BulkAccessBrowseComponent],
providers: [ { provide: SelectableListService, useValue: selectableListService }, ],
schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
NO_ERRORS_SCHEMA,
],
})
.overrideComponent(BulkAccessBrowseComponent, {
remove: {
imports: [
PaginationComponent,
ThemedSearchComponent,
SelectableListItemControlComponent,
ListableObjectComponentLoaderComponent,
],
},
})
.compileComponents();
}));
beforeEach(() => {
@@ -72,8 +98,8 @@ describe('BulkAccessBrowseComponent', () => {
'elementsPerPage': 5,
'totalElements': 2,
'totalPages': 1,
'currentPage': 1
}), [selected1, selected2]) ;
'currentPage': 1,
}), [selected1, selected2]);
const rd = createSuccessfulRemoteDataObject(list);
expect(component.objectsSelected$.value).toEqual(rd);

View File

@@ -1,19 +1,48 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import {
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import {
Component,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import {
NgbAccordionModule,
NgbNavModule,
} from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { NgxPaginationModule } from 'ngx-pagination';
import {
BehaviorSubject,
Subscription,
} from 'rxjs';
import {
distinctUntilChanged,
map,
} from 'rxjs/operators';
import { BehaviorSubject, Subscription } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service';
import { SelectableListState } from '../../../shared/object-list/selectable-list/selectable-list.reducer';
import {
buildPaginatedList,
PaginatedList,
} from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { PageInfo } from '../../../core/shared/page-info.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-configuration.service';
import { hasValue } from '../../../shared/empty.util';
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 { 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 { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { ThemedSearchComponent } from '../../../shared/search/themed-search.component';
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
@Component({
selector: 'ds-bulk-access-browse',
@@ -22,9 +51,24 @@ import { hasValue } from '../../../shared/empty.util';
providers: [
{
provide: SEARCH_CONFIG_SERVICE,
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 {
@@ -49,7 +93,7 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy {
paginationOptions$: BehaviorSubject<PaginationComponentOptions> = new BehaviorSubject<PaginationComponentOptions>(Object.assign(new PaginationComponentOptions(), {
id: 'bas',
pageSize: 5,
currentPage: 1
currentPage: 1,
}));
/**
@@ -67,20 +111,20 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy {
this.subs.push(
this.selectableListService.getSelectableList(this.listId).pipe(
distinctUntilChanged(),
map((list: SelectableListState) => this.generatePaginatedListBySelectedElements(list))
).subscribe(this.objectsSelected$)
map((list: SelectableListState) => this.generatePaginatedListBySelectedElements(list)),
).subscribe(this.objectsSelected$),
);
}
pageNext() {
this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, {
currentPage: this.paginationOptions$.value.currentPage + 1
currentPage: this.paginationOptions$.value.currentPage + 1,
}));
}
pagePrev() {
this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, {
currentPage: this.paginationOptions$.value.currentPage - 1
currentPage: this.paginationOptions$.value.currentPage - 1,
}));
}
@@ -99,12 +143,12 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy {
elementsPerPage: this.paginationOptions$.value.pageSize,
totalElements: list?.selection.length,
totalPages: this.calculatePageCount(this.paginationOptions$.value.pageSize, list?.selection.length),
currentPage: this.paginationOptions$.value.currentPage
currentPage: this.paginationOptions$.value.currentPage,
});
if (pageInfo.currentPage > pageInfo.totalPages) {
pageInfo.currentPage = pageInfo.totalPages;
this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, {
currentPage: pageInfo.currentPage
currentPage: pageInfo.currentPage,
}));
}
return createSuccessfulRemoteDataObject(buildPaginatedList(pageInfo, list?.selection || []));

View File

@@ -1,18 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs';
import { BulkAccessComponent } from './bulk-access.component';
import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { Process } from '../../process-page/processes/process.model';
import { RouterTestingModule } from '@angular/router/testing';
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 { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { ThemeService } from '../../shared/theme-support/theme.service';
import { BulkAccessComponent } from './bulk-access.component';
import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component';
describe('BulkAccessComponent', () => {
let component: BulkAccessComponent;
@@ -31,35 +36,35 @@ describe('BulkAccessComponent', () => {
'startDate': {
'year': 2026,
'month': 5,
'day': 31
'day': 31,
},
'endDate': null
}
'endDate': null,
},
],
'state': {
'item': {
'toggleStatus': true,
'accessMode': 'replace'
'accessMode': 'replace',
},
'bitstream': {
'toggleStatus': false,
'accessMode': '',
'changesLimit': '',
'selectedBitstreams': []
}
}
'selectedBitstreams': [],
},
},
};
const mockFile = {
'uuids': [
'1234', '5678'
'1234', '5678',
],
'file': { }
'file': { },
};
const mockSettings: any = jasmine.createSpyObj('AccessControlFormContainerComponent', {
getValue: jasmine.createSpy('getValue'),
reset: jasmine.createSpy('reset')
reset: jasmine.createSpy('reset'),
});
const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }];
const selectableListState: SelectableListState = { id: 'test', selection };
@@ -71,16 +76,24 @@ describe('BulkAccessComponent', () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule,
TranslateModule.forRoot()
TranslateModule.forRoot(),
BulkAccessComponent,
],
declarations: [ BulkAccessComponent ],
providers: [
{ provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock },
{ 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();
});

View File

@@ -1,17 +1,34 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import {
Component,
OnInit,
ViewChild,
} from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import {
BehaviorSubject,
Subscription,
} from 'rxjs';
import {
distinctUntilChanged,
map,
} from 'rxjs/operators';
import { BehaviorSubject, Subscription } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component';
import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service';
import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { BulkAccessBrowseComponent } from './browse/bulk-access-browse.component';
import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component';
@Component({
selector: 'ds-bulk-access',
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 {
@@ -37,7 +54,7 @@ export class BulkAccessComponent implements OnInit {
constructor(
private bulkAccessControlService: BulkAccessControlService,
private selectableListService: SelectableListService
private selectableListService: SelectableListService,
) {
}
@@ -45,8 +62,8 @@ export class BulkAccessComponent implements OnInit {
this.subs.push(
this.selectableListService.getSelectableList(this.listId).pipe(
distinctUntilChanged(),
map((list: SelectableListState) => this.generateIdListBySelectedElements(list))
).subscribe(this.objectsSelected$)
map((list: SelectableListState) => this.generateIdListBySelectedElements(list)),
).subscribe(this.objectsSelected$),
);
}
@@ -74,12 +91,12 @@ export class BulkAccessComponent implements OnInit {
const { file } = this.bulkAccessControlService.createPayloadFile({
bitstreamAccess,
itemAccess,
state: settings.state
state: settings.state,
});
this.bulkAccessControlService.executeScript(
this.objectsSelected$.value || [],
file
file,
).subscribe();
}

View File

@@ -1,8 +1,13 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
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 { BulkAccessSettingsComponent } from './bulk-access-settings.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('BulkAccessSettingsComponent', () => {
let component: BulkAccessSettingsComponent;
@@ -15,36 +20,39 @@ describe('BulkAccessSettingsComponent', () => {
'startDate': {
'year': 2026,
'month': 5,
'day': 31
'day': 31,
},
'endDate': null
}
'endDate': null,
},
],
'state': {
'item': {
'toggleStatus': true,
'accessMode': 'replace'
'accessMode': 'replace',
},
'bitstream': {
'toggleStatus': false,
'accessMode': '',
'changesLimit': '',
'selectedBitstreams': []
}
}
'selectedBitstreams': [],
},
},
};
const mockControl: any = jasmine.createSpyObj('AccessControlFormContainerComponent', {
getFormValue: jasmine.createSpy('getFormValue'),
reset: jasmine.createSpy('reset')
reset: jasmine.createSpy('reset'),
});
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NgbAccordionModule, TranslateModule.forRoot()],
declarations: [BulkAccessSettingsComponent],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
imports: [NgbAccordionModule, TranslateModule.forRoot(), BulkAccessSettingsComponent],
schemas: [NO_ERRORS_SCHEMA],
})
.overrideComponent(BulkAccessSettingsComponent, {
remove: { imports: [AccessControlFormContainerComponent] },
})
.compileComponents();
});
beforeEach(() => {

View File

@@ -1,13 +1,25 @@
import { Component, ViewChild } from '@angular/core';
import { NgIf } from '@angular/common';
import {
AccessControlFormContainerComponent
} from '../../../shared/access-control-form-container/access-control-form-container.component';
Component,
ViewChild,
} 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';
@Component({
selector: 'ds-bulk-access-settings',
templateUrl: 'bulk-access-settings.component.html',
styleUrls: ['./bulk-access-settings.component.scss'],
exportAs: 'dsBulkSettings'
exportAs: 'dsBulkSettings',
imports: [
NgbAccordionModule,
TranslateModule,
NgIf,
AccessControlFormContainerComponent,
],
standalone: true,
})
export class BulkAccessSettingsComponent {

View File

@@ -1,5 +1,6 @@
/* eslint-disable max-classes-per-file */
import { Action } from '@ngrx/store';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { type } from '../../shared/ngrx/type';

View File

@@ -43,7 +43,7 @@
<ds-themed-loading *ngIf="searching$ | async"></ds-themed-loading>
<ds-pagination
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (searching$ | async) !== true"
[paginationOptions]="config"
[pageInfoState]="pageInfoState$"
[collectionSize]="(pageInfoState$ | async)?.totalElements"
@@ -87,7 +87,7 @@
</ds-pagination>
<div *ngIf="(pageInfoState$ | async)?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
<div *ngIf="(pageInfoState$ | async)?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert">
{{labelPrefix + 'no-items' | translate}}
</div>
</div>

View File

@@ -1,30 +1,62 @@
import { Router } from '@angular/router';
import { Observable, of as observableOf } from 'rxjs';
import { CommonModule } from '@angular/common';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser';
import { NgbModule, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
DebugElement,
NO_ERRORS_SCHEMA,
} from '@angular/core';
import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
waitForAsync,
} from '@angular/core/testing';
import {
FormsModule,
ReactiveFormsModule,
} from '@angular/forms';
import {
BrowserModule,
By,
} from '@angular/platform-browser';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import {
NgbModal,
NgbModule,
} from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
import {
Observable,
of as observableOf,
} from 'rxjs';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FindListOptions } from '../../core/data/find-list-options.model';
import {
buildPaginatedList,
PaginatedList,
} from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data';
import { RequestService } from '../../core/data/request.service';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { PaginationService } from '../../core/pagination/pagination.service';
import { PageInfo } from '../../core/shared/page-info.model';
import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { EPeopleRegistryComponent } from './epeople-registry.component';
import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock';
import { RouterMock } from '../../shared/mocks/router.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import {
EPersonMock,
EPersonMock2,
} from '../../shared/testing/eperson.mock';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { RouterStub } from '../../shared/testing/router.stub';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { RequestService } from '../../core/data/request.service';
import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { EPeopleRegistryComponent } from './epeople-registry.component';
import { EPersonFormComponent } from './eperson-form/eperson-form.component';
describe('EPeopleRegistryComponent', () => {
let component: EPeopleRegistryComponent;
@@ -48,7 +80,7 @@ describe('EPeopleRegistryComponent', () => {
elementsPerPage: this.allEpeople.length,
totalElements: this.allEpeople.length,
totalPages: 1,
currentPage: 1
currentPage: 1,
}), this.allEpeople));
},
getActiveEPerson(): Observable<EPerson> {
@@ -63,7 +95,7 @@ describe('EPeopleRegistryComponent', () => {
elementsPerPage: [result].length,
totalElements: [result].length,
totalPages: 1,
currentPage: 1
currentPage: 1,
}), [result]));
}
if (scope === 'metadata') {
@@ -72,7 +104,7 @@ describe('EPeopleRegistryComponent', () => {
elementsPerPage: this.allEpeople.length,
totalElements: this.allEpeople.length,
totalPages: 1,
currentPage: 1
currentPage: 1,
}), this.allEpeople));
}
const result = this.allEpeople.find((ePerson: EPerson) => {
@@ -82,14 +114,14 @@ describe('EPeopleRegistryComponent', () => {
elementsPerPage: [result].length,
totalElements: [result].length,
totalPages: 1,
currentPage: 1
currentPage: 1,
}), [result]));
}
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
elementsPerPage: this.allEpeople.length,
totalElements: this.allEpeople.length,
totalPages: 1,
currentPage: 1
currentPage: 1,
}), this.allEpeople));
},
deleteEPerson(ePerson: EPerson): Observable<boolean> {
@@ -109,30 +141,38 @@ describe('EPeopleRegistryComponent', () => {
},
getEPeoplePageRouterLink(): string {
return '/access-control/epeople';
}
},
};
authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
isAuthorized: observableOf(true),
});
builderService = getMockFormBuilderService();
paginationService = new PaginationServiceStub();
await TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot(),
],
declarations: [EPeopleRegistryComponent],
TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule.withRoutes([]),
TranslateModule.forRoot(), EPeopleRegistryComponent],
providers: [
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: FormBuilderService, useValue: builderService },
{ provide: Router, useValue: new RouterStub() },
{ provide: Router, useValue: new RouterMock() },
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) },
{ provide: PaginationService, useValue: paginationService }
{ provide: PaginationService, useValue: paginationService },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
schemas: [NO_ERRORS_SCHEMA],
})
.overrideComponent(EPeopleRegistryComponent, {
remove: {
imports: [
EPersonFormComponent,
ThemedLoadingComponent,
PaginationComponent,
],
},
})
.compileComponents();
}));
beforeEach(() => {
@@ -202,7 +242,7 @@ describe('EPeopleRegistryComponent', () => {
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
deleteButtons[0].triggerEventHandler('click', {
preventDefault: () => {/**/
}
},
});
tick();
fixture.detectChanges();

View File

@@ -1,32 +1,86 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { UntypedFormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
import { map, switchMap, take } from 'rxjs/operators';
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
import {
AsyncPipe,
NgClass,
NgForOf,
NgIf,
} from '@angular/common';
import {
Component,
OnDestroy,
OnInit,
} from '@angular/core';
import {
ReactiveFormsModule,
UntypedFormBuilder,
} from '@angular/forms';
import {
Router,
RouterModule,
} from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {
BehaviorSubject,
combineLatest,
Observable,
Subscription,
} from 'rxjs';
import {
map,
switchMap,
take,
} from 'rxjs/operators';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import {
buildPaginatedList,
PaginatedList,
} from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data';
import { RequestService } from '../../core/data/request.service';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { hasValue } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { EpersonDtoModel } from '../../core/eperson/models/eperson-dto.model';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { getAllSucceededRemoteData, getFirstCompletedRemoteData } from '../../core/shared/operators';
import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { RequestService } from '../../core/data/request.service';
import { PageInfo } from '../../core/shared/page-info.model';
import { NoContent } from '../../core/shared/NoContent.model';
import { PaginationService } from '../../core/pagination/pagination.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { getEPersonEditRoute, getEPersonsRoute } from '../access-control-routing-paths';
import { NoContent } from '../../core/shared/NoContent.model';
import {
getAllSucceededRemoteData,
getFirstCompletedRemoteData,
} from '../../core/shared/operators';
import { PageInfo } from '../../core/shared/page-info.model';
import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component';
import { hasValue } from '../../shared/empty.util';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import {
getEPersonEditRoute,
getEPersonsRoute,
} from '../access-control-routing-paths';
import { EPersonFormComponent } from './eperson-form/eperson-form.component';
@Component({
selector: 'ds-epeople-registry',
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.
@@ -62,7 +116,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'elp',
pageSize: 5,
currentPage: 1
currentPage: 1,
});
// The search form
@@ -110,7 +164,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
*/
initialisePage() {
this.searching$.next(true);
this.search({scope: this.currentSearchScope, query: this.currentSearchQuery});
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
this.subs.push(this.ePeople$.pipe(
switchMap((epeople: PaginatedList<EPerson>) => {
if (epeople.pageInfo.totalElements > 0) {
@@ -121,7 +175,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
epersonDtoModel.ableToDelete = authorized;
epersonDtoModel.eperson = eperson;
return epersonDtoModel;
})
}),
);
})).pipe(map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epeople.pageInfo, dtos);
@@ -147,34 +201,34 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
}
this.findListOptionsSub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
switchMap((findListOptions) => {
const query: string = data.query;
const scope: string = data.scope;
if (query != null && this.currentSearchQuery !== query) {
void this.router.navigate([getEPersonsRoute()], {
queryParamsHandling: 'merge'
});
this.currentSearchQuery = query;
this.paginationService.resetPage(this.config.id);
}
if (scope != null && this.currentSearchScope !== scope) {
void this.router.navigate([getEPersonsRoute()], {
queryParamsHandling: 'merge'
});
this.currentSearchScope = scope;
this.paginationService.resetPage(this.config.id);
}
return this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
currentPage: findListOptions.currentPage,
elementsPerPage: findListOptions.pageSize
const query: string = data.query;
const scope: string = data.scope;
if (query != null && this.currentSearchQuery !== query) {
void this.router.navigate([getEPersonsRoute()], {
queryParamsHandling: 'merge',
});
this.currentSearchQuery = query;
this.paginationService.resetPage(this.config.id);
}
if (scope != null && this.currentSearchScope !== scope) {
void this.router.navigate([getEPersonsRoute()], {
queryParamsHandling: 'merge',
});
this.currentSearchScope = scope;
this.paginationService.resetPage(this.config.id);
}
return this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
currentPage: findListOptions.currentPage,
elementsPerPage: findListOptions.pageSize,
});
},
),
getAllSucceededRemoteData(),
).subscribe((peopleRD) => {
this.ePeople$.next(peopleRD.payload);
this.pageInfoState$.next(peopleRD.payload.pageInfo);
}
this.ePeople$.next(peopleRD.payload);
this.pageInfoState$.next(peopleRD.payload.pageInfo);
},
);
}
@@ -184,7 +238,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
*/
isActive(eperson: EPerson): Observable<boolean> {
return this.getActiveEPerson().pipe(
map((activeEPerson) => eperson === activeEPerson)
map((activeEPerson) => eperson === activeEPerson),
);
}
@@ -213,7 +267,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
if (hasValue(ePerson.id)) {
this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
if (restResponse.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: this.dsoNameService.getName(ePerson)}));
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(ePerson) }));
} else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { id: ePerson.id, statusCode: restResponse.statusCode, errorMessage: restResponse.errorMessage }));
}
@@ -244,7 +298,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
this.searchForm.patchValue({
query: '',
});
this.search({query: ''});
this.search({ query: '' });
}
getEditEPeoplePage(id: string): string {

View File

@@ -1,6 +1,12 @@
import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from './epeople-registry.actions';
import { ePeopleRegistryReducer, EPeopleRegistryState } from './epeople-registry.reducers';
import { EPersonMock } from '../../shared/testing/eperson.mock';
import {
EPeopleRegistryCancelEPersonAction,
EPeopleRegistryEditEPersonAction,
} from './epeople-registry.actions';
import {
ePeopleRegistryReducer,
EPeopleRegistryState,
} from './epeople-registry.reducers';
const initialState: EPeopleRegistryState = {
editEPerson: null,

View File

@@ -2,7 +2,7 @@ import { EPerson } from '../../core/eperson/models/eperson.model';
import {
EPeopleRegistryAction,
EPeopleRegistryActionTypes,
EPeopleRegistryEditEPersonAction
EPeopleRegistryEditEPersonAction,
} from './epeople-registry.actions';
/**
@@ -30,13 +30,13 @@ export function ePeopleRegistryReducer(state = initialState, action: EPeopleRegi
case EPeopleRegistryActionTypes.EDIT_EPERSON: {
return Object.assign({}, state, {
editEPerson: (action as EPeopleRegistryEditEPersonAction).eperson
editEPerson: (action as EPeopleRegistryEditEPersonAction).eperson,
});
}
case EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON: {
return Object.assign({}, state, {
editEPerson: null
editEPerson: null,
});
}

View File

@@ -25,7 +25,7 @@
</button>
</div>
<div *ngIf="displayResetPassword" between class="btn-group">
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" type="button" (click)="resetPassword()">
<button class="btn btn-primary" [disabled]="(canReset$ | async) !== true" type="button" (click)="resetPassword()">
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
</button>
</div>
@@ -47,13 +47,13 @@
<div *ngIf="epersonService.getActiveEPerson() | async">
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
<ds-themed-loading [showMessage]="false" *ngIf="groups$ | async | dsHasNoValue"></ds-themed-loading>
<ds-pagination
*ngIf="(groups | async)?.payload?.totalElements > 0"
*ngIf="(groups$ | async)?.payload?.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(groups | async)?.payload"
[collectionSize]="(groups | async)?.payload?.totalElements"
[pageInfoState]="groupsPageInfoState$"
[collectionSize]="(groups$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)">
@@ -68,7 +68,7 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let group of (groups | async)?.payload?.page">
<tr *ngFor="let group of (groups$ | async)?.payload?.page">
<td class="align-middle">{{group.id}}</td>
<td class="align-middle">
<a (click)="groupsDataService.startEditingNewGroup(group)"
@@ -84,7 +84,7 @@
</ds-pagination>
<div *ngIf="(groups | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
<div *ngIf="(groups$ | async)?.payload?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert">
<div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div>
<div>
<button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]"

View File

@@ -1,40 +1,73 @@
import { Observable, of as observableOf } from 'rxjs';
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser';
import {
ComponentFixture,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import {
FormsModule,
ReactiveFormsModule,
UntypedFormControl,
UntypedFormGroup,
Validators,
} from '@angular/forms';
import {
BrowserModule,
By,
} from '@angular/platform-browser';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
import {
TranslateLoader,
TranslateModule,
} from '@ngx-translate/core';
import {
Observable,
of as observableOf,
} from 'rxjs';
import { AuthService } from '../../../core/auth/auth.service';
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FindListOptions } from '../../../core/data/find-list-options.model';
import {
buildPaginatedList,
PaginatedList,
} from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { RequestService } from '../../../core/data/request.service';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { PageInfo } from '../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { FormComponent } from '../../../shared/form/form.component';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
import { AuthServiceStub } from '../../../shared/testing/auth-service.stub';
import {
EPersonMock,
EPersonMock2,
} from '../../../shared/testing/eperson.mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { RouterStub } from '../../../shared/testing/router.stub';
import { createPaginatedList } from '../../../shared/testing/utils.test';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { HasNoValuePipe } from '../../../shared/utils/has-no-value.pipe';
import { EPeopleRegistryComponent } from '../epeople-registry.component';
import { EPersonFormComponent } from './eperson-form.component';
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
import { AuthService } from '../../../core/auth/auth.service';
import { AuthServiceStub } from '../../../shared/testing/auth-service.stub';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service';
import { createPaginatedList } from '../../../shared/testing/utils.test';
import { RequestService } from '../../../core/data/request.service';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../../core/data/find-list-options.model';
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterStub } from '../../../shared/testing/router.stub';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
describe('EPersonFormComponent', () => {
let component: EPersonFormComponent;
@@ -115,70 +148,70 @@ describe('EPersonFormComponent', () => {
},
findById(_id: string, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<EPerson>[]): Observable<RemoteData<EPerson>> {
return createSuccessfulRemoteDataObject$(null);
}
},
};
builderService = Object.assign(getMockFormBuilderService(),{
createFormGroup(formModel, options = null) {
const controls = {};
formModel.forEach( model => {
model.parent = parent;
const controlModel = model;
const controlState = { value: controlModel.value, disabled: controlModel.disabled };
const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn);
controls[model.id] = new UntypedFormControl(controlState, controlOptions);
model.parent = parent;
const controlModel = model;
const controlState = { value: controlModel.value, disabled: controlModel.disabled };
const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn);
controls[model.id] = new UntypedFormControl(controlState, controlOptions);
});
return new UntypedFormGroup(controls, options);
},
createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) {
return {
validators: validatorsConfig !== null ? this.getValidators(validatorsConfig) : null,
validators: validatorsConfig !== null ? this.getValidators(validatorsConfig) : null,
};
},
getValidators(validatorsConfig) {
return this.getValidatorFns(validatorsConfig);
return this.getValidatorFns(validatorsConfig);
},
getValidatorFns(validatorsConfig, validatorsToken = this._NG_VALIDATORS) {
let validatorFns = [];
if (this.isObject(validatorsConfig)) {
validatorFns = Object.keys(validatorsConfig).map(validatorConfigKey => {
const validatorConfigValue = validatorsConfig[validatorConfigKey];
if (this.isValidatorDescriptor(validatorConfigValue)) {
const descriptor = validatorConfigValue;
return this.getValidatorFn(descriptor.name, descriptor.args, validatorsToken);
}
return this.getValidatorFn(validatorConfigKey, validatorConfigValue, validatorsToken);
});
validatorFns = Object.keys(validatorsConfig).map(validatorConfigKey => {
const validatorConfigValue = validatorsConfig[validatorConfigKey];
if (this.isValidatorDescriptor(validatorConfigValue)) {
const descriptor = validatorConfigValue;
return this.getValidatorFn(descriptor.name, descriptor.args, validatorsToken);
}
return this.getValidatorFn(validatorConfigKey, validatorConfigValue, validatorsToken);
});
}
return validatorFns;
},
getValidatorFn(validatorName, validatorArgs = null, validatorsToken = this._NG_VALIDATORS) {
let validatorFn;
if (Validators.hasOwnProperty(validatorName)) { // Built-in Angular Validators
validatorFn = Validators[validatorName];
validatorFn = Validators[validatorName];
} else { // Custom Validators
if (this._DYNAMIC_VALIDATORS && this._DYNAMIC_VALIDATORS.has(validatorName)) {
validatorFn = this._DYNAMIC_VALIDATORS.get(validatorName);
} else if (validatorsToken) {
validatorFn = validatorsToken.find(validator => validator.name === validatorName);
}
if (this._DYNAMIC_VALIDATORS && this._DYNAMIC_VALIDATORS.has(validatorName)) {
validatorFn = this._DYNAMIC_VALIDATORS.get(validatorName);
} else if (validatorsToken) {
validatorFn = validatorsToken.find(validator => validator.name === validatorName);
}
}
if (validatorFn === undefined) { // throw when no validator could be resolved
throw new Error(`validator '${validatorName}' is not provided via NG_VALIDATORS, NG_ASYNC_VALIDATORS or DYNAMIC_FORM_VALIDATORS`);
throw new Error(`validator '${validatorName}' is not provided via NG_VALIDATORS, NG_ASYNC_VALIDATORS or DYNAMIC_FORM_VALIDATORS`);
}
if (validatorArgs !== null) {
return validatorFn(validatorArgs);
return validatorFn(validatorArgs);
}
return validatorFn;
},
},
isValidatorDescriptor(value) {
if (this.isObject(value)) {
return value.hasOwnProperty('name') && value.hasOwnProperty('args');
}
return false;
if (this.isObject(value)) {
return value.hasOwnProperty('name') && value.hasOwnProperty('args');
}
return false;
},
isObject(value) {
return typeof value === 'object' && value !== null;
}
},
});
authService = new AuthServiceStub();
authorizationService = jasmine.createSpyObj('authorizationService', {
@@ -187,7 +220,7 @@ describe('EPersonFormComponent', () => {
});
groupsDataService = jasmine.createSpyObj('groupsDataService', {
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
getGroupRegistryRouterLink: ''
getGroupRegistryRouterLink: '',
});
paginationService = new PaginationServiceStub();
@@ -198,11 +231,12 @@ describe('EPersonFormComponent', () => {
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
useClass: TranslateLoaderMock,
},
}),
EPersonFormComponent,
HasNoValuePipe,
],
declarations: [EPersonFormComponent],
providers: [
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: GroupDataService, useValue: groupsDataService },
@@ -211,18 +245,22 @@ describe('EPersonFormComponent', () => {
{ provide: AuthService, useValue: authService },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: PaginationService, useValue: paginationService },
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])},
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) },
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService },
{ provide: ActivatedRoute, useValue: route },
{ provide: Router, useValue: router },
EPeopleRegistryComponent
EPeopleRegistryComponent,
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
schemas: [NO_ERRORS_SCHEMA],
})
.overrideComponent(EPersonFormComponent, {
remove: { imports: [ ThemedLoadingComponent, PaginationComponent,FormComponent] },
})
.compileComponents();
}));
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
registerEmail: createSuccessfulRemoteDataObject$(null)
registerEmail: createSuccessfulRemoteDataObject$(null),
});
beforeEach(() => {
@@ -254,12 +292,12 @@ describe('EPersonFormComponent', () => {
metadata: {
'eperson.firstname': [
{
value: firstName
}
value: firstName,
},
],
'eperson.lastname': [
{
value: lastName
value: lastName,
},
],
},
@@ -298,16 +336,16 @@ describe('EPersonFormComponent', () => {
fixture.detectChanges();
});
it('firstName should be valid because the firstName is set', () => {
expect(component.formGroup.controls.firstName.valid).toBeTrue();
expect(component.formGroup.controls.firstName.errors).toBeNull();
expect(component.formGroup.controls.firstName.valid).toBeTrue();
expect(component.formGroup.controls.firstName.errors).toBeNull();
});
it('lastName should be valid because the lastName is set', () => {
expect(component.formGroup.controls.lastName.valid).toBeTrue();
expect(component.formGroup.controls.lastName.errors).toBeNull();
expect(component.formGroup.controls.lastName.valid).toBeTrue();
expect(component.formGroup.controls.lastName.errors).toBeNull();
});
it('email should be valid because the email is set', () => {
expect(component.formGroup.controls.email.valid).toBeTrue();
expect(component.formGroup.controls.email.errors).toBeNull();
expect(component.formGroup.controls.email.valid).toBeTrue();
expect(component.formGroup.controls.email.errors).toBeNull();
});
});
@@ -318,8 +356,8 @@ describe('EPersonFormComponent', () => {
fixture.detectChanges();
});
it('email should not be valid because the email pattern', () => {
expect(component.formGroup.controls.email.valid).toBeFalse();
expect(component.formGroup.controls.email.errors.pattern).toBeTruthy();
expect(component.formGroup.controls.email.valid).toBeFalse();
expect(component.formGroup.controls.email.errors.pattern).toBeTruthy();
});
});
@@ -328,7 +366,7 @@ describe('EPersonFormComponent', () => {
const ePersonServiceWithEperson = Object.assign(ePersonDataServiceStub,{
getEPersonByEmail(): Observable<RemoteData<EPerson>> {
return createSuccessfulRemoteDataObject$(EPersonMock);
}
},
});
component.formGroup.controls.email.setValue('test@test.com');
component.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(ePersonServiceWithEperson));
@@ -336,8 +374,8 @@ describe('EPersonFormComponent', () => {
});
it('email should not be valid because email is already taken', () => {
expect(component.formGroup.controls.email.valid).toBeFalse();
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
expect(component.formGroup.controls.email.valid).toBeFalse();
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
});
});
@@ -363,12 +401,12 @@ describe('EPersonFormComponent', () => {
metadata: {
'eperson.firstname': [
{
value: firstName
}
value: firstName,
},
],
'eperson.lastname': [
{
value: lastName
value: lastName,
},
],
},
@@ -404,19 +442,19 @@ describe('EPersonFormComponent', () => {
metadata: {
'eperson.firstname': [
{
value: firstName
}
value: firstName,
},
],
'eperson.lastname': [
{
value: lastName
value: lastName,
},
],
},
email: email,
canLogIn: canLogIn,
requireCertificate: requireCertificate,
_links: undefined
_links: undefined,
});
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
component.onSubmit();
@@ -436,7 +474,7 @@ describe('EPersonFormComponent', () => {
spyOn(authService, 'impersonate').and.callThrough();
ePersonId = 'testEPersonId';
component.epersonInitial = Object.assign(new EPerson(), {
id: ePersonId
id: ePersonId,
});
component.impersonate();
});
@@ -524,7 +562,7 @@ describe('EPersonFormComponent', () => {
ePersonEmail = 'person.email@4science.it';
component.epersonInitial = Object.assign(new EPerson(), {
id: ePersonId,
email: ePersonEmail
email: ePersonEmail,
});
component.resetPassword();
});

View File

@@ -1,49 +1,99 @@
import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
import {
AsyncPipe,
NgClass,
NgFor,
NgIf,
} from '@angular/common';
import {
ChangeDetectorRef,
Component,
EventEmitter,
OnDestroy,
OnInit,
Output,
} from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import {
ActivatedRoute,
Router,
RouterLink,
} from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
DynamicCheckboxModel,
DynamicFormControlModel,
DynamicFormLayout,
DynamicInputModel
DynamicInputModel,
} from '@ng-dynamic-forms/core';
import { TranslateService } from '@ngx-translate/core';
import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
import { debounceTime, finalize, map, switchMap, take } from 'rxjs/operators';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {
combineLatest as observableCombineLatest,
Observable,
of as observableOf,
Subscription,
} from 'rxjs';
import {
debounceTime,
finalize,
map,
switchMap,
take,
} from 'rxjs/operators';
import { AuthService } from '../../../core/auth/auth.service';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { RequestService } from '../../../core/data/request.service';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model';
import { Group } from '../../../core/eperson/models/group.model';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { NoContent } from '../../../core/shared/NoContent.model';
import {
getFirstCompletedRemoteData,
getFirstSucceededRemoteData,
getRemoteDataPayload
getRemoteDataPayload,
} from '../../../core/shared/operators';
import { PageInfo } from '../../../core/shared/page-info.model';
import { Registration } from '../../../core/shared/registration.model';
import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component';
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
import { hasValue } from '../../../shared/empty.util';
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 { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { AuthService } from '../../../core/auth/auth.service';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { RequestService } from '../../../core/data/request.service';
import { NoContent } from '../../../core/shared/NoContent.model';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { followLink } from '../../../shared/utils/follow-link-config.model';
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
import { Registration } from '../../../core/shared/registration.model';
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { ActivatedRoute, Router } from '@angular/router';
import { HasNoValuePipe } from '../../../shared/utils/has-no-value.pipe';
import { getEPersonsRoute } from '../../access-control-routing-paths';
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
@Component({
selector: 'ds-eperson-form',
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
@@ -83,28 +133,28 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
formLayout: DynamicFormLayout = {
firstName: {
grid: {
host: 'row'
}
host: 'row',
},
},
lastName: {
grid: {
host: 'row'
}
host: 'row',
},
},
email: {
grid: {
host: 'row'
}
host: 'row',
},
},
canLogIn: {
grid: {
host: 'col col-sm-6 d-inline-block'
}
host: 'col col-sm-6 d-inline-block',
},
},
requireCertificate: {
grid: {
host: 'col col-sm-6 d-inline-block'
}
host: 'col col-sm-6 d-inline-block',
},
},
};
@@ -147,7 +197,12 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
/**
* A list of all the groups this EPerson is a member of
*/
groups: Observable<RemoteData<PaginatedList<Group>>>;
groups$: Observable<RemoteData<PaginatedList<Group>>>;
/**
* The pagination of the {@link groups$} list.
*/
groupsPageInfoState$: Observable<PageInfo>;
/**
* Pagination config used to display the list of groups
@@ -155,7 +210,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'gem',
pageSize: 5,
currentPage: 1
currentPage: 1,
});
/**
@@ -257,23 +312,23 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
required: true,
errorMessages: {
emailTaken: 'error.validation.emailTaken',
pattern: 'error.validation.NotValidEmail'
pattern: 'error.validation.NotValidEmail',
},
hint: emailHint
hint: emailHint,
});
this.canLogIn = new DynamicCheckboxModel(
{
id: 'canLogIn',
label: canLogIn,
name: 'canLogIn',
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true)
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true),
});
this.requireCertificate = new DynamicCheckboxModel(
{
id: 'requireCertificate',
label: requireCertificate,
name: 'requireCertificate',
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false)
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false),
});
this.formModel = [
this.firstName,
@@ -285,9 +340,9 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
if (eperson != null) {
this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, {
this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, {
currentPage: 1,
elementsPerPage: this.config.pageSize
elementsPerPage: this.config.pageSize,
});
}
this.formGroup.patchValue({
@@ -295,7 +350,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '',
email: eperson != null ? eperson.email : '',
canLogIn: eperson != null ? eperson.canLogIn : true,
requireCertificate: eperson != null ? eperson.requireCertificate : false
requireCertificate: eperson != null ? eperson.requireCertificate : false,
});
if (eperson === null && !!this.formGroup.controls.email) {
@@ -308,11 +363,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
const activeEPerson$ = this.epersonService.getActiveEPerson();
this.groups = activeEPerson$.pipe(
this.groups$ = activeEPerson$.pipe(
switchMap((eperson) => {
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
currentPage: 1,
elementsPerPage: this.config.pageSize
elementsPerPage: this.config.pageSize,
})]);
}),
switchMap(([eperson, findListOptions]) => {
@@ -320,7 +375,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
}
return observableOf(undefined);
})
}),
);
this.groupsPageInfoState$ = this.groups$.pipe(
map(groupsRD => groupsRD.payload.pageInfo),
);
this.canImpersonate$ = activeEPerson$.pipe(
@@ -330,10 +389,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
} else {
return observableOf(false);
}
})
}),
);
this.canDelete$ = activeEPerson$.pipe(
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)),
);
this.canReset$ = observableOf(true);
});
@@ -361,12 +420,12 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
metadata: {
'eperson.firstname': [
{
value: this.firstName.value
}
value: this.firstName.value,
},
],
'eperson.lastname': [
{
value: this.lastName.value
value: this.lastName.value,
},
],
},
@@ -379,7 +438,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
} else {
this.editEPerson(ePerson, values);
}
}
},
);
}
@@ -392,7 +451,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
const response = this.epersonService.create(ePersonToCreate);
response.pipe(
getFirstCompletedRemoteData()
getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData<EPerson>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: this.dsoNameService.getName(ePersonToCreate) }));
@@ -418,12 +477,12 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
metadata: {
'eperson.firstname': [
{
value: (this.firstName.value ? this.firstName.value : ePerson.firstMetadataValue('eperson.firstname'))
}
value: (this.firstName.value ? this.firstName.value : ePerson.firstMetadataValue('eperson.firstname')),
},
],
'eperson.lastname': [
{
value: (this.lastName.value ? this.lastName.value : ePerson.firstMetadataValue('eperson.lastname'))
value: (this.lastName.value ? this.lastName.value : ePerson.firstMetadataValue('eperson.lastname')),
},
],
},
@@ -457,7 +516,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
onPageChange(event) {
this.updateGroups({
currentPage: event,
elementsPerPage: this.config.pageSize
elementsPerPage: this.config.pageSize,
});
}
@@ -493,15 +552,15 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
this.canDelete$ = observableOf(false);
return this.epersonService.deleteEPerson(eperson).pipe(
getFirstCompletedRemoteData(),
map((restResponse: RemoteData<NoContent>) => ({ restResponse, eperson }))
map((restResponse: RemoteData<NoContent>) => ({ restResponse, eperson })),
);
} else {
return observableOf(null);
}
}),
finalize(() => this.canDelete$ = observableOf(true))
finalize(() => this.canDelete$ = observableOf(true)),
);
})
}),
).subscribe(({ restResponse, eperson }: { restResponse: RemoteData<NoContent> | null, eperson: EPerson }) => {
if (restResponse?.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) }));
@@ -529,14 +588,14 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
if (hasValue(this.epersonInitial.email)) {
this.epersonRegistrationService.registerEmail(this.epersonInitial.email, null, TYPE_REQUEST_FORGOT).pipe(getFirstCompletedRemoteData())
.subscribe((response: RemoteData<Registration>) => {
if (response.hasSucceeded) {
this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'),
this.translateService.get('forgot-email.form.success.content', {email: this.epersonInitial.email}));
} else {
this.notificationsService.error(this.translateService.get('forgot-email.form.error.head'),
this.translateService.get('forgot-email.form.error.content', {email: this.epersonInitial.email}));
}
if (response.hasSucceeded) {
this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'),
this.translateService.get('forgot-email.form.success.content', { email: this.epersonInitial.email }));
} else {
this.notificationsService.error(this.translateService.get('forgot-email.form.error.head'),
this.translateService.get('forgot-email.form.error.content', { email: this.epersonInitial.email }));
}
},
);
}
}
@@ -562,13 +621,13 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
// Relevant message for email in use
this.subs.push(this.epersonService.searchByScope('email', ePerson.email, {
currentPage: 1,
elementsPerPage: 0
elementsPerPage: 0,
}).pipe(getFirstSucceededRemoteData(), getRemoteDataPayload())
.subscribe((list: PaginatedList<EPerson>) => {
if (list.totalElements > 0) {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.' + notificationSection + '.failure.emailInUse', {
name: this.dsoNameService.getName(ePerson),
email: ePerson.email
email: ePerson.email,
}));
}
}));
@@ -579,7 +638,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
*/
private updateGroups(options) {
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, options);
this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, options);
}));
}
}

View File

@@ -1,9 +1,12 @@
import { AbstractControl, ValidationErrors } from '@angular/forms';
import {
AbstractControl,
ValidationErrors,
} from '@angular/forms';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { getFirstSucceededRemoteData, } from '../../../../core/shared/operators';
import { getFirstSucceededRemoteData } from '../../../../core/shared/operators';
export class ValidateEmailNotTaken {
@@ -17,8 +20,8 @@ export class ValidateEmailNotTaken {
.pipe(
getFirstSucceededRemoteData(),
map(res => {
return !!res.payload ? { emailTaken: true } : null;
})
return res.payload ? { emailTaken: true } : null;
}),
);
};
}

View File

@@ -1,13 +1,20 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { RemoteData } from '../../core/data/remote-data';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { ResolvedAction } from '../../core/resolving/resolver.actions';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import {
ActivatedRouteSnapshot,
RouterStateSnapshot,
} from '@angular/router';
import { Store } from '@ngrx/store';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { Observable } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { ResolvedAction } from '../../core/resolving/resolver.actions';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import {
followLink,
FollowLinkConfig,
} from '../../shared/utils/follow-link-config.model';
export const EPERSON_EDIT_FOLLOW_LINKS: FollowLinkConfig<EPerson>[] = [
followLink('groups'),
@@ -19,7 +26,7 @@ export const EPERSON_EDIT_FOLLOW_LINKS: FollowLinkConfig<EPerson>[] = [
@Injectable({
providedIn: 'root',
})
export class EPersonResolver implements Resolve<RemoteData<EPerson>> {
export class EPersonResolver {
constructor(
protected ePersonService: EPersonDataService,

View File

@@ -25,7 +25,7 @@
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
<ds-alert *ngIf="!(canEdit$ | async) && (groupDataService.getActiveGroup() | async)" [type]="AlertTypeEnum.Warning"
<ds-alert *ngIf="(canEdit$ | async) !== true && (groupDataService.getActiveGroup() | async)" [type]="AlertTypeEnum.Warning"
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName((getLinkedDSO(groupBeingEdited) | async)?.payload), comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })">
</ds-alert>
@@ -47,10 +47,10 @@
</ds-form>
<div class="mb-5">
<ds-members-list *ngIf="groupBeingEdited != null"
<ds-members-list *ngIf="groupBeingEdited !== undefined"
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
</div>
<ds-subgroups-list *ngIf="groupBeingEdited != null"
<ds-subgroups-list *ngIf="groupBeingEdited !== undefined"
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>

View File

@@ -1,43 +1,79 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import {
ComponentFixture,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import {
FormsModule,
ReactiveFormsModule,
UntypedFormControl,
UntypedFormGroup,
Validators,
} from '@angular/forms';
import {
BrowserModule,
By,
} from '@angular/platform-browser';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { Store } from '@ngrx/store';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf } from 'rxjs';
import {
TranslateLoader,
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { Operation } from 'fast-json-patch';
import {
Observable,
of as observableOf,
} from 'rxjs';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { DSOChangeAnalyzer } from '../../../core/data/dso-change-analyzer.service';
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
import {
buildPaginatedList,
PaginatedList,
} from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service';
import { Group } from '../../../core/eperson/models/group.model';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
import { NoContent } from '../../../core/shared/NoContent.model';
import { PageInfo } from '../../../core/shared/page-info.model';
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 { NotificationsService } from '../../../shared/notifications/notifications.service';
import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock';
import { GroupFormComponent } from './group-form.component';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock';
import { RouterMock } from '../../../shared/mocks/router.mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { Operation } from 'fast-json-patch';
import { ValidateGroupExists } from './validators/group-exists.validator';
import { NoContent } from '../../../core/shared/NoContent.model';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { FormComponent } from '../../../shared/form/form.component';
import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
import { RouterMock } from '../../../shared/mocks/router.mock';
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import {
GroupMock,
GroupMock2,
} from '../../../shared/testing/group-mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock';
import { GroupFormComponent } from './group-form.component';
import { MembersListComponent } from './members-list/members-list.component';
import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component';
import { ValidateGroupExists } from './validators/group-exists.validator';
describe('GroupFormComponent', () => {
let component: GroupFormComponent;
@@ -65,8 +101,8 @@ describe('GroupFormComponent', () => {
metadata: {
'dc.description': [
{
value: groupDescription
}
value: groupDescription,
},
],
},
});
@@ -105,7 +141,7 @@ describe('GroupFormComponent', () => {
create(group: Group): Observable<RemoteData<Group>> {
this.allGroups = [...this.allGroups, group];
this.createdGroup = Object.assign({}, group, {
_links: { self: { href: 'group-selflink' } }
_links: { self: { href: 'group-selflink' } },
});
return createSuccessfulRemoteDataObject$(this.createdGroup);
},
@@ -114,78 +150,78 @@ describe('GroupFormComponent', () => {
},
getGroupEditPageRouterLinkWithID(id: string) {
return `group-edit-page-for-${id}`;
}
},
};
authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
isAuthorized: observableOf(true),
});
dsoDataServiceStub = {
findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
return null;
}
},
};
builderService = Object.assign(getMockFormBuilderService(),{
createFormGroup(formModel, options = null) {
const controls = {};
formModel.forEach( model => {
model.parent = parent;
const controlModel = model;
const controlState = { value: controlModel.value, disabled: controlModel.disabled };
const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn);
controls[model.id] = new UntypedFormControl(controlState, controlOptions);
model.parent = parent;
const controlModel = model;
const controlState = { value: controlModel.value, disabled: controlModel.disabled };
const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn);
controls[model.id] = new UntypedFormControl(controlState, controlOptions);
});
return new UntypedFormGroup(controls, options);
},
createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) {
return {
validators: validatorsConfig !== null ? this.getValidators(validatorsConfig) : null,
validators: validatorsConfig !== null ? this.getValidators(validatorsConfig) : null,
};
},
getValidators(validatorsConfig) {
return this.getValidatorFns(validatorsConfig);
return this.getValidatorFns(validatorsConfig);
},
getValidatorFns(validatorsConfig, validatorsToken = this._NG_VALIDATORS) {
let validatorFns = [];
if (this.isObject(validatorsConfig)) {
validatorFns = Object.keys(validatorsConfig).map(validatorConfigKey => {
const validatorConfigValue = validatorsConfig[validatorConfigKey];
if (this.isValidatorDescriptor(validatorConfigValue)) {
const descriptor = validatorConfigValue;
return this.getValidatorFn(descriptor.name, descriptor.args, validatorsToken);
}
return this.getValidatorFn(validatorConfigKey, validatorConfigValue, validatorsToken);
});
validatorFns = Object.keys(validatorsConfig).map(validatorConfigKey => {
const validatorConfigValue = validatorsConfig[validatorConfigKey];
if (this.isValidatorDescriptor(validatorConfigValue)) {
const descriptor = validatorConfigValue;
return this.getValidatorFn(descriptor.name, descriptor.args, validatorsToken);
}
return this.getValidatorFn(validatorConfigKey, validatorConfigValue, validatorsToken);
});
}
return validatorFns;
},
getValidatorFn(validatorName, validatorArgs = null, validatorsToken = this._NG_VALIDATORS) {
let validatorFn;
if (Validators.hasOwnProperty(validatorName)) { // Built-in Angular Validators
validatorFn = Validators[validatorName];
validatorFn = Validators[validatorName];
} else { // Custom Validators
if (this._DYNAMIC_VALIDATORS && this._DYNAMIC_VALIDATORS.has(validatorName)) {
validatorFn = this._DYNAMIC_VALIDATORS.get(validatorName);
} else if (validatorsToken) {
validatorFn = validatorsToken.find(validator => validator.name === validatorName);
}
if (this._DYNAMIC_VALIDATORS && this._DYNAMIC_VALIDATORS.has(validatorName)) {
validatorFn = this._DYNAMIC_VALIDATORS.get(validatorName);
} else if (validatorsToken) {
validatorFn = validatorsToken.find(validator => validator.name === validatorName);
}
}
if (validatorFn === undefined) { // throw when no validator could be resolved
throw new Error(`validator '${validatorName}' is not provided via NG_VALIDATORS, NG_ASYNC_VALIDATORS or DYNAMIC_FORM_VALIDATORS`);
throw new Error(`validator '${validatorName}' is not provided via NG_VALIDATORS, NG_ASYNC_VALIDATORS or DYNAMIC_FORM_VALIDATORS`);
}
if (validatorArgs !== null) {
return validatorFn(validatorArgs);
return validatorFn(validatorArgs);
}
return validatorFn;
},
},
isValidatorDescriptor(value) {
if (this.isObject(value)) {
return value.hasOwnProperty('name') && value.hasOwnProperty('args');
}
return false;
if (this.isObject(value)) {
return value.hasOwnProperty('name') && value.hasOwnProperty('args');
}
return false;
},
isObject(value) {
return typeof value === 'object' && value !== null;
}
},
});
translateService = getMockTranslateService();
router = new RouterMock();
@@ -195,11 +231,9 @@ describe('GroupFormComponent', () => {
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
}),
],
declarations: [GroupFormComponent],
useClass: TranslateLoaderMock,
},
}), GroupFormComponent],
providers: [
{ provide: DSONameService, useValue: new DSONameServiceMock() },
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
@@ -211,18 +245,29 @@ describe('GroupFormComponent', () => {
{ provide: HttpClient, useValue: {} },
{ provide: ObjectCacheService, useValue: {} },
{ provide: UUIDService, useValue: {} },
{ provide: XSRFService, useValue: {} },
{ provide: Store, useValue: {} },
{ provide: RemoteDataBuildService, useValue: {} },
{ provide: HALEndpointService, useValue: {} },
{
provide: ActivatedRoute,
useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) }
useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) },
},
{ provide: Router, useValue: router },
{ provide: AuthorizationDataService, useValue: authorizationService },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
schemas: [NO_ERRORS_SCHEMA],
})
.overrideComponent(GroupFormComponent, {
remove: { imports: [
FormComponent,
AlertComponent,
ContextHelpDirective,
MembersListComponent,
SubgroupsListComponent,
] },
})
.compileComponents();
}));
beforeEach(() => {
@@ -257,8 +302,8 @@ describe('GroupFormComponent', () => {
metadata: {
'dc.description': [
{
value: groupDescription
}
value: groupDescription,
},
],
},
});
@@ -273,11 +318,11 @@ describe('GroupFormComponent', () => {
const operations = [{
op: 'add',
path: '/metadata/dc.description',
value: 'testDescription'
value: 'testDescription',
}, {
op: 'replace',
path: '/name',
value: 'newGroupName'
value: 'newGroupName',
}];
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
});
@@ -289,7 +334,7 @@ describe('GroupFormComponent', () => {
const operations = [{
op: 'add',
path: '/metadata/dc.description',
value: 'testDescription'
value: 'testDescription',
}];
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
});
@@ -301,7 +346,7 @@ describe('GroupFormComponent', () => {
const operations = [{
op: 'replace',
path: '/name',
value: 'newGroupName'
value: 'newGroupName',
}];
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
});
@@ -338,8 +383,8 @@ describe('GroupFormComponent', () => {
metadata: {
'dc.description': [
{
value: groupDescription
}
value: groupDescription,
},
],
},
});
@@ -376,7 +421,7 @@ describe('GroupFormComponent', () => {
const groupsDataServiceStubWithGroup = Object.assign(groupsDataServiceStub,{
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [expected]));
}
},
});
component.formGroup.controls.groupName.setValue('testName');
component.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(groupsDataServiceStubWithGroup));
@@ -400,7 +445,7 @@ describe('GroupFormComponent', () => {
component.canEdit$ = observableOf(true);
component.groupBeingEdited = {
permanent: false
permanent: false,
} as Group;
fixture.detectChanges();

View File

@@ -1,23 +1,52 @@
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core';
import {
AsyncPipe,
NgIf,
} from '@angular/common';
import {
ChangeDetectorRef,
Component,
EventEmitter,
HostListener,
OnDestroy,
OnInit,
Output,
} from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
DynamicFormControlModel,
DynamicFormLayout,
DynamicInputModel,
DynamicTextAreaModel
DynamicTextAreaModel,
} 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 {
combineLatest as observableCombineLatest,
Observable,
of as observableOf,
Subscription,
} from 'rxjs';
import { catchError, map, switchMap, take, filter, debounceTime } from 'rxjs/operators';
import {
catchError,
debounceTime,
filter,
map,
switchMap,
take,
} from 'rxjs/operators';
import { environment } from '../../../../environments/environment';
import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths';
import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
@@ -30,28 +59,48 @@ import { Group } from '../../../core/eperson/models/group.model';
import { Collection } from '../../../core/shared/collection.model';
import { Community } from '../../../core/shared/community.model';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { NoContent } from '../../../core/shared/NoContent.model';
import {
getRemoteDataPayload,
getFirstSucceededRemoteData,
getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload
getFirstSucceededRemoteData,
getFirstSucceededRemoteDataPayload,
getRemoteDataPayload,
} from '../../../core/shared/operators';
import { AlertComponent } from '../../../shared/alert/alert.component';
import { AlertType } from '../../../shared/alert/alert-type';
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
import { hasValue, isNotEmpty, hasValueOperator } from '../../../shared/empty.util';
import { ContextHelpDirective } from '../../../shared/context-help.directive';
import {
hasValue,
hasValueOperator,
isNotEmpty,
} from '../../../shared/empty.util';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { FormComponent } from '../../../shared/form/form.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { followLink } from '../../../shared/utils/follow-link-config.model';
import { NoContent } from '../../../core/shared/NoContent.model';
import { Operation } from 'fast-json-patch';
import {
getGroupEditRoute,
getGroupsRoute,
} 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 { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { environment } from '../../../../environments/environment';
import { getGroupEditRoute, getGroupsRoute } from '../../access-control-routing-paths';
@Component({
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
@@ -83,13 +132,13 @@ export class GroupFormComponent implements OnInit, OnDestroy {
formLayout: DynamicFormLayout = {
groupName: {
grid: {
host: 'row'
}
host: 'row',
},
},
groupDescription: {
grid: {
host: 'row'
}
host: 'row',
},
},
};
@@ -176,7 +225,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
observableCombineLatest([
this.translateService.get(`${this.messagePrefix}.groupName`),
this.translateService.get(`${this.messagePrefix}.groupCommunity`),
this.translateService.get(`${this.messagePrefix}.groupDescription`)
this.translateService.get(`${this.messagePrefix}.groupDescription`),
]).subscribe(([groupName, groupCommunity, groupDescription]) => {
this.groupName = new DynamicInputModel({
id: 'groupName',
@@ -207,7 +256,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
];
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
if (!!this.formGroup.controls.groupName) {
if (this.formGroup.controls.groupName) {
this.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService));
this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => {
this.changeDetectorRef.detectChanges();
@@ -219,7 +268,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
this.groupDataService.getActiveGroup(),
this.canEdit$,
this.groupDataService.getActiveGroup()
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload())))
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload()))),
]).subscribe(([activeGroup, canEdit, linkedObject]) => {
if (activeGroup != null) {
@@ -254,7 +303,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
}
}, 200);
}
})
}),
);
});
}
@@ -282,9 +331,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
metadata: {
'dc.description': [
{
value: this.groupDescription.value
}
]
value: this.groupDescription.value,
},
],
},
};
if (group === null) {
@@ -292,7 +341,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
} else {
this.editGroup(group);
}
}
},
);
}
@@ -303,7 +352,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
createNewGroup(values) {
const groupToCreate = Object.assign(new Group(), values);
this.groupDataService.create(groupToCreate).pipe(
getFirstCompletedRemoteData()
getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData<Group>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.created.success', { name: groupToCreate.name }));
@@ -332,7 +381,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
// Relevant message for group name in use
this.subs.push(this.groupDataService.searchGroups(group.name, {
currentPage: 1,
elementsPerPage: 0
elementsPerPage: 0,
}).pipe(getFirstSucceededRemoteData(), getRemoteDataPayload())
.subscribe((list: PaginatedList<Group>) => {
if (list.totalElements > 0) {
@@ -354,7 +403,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
operations = [...operations, {
op: 'add',
path: '/metadata/dc.description',
value: this.groupDescription.value
value: this.groupDescription.value,
}];
}
@@ -362,12 +411,12 @@ export class GroupFormComponent implements OnInit, OnDestroy {
operations = [...operations, {
op: 'replace',
path: '/name',
value: this.groupName.value
value: this.groupName.value,
}];
}
this.groupDataService.patch(group, operations).pipe(
getFirstCompletedRemoteData()
getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData<Group>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.edited.success', { name: this.dsoNameService.getName(rd.payload) }));
@@ -506,7 +555,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
return getCollectionEditRolesRoute(rd.payload.id);
}
}
})
}),
);
}
}

View File

@@ -49,7 +49,7 @@
</ds-pagination>
<div *ngIf="(ePeopleMembersOfGroup | async) == undefined || (ePeopleMembersOfGroup | async)?.totalElements == 0" class="alert alert-info w-100 mb-2"
<div *ngIf="(ePeopleMembersOfGroup | async) === undefined || (ePeopleMembersOfGroup | async)?.totalElements === 0" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-members-yet' | translate}}
</div>
@@ -130,7 +130,7 @@
</ds-pagination>
<div *ngIf="(ePeopleSearch | async)?.totalElements == 0 && searchDone"
<div *ngIf="(ePeopleSearch | async)?.totalElements === 0 && searchDone"
class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-items' | translate}}

View File

@@ -1,35 +1,74 @@
import { CommonModule } from '@angular/common';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import {
DebugElement,
NO_ERRORS_SCHEMA,
} from '@angular/core';
import {
ComponentFixture,
fakeAsync,
flush,
inject,
TestBed,
tick,
waitForAsync,
} from '@angular/core/testing';
import {
FormsModule,
ReactiveFormsModule,
} from '@angular/forms';
import {
BrowserModule,
By,
} from '@angular/platform-browser';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf } from 'rxjs';
import {
TranslateLoader,
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {
Observable,
of as observableOf,
} from 'rxjs';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { RestResponse } from '../../../../core/cache/response.models';
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
import {
buildPaginatedList,
PaginatedList,
} from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { EPerson } from '../../../../core/eperson/models/eperson.model';
import { Group } from '../../../../core/eperson/models/group.model';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { GroupMock } from '../../../../shared/testing/group-mock';
import { MembersListComponent } from './members-list.component';
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
import { RouterMock } from '../../../../shared/mocks/router.mock';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
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 { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock';
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
import { RouterMock } from '../../../../shared/mocks/router.mock';
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub';
import {
EPersonMock,
EPersonMock2,
} from '../../../../shared/testing/eperson.mock';
import { GroupMock } from '../../../../shared/testing/group-mock';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
import { MembersListComponent } from './members-list.component';
// todo: optimize imports
describe('MembersListComponent', () => {
let component: MembersListComponent;
@@ -109,7 +148,7 @@ describe('MembersListComponent', () => {
// Add eperson to list of non-members
this.epersonNonMembers = [...this.epersonNonMembers, epersonToDelete];
return observableOf(new RestResponse(true, 200, 'Success'));
}
},
};
builderService = getMockFormBuilderService();
translateService = getMockTranslateService();
@@ -120,11 +159,9 @@ describe('MembersListComponent', () => {
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
}),
],
declarations: [MembersListComponent],
useClass: TranslateLoaderMock,
},
}), MembersListComponent],
providers: [MembersListComponent,
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: GroupDataService, useValue: groupsDataServiceStub },
@@ -133,9 +170,16 @@ describe('MembersListComponent', () => {
{ provide: Router, useValue: new RouterMock() },
{ provide: PaginationService, useValue: paginationService },
{ provide: DSONameService, useValue: new DSONameServiceMock() },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
schemas: [NO_ERRORS_SCHEMA],
})
.overrideComponent(MembersListComponent, {
remove: {
imports: [PaginationComponent, ContextHelpDirective],
},
})
.compileComponents();
}));
beforeEach(() => {

View File

@@ -1,30 +1,59 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { UntypedFormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import {
AsyncPipe,
NgClass,
NgForOf,
NgIf,
} from '@angular/common';
import {
Component,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import {
ReactiveFormsModule,
UntypedFormBuilder,
} from '@angular/forms';
import {
Router,
RouterLink,
} from '@angular/router';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {
BehaviorSubject,
Observable,
Subscription,
BehaviorSubject
} from 'rxjs';
import { map, switchMap, take } from 'rxjs/operators';
import {
map,
switchMap,
take,
} from 'rxjs/operators';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { PaginatedList } from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { EPerson } from '../../../../core/eperson/models/eperson.model';
import { Group } from '../../../../core/eperson/models/group.model';
import {
getFirstCompletedRemoteData,
getAllCompletedRemoteData,
getRemoteDataPayload
} from '../../../../core/shared/operators';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import {
getAllCompletedRemoteData,
getFirstCompletedRemoteData,
getRemoteDataPayload,
} from '../../../../core/shared/operators';
import { ContextHelpDirective } from '../../../../shared/context-help.directive';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { getEPersonEditRoute } from '../../../access-control-routing-paths';
// todo: optimize imports
/**
* Keys to keep track of specific subscriptions
*/
@@ -65,7 +94,19 @@ export interface EPersonListActionConfig {
@Component({
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
@@ -73,21 +114,21 @@ export interface EPersonListActionConfig {
export class MembersListComponent implements OnInit, OnDestroy {
@Input()
messagePrefix: string;
messagePrefix: string;
@Input()
actionConfig: EPersonListActionConfig = {
add: {
css: 'btn-outline-primary',
disabled: false,
icon: 'fas fa-plus fa-fw',
},
remove: {
css: 'btn-outline-danger',
disabled: false,
icon: 'fas fa-trash-alt fa-fw'
},
};
actionConfig: EPersonListActionConfig = {
add: {
css: 'btn-outline-primary',
disabled: false,
icon: 'fas fa-plus fa-fw',
},
remove: {
css: 'btn-outline-danger',
disabled: false,
icon: 'fas fa-trash-alt fa-fw',
},
};
/**
* EPeople being displayed in search result, initially all members, after search result of search
@@ -104,7 +145,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'sml',
pageSize: 5,
currentPage: 1
currentPage: 1,
});
/**
* Pagination config used to display the list of EPerson Membes of active group being edited
@@ -112,7 +153,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'ml',
pageSize: 5,
currentPage: 1
currentPage: 1,
});
/**
@@ -155,7 +196,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
if (activeGroup != null) {
this.groupBeingEdited = activeGroup;
this.retrieveMembers(this.config.currentPage);
this.search({query: ''});
this.search({ query: '' });
}
}));
}
@@ -172,9 +213,9 @@ export class MembersListComponent implements OnInit, OnDestroy {
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
switchMap((currentPagination) => {
return this.ePersonDataService.findListByHref(this.groupBeingEdited._links.epersons.href, {
currentPage: currentPagination.currentPage,
elementsPerPage: currentPagination.pageSize
}
currentPage: currentPagination.currentPage,
elementsPerPage: currentPagination.pageSize,
},
);
}),
getAllCompletedRemoteData(),
@@ -217,7 +258,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
// Reload search results (if there is an active query).
// This will potentially add this deleted subgroup into the list of search results.
if (this.currentSearchQuery != null) {
this.search({query: this.currentSearchQuery});
this.search({ query: this.currentSearchQuery });
}
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
@@ -237,7 +278,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
// Reload search results (if there is an active query).
// This will potentially add this deleted subgroup into the list of search results.
if (this.currentSearchQuery != null) {
this.search({query: this.currentSearchQuery});
this.search({ query: this.currentSearchQuery });
}
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
@@ -263,7 +304,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
return this.ePersonDataService.searchNonMembers(this.currentSearchQuery, this.groupBeingEdited.id, {
currentPage: paginationOptions.currentPage,
elementsPerPage: paginationOptions.pageSize
elementsPerPage: paginationOptions.pageSize,
}, false, true);
}),
getAllCompletedRemoteData(),

View File

@@ -45,7 +45,7 @@
</div>
</ds-pagination>
<div *ngIf="(subGroups$ | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2"
<div *ngIf="(subGroups$ | async)?.payload?.totalElements === 0" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-subgroups-yet' | translate}}
</div>
@@ -124,7 +124,7 @@
</div>
</ds-pagination>
<div *ngIf="(searchResults$ | async)?.payload?.totalElements == 0 && searchDone" class="alert alert-info w-100 mb-2"
<div *ngIf="(searchResults$ | async)?.payload?.totalElements === 0 && searchDone" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-items' | translate}}
</div>

View File

@@ -1,35 +1,69 @@
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import {
DebugElement,
NO_ERRORS_SCHEMA,
} from '@angular/core';
import {
ComponentFixture,
fakeAsync,
flush,
inject,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import {
FormsModule,
ReactiveFormsModule,
} from '@angular/forms';
import {
BrowserModule,
By,
} from '@angular/platform-browser';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf } from 'rxjs';
import {
TranslateLoader,
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {
Observable,
of as observableOf,
} from 'rxjs';
import { EPersonMock2 } from 'src/app/shared/testing/eperson.mock';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { RestResponse } from '../../../../core/cache/response.models';
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
import {
buildPaginatedList,
PaginatedList,
} from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { Group } from '../../../../core/eperson/models/group.model';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock';
import { SubgroupsListComponent } from './subgroups-list.component';
import {
createSuccessfulRemoteDataObject$
} from '../../../../shared/remote-data.utils';
import { RouterMock } from '../../../../shared/mocks/router.mock';
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
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 { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock';
import { EPersonMock2 } from 'src/app/shared/testing/eperson.mock';
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
import { RouterMock } from '../../../../shared/mocks/router.mock';
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub';
import {
GroupMock,
GroupMock2,
} from '../../../../shared/testing/group-mock';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
import { SubgroupsListComponent } from './subgroups-list.component';
describe('SubgroupsListComponent', () => {
let component: SubgroupsListComponent;
@@ -51,12 +85,12 @@ describe('SubgroupsListComponent', () => {
selfRegistered: false,
permanent: false,
_links: {
self: {
href: 'https://rest.api/server/api/eperson/groups/activegroupid',
},
subgroups: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/subgroups' },
object: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/object' },
epersons: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/epersons' }
self: {
href: 'https://rest.api/server/api/eperson/groups/activegroupid',
},
subgroups: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/subgroups' },
object: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/object' },
epersons: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/epersons' },
},
_name: 'activegroupname',
id: 'activegroupid',
@@ -91,7 +125,9 @@ describe('SubgroupsListComponent', () => {
if (query === '') {
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupNonMembers));
}
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
return createSuccessfulRemoteDataObject$(
buildPaginatedList(new PageInfo(), []),
);
},
addSubGroupToGroup(parentGroup, subgroupToAdd: Group): Observable<RestResponse> {
// Add group to list of subgroups
@@ -120,33 +156,49 @@ describe('SubgroupsListComponent', () => {
// Add group to list of non-members
this.groupNonMembers = [...this.groupNonMembers, subgroupToDelete];
return observableOf(new RestResponse(true, 200, 'Success'));
}
},
};
routerStub = new RouterMock();
builderService = getMockFormBuilderService();
translateService = getMockTranslateService();
paginationService = new PaginationServiceStub();
return TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
imports: [
CommonModule,
NgbModule,
FormsModule,
ReactiveFormsModule,
BrowserModule,
// ContextHelpDirective,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
useClass: TranslateLoaderMock,
},
}),
SubgroupsListComponent,
],
declarations: [SubgroupsListComponent],
providers: [SubgroupsListComponent,
providers: [
SubgroupsListComponent,
{ provide: DSONameService, useValue: new DSONameServiceMock() },
{ provide: GroupDataService, useValue: groupsDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{
provide: NotificationsService,
useValue: new NotificationsServiceStub(),
},
{ provide: FormBuilderService, useValue: builderService },
{ provide: Router, useValue: routerStub },
{ provide: PaginationService, useValue: paginationService },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
schemas: [NO_ERRORS_SCHEMA],
})
.overrideComponent(SubgroupsListComponent, {
remove: {
imports: [ContextHelpDirective, PaginationComponent],
},
})
.compileComponents();
}));
beforeEach(() => {

View File

@@ -1,23 +1,54 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { UntypedFormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { map, switchMap, take } from 'rxjs/operators';
import {
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import {
Component,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import {
ReactiveFormsModule,
UntypedFormBuilder,
} from '@angular/forms';
import {
Router,
RouterLink,
} from '@angular/router';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {
BehaviorSubject,
Observable,
Subscription,
} from 'rxjs';
import {
map,
switchMap,
take,
} from 'rxjs/operators';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { PaginatedList } from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { Group } from '../../../../core/eperson/models/group.model';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { NoContent } from '../../../../core/shared/NoContent.model';
import {
getAllCompletedRemoteData,
getFirstCompletedRemoteData
getFirstCompletedRemoteData,
} from '../../../../core/shared/operators';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { ContextHelpDirective } from '../../../../shared/context-help.directive';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { NoContent } from '../../../../core/shared/NoContent.model';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { followLink } from '../../../../shared/utils/follow-link-config.model';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
/**
* Keys to keep track of specific subscriptions
@@ -30,7 +61,18 @@ enum SubKey {
@Component({
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
@@ -38,7 +80,7 @@ enum SubKey {
export class SubgroupsListComponent implements OnInit, OnDestroy {
@Input()
messagePrefix: string;
messagePrefix: string;
/**
* Result of search groups, initially all groups
@@ -49,6 +91,8 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
*/
subGroups$: BehaviorSubject<RemoteData<PaginatedList<Group>>> = new BehaviorSubject(undefined);
subGroupsPageInfoState$: Observable<PageInfo>;
/**
* Map of active subscriptions
*/
@@ -60,7 +104,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'ssgl',
pageSize: 5,
currentPage: 1
currentPage: 1,
});
/**
* Pagination config used to display the list of subgroups of currently active group being edited
@@ -68,7 +112,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'sgl',
pageSize: 5,
currentPage: 1
currentPage: 1,
});
// The search form
@@ -102,9 +146,12 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
if (activeGroup != null) {
this.groupBeingEdited = activeGroup;
this.retrieveSubGroups();
this.search({query: ''});
this.search({ query: '' });
}
}));
this.subGroupsPageInfoState$ = this.subGroups$.pipe(
map(subGroupsRD => subGroupsRD?.payload?.pageInfo),
);
}
/**
@@ -119,13 +166,13 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
SubKey.Members,
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
switchMap((config) => this.groupDataService.findListByHref(this.groupBeingEdited._links.subgroups.href, {
currentPage: config.currentPage,
elementsPerPage: config.pageSize
},
true,
true,
followLink('object')
))
currentPage: config.currentPage,
elementsPerPage: config.pageSize,
},
true,
true,
followLink('object'),
)),
).subscribe((rd: RemoteData<PaginatedList<Group>>) => {
this.subGroups$.next(rd);
}));
@@ -143,7 +190,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
// Reload search results (if there is an active query).
// This will potentially add this deleted subgroup into the list of search results.
if (this.currentSearchQuery != null) {
this.search({query: this.currentSearchQuery});
this.search({ query: this.currentSearchQuery });
}
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
@@ -164,7 +211,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
// Reload search results (if there is an active query).
// This will potentially remove this added subgroup from search results.
if (this.currentSearchQuery != null) {
this.search({query: this.currentSearchQuery});
this.search({ query: this.currentSearchQuery });
}
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.subgroupToAddIsActiveGroup'));
@@ -194,7 +241,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
return this.groupDataService.searchNonMemberGroups(this.currentSearchQuery, this.groupBeingEdited.id, {
currentPage: paginationOptions.currentPage,
elementsPerPage: paginationOptions.pageSize
elementsPerPage: paginationOptions.pageSize,
}, false, true, followLink('object'));
}),
getAllCompletedRemoteData(),

View File

@@ -1,10 +1,13 @@
import { AbstractControl, ValidationErrors } from '@angular/forms';
import {
AbstractControl,
ValidationErrors,
} from '@angular/forms';
import { Observable } from 'rxjs';
import { map} from 'rxjs/operators';
import { map } from 'rxjs/operators';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators';
import { Group } from '../../../../core/eperson/models/group.model';
import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators';
export class ValidateGroupExists {
@@ -16,9 +19,9 @@ export class ValidateGroupExists {
static createValidator(groupDataService: GroupDataService) {
return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
return groupDataService.searchGroups(control.value, {
currentPage: 1,
elementsPerPage: 100
})
currentPage: 1,
elementsPerPage: 100,
})
.pipe(
getFirstSucceededRemoteListPayload(),
map( (groups: Group[]) => {

View File

@@ -1,10 +1,14 @@
import { GroupPageGuard } from './group-page.guard';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { ActivatedRouteSnapshot, Router } from '@angular/router';
import {
ActivatedRouteSnapshot,
Router,
} from '@angular/router';
import { of as observableOf } from 'rxjs';
import { AuthService } from '../../core/auth/auth.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { GroupPageGuard } from './group-page.guard';
describe('GroupPageGuard', () => {
const groupsEndpointUrl = 'https://test.org/api/eperson/groups';
@@ -13,7 +17,7 @@ describe('GroupPageGuard', () => {
const routeSnapshotWithGroupId = {
params: {
groupId: groupUuid,
}
},
} as unknown as ActivatedRouteSnapshot;
let guard: GroupPageGuard;
@@ -50,10 +54,10 @@ describe('GroupPageGuard', () => {
it('should return true', (done) => {
guard.canActivate(
routeSnapshotWithGroupId, { url: 'current-url'} as any
routeSnapshotWithGroupId, { url: 'current-url' } as any,
).subscribe((result) => {
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
FeatureID.CanManageGroup, groupEndpointUrl, undefined
FeatureID.CanManageGroup, groupEndpointUrl, undefined,
);
expect(result).toBeTrue();
done();
@@ -68,10 +72,10 @@ describe('GroupPageGuard', () => {
it('should not return true', (done) => {
guard.canActivate(
routeSnapshotWithGroupId, { url: 'current-url'} as any
routeSnapshotWithGroupId, { url: 'current-url' } as any,
).subscribe((result) => {
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
FeatureID.CanManageGroup, groupEndpointUrl, undefined
FeatureID.CanManageGroup, groupEndpointUrl, undefined,
);
expect(result).not.toBeTrue();
done();

View File

@@ -1,15 +1,23 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { Observable, of as observableOf } from 'rxjs';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { AuthService } from '../../core/auth/auth.service';
import { SomeFeatureAuthorizationGuard } from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot,
} from '@angular/router';
import {
Observable,
of as observableOf,
} from 'rxjs';
import { map } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { SomeFeatureAuthorizationGuard } from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class GroupPageGuard extends SomeFeatureAuthorizationGuard {
@@ -28,7 +36,7 @@ export class GroupPageGuard extends SomeFeatureAuthorizationGuard {
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
return this.halEndpointService.getEndpoint(this.groupsEndpoint).pipe(
map(groupsUrl => `${groupsUrl}/${route?.params?.groupId}`)
map(groupsUrl => `${groupsUrl}/${route?.params?.groupId}`),
);
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable max-classes-per-file */
import { Action } from '@ngrx/store';
import { Group } from '../../core/eperson/models/group.model';
import { type } from '../../shared/ngrx/type';

View File

@@ -1,6 +1,12 @@
import { GroupMock } from '../../shared/testing/group-mock';
import { GroupRegistryCancelGroupAction, GroupRegistryEditGroupAction } from './group-registry.actions';
import { groupRegistryReducer, GroupRegistryState } from './group-registry.reducers';
import {
GroupRegistryCancelGroupAction,
GroupRegistryEditGroupAction,
} from './group-registry.actions';
import {
groupRegistryReducer,
GroupRegistryState,
} from './group-registry.reducers';
const initialState: GroupRegistryState = {
editGroup: null,

View File

@@ -1,5 +1,9 @@
import { Group } from '../../core/eperson/models/group.model';
import { GroupRegistryAction, GroupRegistryActionTypes, GroupRegistryEditGroupAction } from './group-registry.actions';
import {
GroupRegistryAction,
GroupRegistryActionTypes,
GroupRegistryEditGroupAction,
} from './group-registry.actions';
/**
* The metadata registry state.
@@ -27,13 +31,13 @@ export function groupRegistryReducer(state = initialState, action: GroupRegistry
case GroupRegistryActionTypes.EDIT_GROUP: {
return Object.assign({}, state, {
editGroup: (action as GroupRegistryEditGroupAction).group
editGroup: (action as GroupRegistryEditGroupAction).group,
});
}
case GroupRegistryActionTypes.CANCEL_EDIT_GROUP: {
return Object.assign({}, state, {
editGroup: null
editGroup: null,
});
}

View File

@@ -35,7 +35,7 @@
<ds-themed-loading *ngIf="loading$ | async"></ds-themed-loading>
<ds-pagination
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(loading$ | async)"
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (loading$ | async) !== true"
[paginationOptions]="config"
[pageInfoState]="pageInfoState$"
[collectionSize]="(pageInfoState$ | async)?.totalElements"
@@ -91,7 +91,7 @@
</div>
</ds-pagination>
<div *ngIf="(pageInfoState$ | async)?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
<div *ngIf="(pageInfoState$ | async)?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert">
{{messagePrefix + 'no-items' | translate}}
</div>

View File

@@ -1,39 +1,79 @@
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import {
ComponentFixture,
fakeAsync,
inject,
TestBed,
tick,
waitForAsync,
} from '@angular/core/testing';
import {
FormsModule,
ReactiveFormsModule,
} from '@angular/forms';
import {
BrowserModule,
By,
} from '@angular/platform-browser';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Observable, of as observableOf } from 'rxjs';
import { provideMockStore } from '@ngrx/store/testing';
import {
TranslateLoader,
TranslateModule,
} from '@ngx-translate/core';
import {
Observable,
of as observableOf,
of,
} from 'rxjs';
import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface';
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 { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import {
buildPaginatedList,
PaginatedList,
} from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data';
import { RequestService } from '../../core/data/request.service';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../core/eperson/group-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { Group } from '../../core/eperson/models/group.model';
import { PaginationService } from '../../core/pagination/pagination.service';
import { RouteService } from '../../core/services/route.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { PageInfo } from '../../core/shared/page-info.model';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { GroupMock, GroupMock2 } from '../../shared/testing/group-mock';
import { GroupsRegistryComponent } from './groups-registry.component';
import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { TranslateLoaderMock } from '../../shared/testing/translate-loader.mock';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { routeServiceStub } from '../../shared/testing/route-service.stub';
import { RouterMock } from '../../shared/mocks/router.mock';
import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { NoContent } from '../../core/shared/NoContent.model';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock, UNDEFINED_NAME } from '../../shared/mocks/dso-name.service.mock';
import { PageInfo } from '../../core/shared/page-info.model';
import {
DSONameServiceMock,
UNDEFINED_NAME,
} from '../../shared/mocks/dso-name.service.mock';
import { RouterMock } from '../../shared/mocks/router.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import {
EPersonMock,
EPersonMock2,
} from '../../shared/testing/eperson.mock';
import {
GroupMock,
GroupMock2,
} from '../../shared/testing/group-mock';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { routeServiceStub } from '../../shared/testing/route-service.stub';
import { TranslateLoaderMock } from '../../shared/testing/translate-loader.mock';
import { GroupsRegistryComponent } from './groups-registry.component';
describe('GroupsRegistryComponent', () => {
let component: GroupsRegistryComponent;
@@ -42,6 +82,7 @@ describe('GroupsRegistryComponent', () => {
let groupsDataServiceStub: any;
let dsoDataServiceStub: any;
let authorizationService: AuthorizationDataService;
let configurationDataService: jasmine.SpyObj<ConfigurationDataService>;
let mockGroups;
let mockEPeople;
@@ -78,24 +119,24 @@ describe('GroupsRegistryComponent', () => {
elementsPerPage: 1,
totalElements: 0,
totalPages: 0,
currentPage: 1
currentPage: 1,
}), []));
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons':
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
elementsPerPage: 1,
totalElements: 1,
totalPages: 1,
currentPage: 1
currentPage: 1,
}), [EPersonMock]));
default:
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
elementsPerPage: 1,
totalElements: 0,
totalPages: 0,
currentPage: 1
currentPage: 1,
}), []));
}
}
},
};
groupsDataServiceStub = {
allGroups: mockGroups,
@@ -106,21 +147,21 @@ describe('GroupsRegistryComponent', () => {
elementsPerPage: 1,
totalElements: 0,
totalPages: 0,
currentPage: 1
currentPage: 1,
}), []));
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/groups':
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
elementsPerPage: 1,
totalElements: 1,
totalPages: 1,
currentPage: 1
currentPage: 1,
}), [GroupMock2]));
default:
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
elementsPerPage: 1,
totalElements: 0,
totalPages: 0,
currentPage: 1
currentPage: 1,
}), []));
}
},
@@ -136,7 +177,7 @@ describe('GroupsRegistryComponent', () => {
elementsPerPage: this.allGroups.length,
totalElements: this.allGroups.length,
totalPages: 1,
currentPage: 1
currentPage: 1,
}), this.allGroups));
}
const result = this.allGroups.find((group: Group) => {
@@ -146,7 +187,7 @@ describe('GroupsRegistryComponent', () => {
elementsPerPage: [result].length,
totalElements: [result].length,
totalPages: 1,
currentPage: 1
currentPage: 1,
}), [result]));
},
delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
@@ -156,9 +197,13 @@ describe('GroupsRegistryComponent', () => {
dsoDataServiceStub = {
findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
return createSuccessfulRemoteDataObject$(undefined);
}
},
};
configurationDataService = jasmine.createSpyObj('ConfigurationDataService', {
findByPropertyName: of({ payload: { value: 'test' } }),
});
authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
setIsAuthorized(true, true);
paginationService = new PaginationServiceStub();
@@ -167,24 +212,26 @@ describe('GroupsRegistryComponent', () => {
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
}),
],
declarations: [GroupsRegistryComponent],
useClass: TranslateLoaderMock,
},
}), GroupsRegistryComponent],
providers: [GroupsRegistryComponent,
{ provide: DSONameService, useValue: new DSONameServiceMock() },
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: GroupDataService, useValue: groupsDataServiceStub },
{ provide: DSpaceObjectDataService, useValue: dsoDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: ConfigurationDataService, useValue: configurationDataService },
{ provide: RouteService, useValue: routeServiceStub },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
{ provide: Router, useValue: new RouterMock() },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ 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();
}));
@@ -237,16 +284,16 @@ describe('GroupsRegistryComponent', () => {
it('should not check the canManageGroup permissions', () => {
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
FeatureID.CanManageGroup, mockGroups[0].self
FeatureID.CanManageGroup, mockGroups[0].self,
);
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
FeatureID.CanManageGroup, mockGroups[0].self, undefined // treated differently
FeatureID.CanManageGroup, mockGroups[0].self, undefined, // treated differently
);
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
FeatureID.CanManageGroup, mockGroups[1].self
FeatureID.CanManageGroup, mockGroups[1].self,
);
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
FeatureID.CanManageGroup, mockGroups[1].self, undefined // treated differently
FeatureID.CanManageGroup, mockGroups[1].self, undefined, // treated differently
);
});
});

View File

@@ -1,47 +1,94 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { UntypedFormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import {
AsyncPipe,
NgForOf,
NgIf,
NgSwitch,
NgSwitchCase,
} from '@angular/common';
import {
Component,
OnDestroy,
OnInit,
} from '@angular/core';
import {
ReactiveFormsModule,
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 {
BehaviorSubject,
combineLatest as observableCombineLatest,
EMPTY,
Observable,
of as observableOf,
Subscription
Subscription,
} from 'rxjs';
import { catchError, defaultIfEmpty, map, switchMap, tap } from 'rxjs/operators';
import {
catchError,
defaultIfEmpty,
map,
switchMap,
tap,
} from 'rxjs/operators';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
import {
buildPaginatedList,
PaginatedList,
} from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data';
import { RequestService } from '../../core/data/request.service';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../core/eperson/group-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { GroupDtoModel } from '../../core/eperson/models/group-dto.model';
import { Group } from '../../core/eperson/models/group.model';
import { GroupDtoModel } from '../../core/eperson/models/group-dto.model';
import { PaginationService } from '../../core/pagination/pagination.service';
import { RouteService } from '../../core/services/route.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { NoContent } from '../../core/shared/NoContent.model';
import {
getAllSucceededRemoteData,
getFirstCompletedRemoteData,
getFirstSucceededRemoteData,
getRemoteDataPayload
getRemoteDataPayload,
} from '../../core/shared/operators';
import { PageInfo } from '../../core/shared/page-info.model';
import { hasValue } from '../../shared/empty.util';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { NoContent } from '../../core/shared/NoContent.model';
import { PaginationService } from '../../core/pagination/pagination.service';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
@Component({
selector: 'ds-groups-registry',
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.
@@ -57,7 +104,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'gl',
pageSize: 5,
currentPage: 1
currentPage: 1,
});
/**
@@ -133,7 +180,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
const query: string = data.query;
if (query != null && this.currentSearchQuery !== query) {
this.currentSearchQuery = query;
this.paginationService.updateRouteWithUrl(this.config.id, [], {page: 1});
this.paginationService.updateRouteWithUrl(this.config.id, [], { page: 1 });
}
return this.groupService.searchGroups(this.currentSearchQuery.trim(), {
currentPage: paginationOptions.currentPage,
@@ -155,19 +202,19 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
this.canManageGroup$(isSiteAdmin, group),
this.hasLinkedDSO(group),
this.getSubgroups(group),
this.getMembers(group)
this.getMembers(group),
]).pipe(
map(([canDelete, canManageGroup, hasLinkedDSO, subgroups, members]:
[boolean, boolean, boolean, RemoteData<PaginatedList<Group>>, RemoteData<PaginatedList<EPerson>>]) => {
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
groupDtoModel.ableToDelete = canDelete && !hasLinkedDSO;
groupDtoModel.ableToEdit = canManageGroup;
groupDtoModel.group = group;
groupDtoModel.subgroups = subgroups.payload;
groupDtoModel.epersons = members.payload;
return groupDtoModel;
}
)
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
groupDtoModel.ableToDelete = canDelete && !hasLinkedDSO;
groupDtoModel.ableToEdit = canManageGroup;
groupDtoModel.group = group;
groupDtoModel.subgroups = subgroups.payload;
groupDtoModel.epersons = members.payload;
return groupDtoModel;
},
),
);
} else {
return EMPTY;
@@ -175,9 +222,9 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
})]).pipe(defaultIfEmpty([]), map((dtos: GroupDtoModel[]) => {
return buildPaginatedList(groups.pageInfo, dtos);
}));
})
}),
);
})
}),
).subscribe((value: PaginatedList<GroupDtoModel>) => {
this.groupsDto$.next(value);
this.pageInfoState$.next(value.pageInfo);
@@ -185,7 +232,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
});
this.subs.push(this.searchSub);
}
}
canManageGroup$(isSiteAdmin: boolean, group: Group): Observable<boolean> {
if (isSiteAdmin) {
@@ -210,7 +257,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: this.dsoNameService.getName(group.group) }),
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.content', { cause: rd.errorMessage }));
}
});
});
}
}
@@ -222,9 +269,9 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
*/
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
currentPage: 1,
elementsPerPage: 1,
}).pipe(getFirstSucceededRemoteData());
currentPage: 1,
elementsPerPage: 1,
}).pipe(getFirstSucceededRemoteData());
}
/**
@@ -235,9 +282,9 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
*/
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
return this.groupService.findListByHref(group._links.subgroups.href, {
currentPage: 1,
elementsPerPage: 1,
}).pipe(getFirstSucceededRemoteData());
currentPage: 1,
elementsPerPage: 1,
}).pipe(getFirstSucceededRemoteData());
}
/**

View File

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

View File

@@ -1,4 +1,7 @@
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
@@ -6,6 +9,11 @@ import { Component } from '@angular/core';
@Component({
selector: 'ds-admin-curation-task',
templateUrl: './admin-curation-tasks.component.html',
imports: [
CurationFormComponent,
TranslateModule,
],
standalone: true,
})
export class AdminCurationTasksComponent {

View File

@@ -1,22 +1,32 @@
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
import { BatchImportPageComponent } from './batch-import-page.component';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { FormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive';
import { FileValidator } from '../../shared/utils/require-file.validator';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import {
BATCH_IMPORT_SCRIPT_NAME,
ScriptDataService
} from '../../core/data/processes/script-data.service';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
ComponentFixture,
fakeAsync,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import {
BATCH_IMPORT_SCRIPT_NAME,
ScriptDataService,
} from '../../core/data/processes/script-data.service';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils';
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 { FileValidator } from '../../shared/utils/require-file.validator';
import { BatchImportPageComponent } from './batch-import-page.component';
describe('BatchImportPageComponent', () => {
let component: BatchImportPageComponent;
@@ -31,14 +41,14 @@ describe('BatchImportPageComponent', () => {
notificationService = new NotificationsServiceStub();
scriptService = jasmine.createSpyObj('scriptService',
{
invoke: createSuccessfulRemoteDataObject$({ processId: '46' })
}
invoke: createSuccessfulRemoteDataObject$({ processId: '46' }),
},
);
router = jasmine.createSpyObj('router', {
navigateByUrl: jasmine.createSpy('navigateByUrl')
navigateByUrl: jasmine.createSpy('navigateByUrl'),
});
locationStub = jasmine.createSpyObj('location', {
back: jasmine.createSpy('back')
back: jasmine.createSpy('back'),
});
}
@@ -48,17 +58,23 @@ describe('BatchImportPageComponent', () => {
imports: [
FormsModule,
TranslateModule.forRoot(),
RouterTestingModule.withRoutes([])
RouterTestingModule.withRoutes([]),
BatchImportPageComponent, FileValueAccessorDirective, FileValidator,
],
declarations: [BatchImportPageComponent, FileValueAccessorDirective, FileValidator],
providers: [
{ provide: NotificationsService, useValue: notificationService },
{ provide: ScriptDataService, useValue: scriptService },
{ provide: Router, useValue: router },
{ provide: Location, useValue: locationStub },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
schemas: [NO_ERRORS_SCHEMA],
})
.overrideComponent(BatchImportPageComponent, {
remove: {
imports: [FileDropzoneNoUploaderComponent],
},
})
.compileComponents();
}));
beforeEach(() => {
@@ -108,7 +124,7 @@ describe('BatchImportPageComponent', () => {
it('metadata-import script is invoked with --zip fileName and the mockFile', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--add' }),
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' })
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
];
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
});
@@ -181,7 +197,7 @@ describe('BatchImportPageComponent', () => {
it('metadata-import script is invoked with --url and the file url', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--add' }),
Object.assign(new ProcessParameter(), { name: '--url', value: 'example.fileURL.com' })
Object.assign(new ProcessParameter(), { name: '--url', value: 'example.fileURL.com' }),
];
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [null]);
});

View File

@@ -1,26 +1,48 @@
import { Component } from '@angular/core';
import { Location } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { BATCH_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
import { Router } from '@angular/router';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { RemoteData } from '../../core/data/remote-data';
import { Process } from '../../process-page/processes/process.model';
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths';
import {
ImportBatchSelectorComponent
} from '../../shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component';
Location,
NgIf,
} from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { UiSwitchModule } from 'ngx-ui-switch';
import { take } from 'rxjs/operators';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import {
BATCH_IMPORT_SCRIPT_NAME,
ScriptDataService,
} from '../../core/data/processes/script-data.service';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths';
import { Process } from '../../process-page/processes/process.model';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
import { ImportBatchSelectorComponent } from '../../shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component';
import {
isEmpty,
isNotEmpty,
} from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component';
@Component({
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 {
/**
@@ -91,7 +113,7 @@ export class BatchImportPageComponent {
}
} else {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--add' })
Object.assign(new ProcessParameter(), { name: '--add' }),
];
if (this.isUpload) {
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }));

View File

@@ -1,19 +1,32 @@
import { Location } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
import {
ComponentFixture,
fakeAsync,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
import {
METADATA_IMPORT_SCRIPT_NAME,
ScriptDataService,
} from '../../core/data/processes/script-data.service';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils';
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 { FileValidator } from '../../shared/utils/require-file.validator';
import { MetadataImportPageComponent } from './metadata-import-page.component';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
describe('MetadataImportPageComponent', () => {
let comp: MetadataImportPageComponent;
@@ -28,14 +41,14 @@ describe('MetadataImportPageComponent', () => {
notificationService = new NotificationsServiceStub();
scriptService = jasmine.createSpyObj('scriptService',
{
invoke: createSuccessfulRemoteDataObject$({ processId: '45' })
}
invoke: createSuccessfulRemoteDataObject$({ processId: '45' }),
},
);
router = jasmine.createSpyObj('router', {
navigateByUrl: jasmine.createSpy('navigateByUrl')
navigateByUrl: jasmine.createSpy('navigateByUrl'),
});
locationStub = jasmine.createSpyObj('location', {
back: jasmine.createSpy('back')
back: jasmine.createSpy('back'),
});
}
@@ -45,17 +58,23 @@ describe('MetadataImportPageComponent', () => {
imports: [
FormsModule,
TranslateModule.forRoot(),
RouterTestingModule.withRoutes([])
RouterTestingModule.withRoutes([]),
MetadataImportPageComponent, FileValueAccessorDirective, FileValidator,
],
declarations: [MetadataImportPageComponent, FileValueAccessorDirective, FileValidator],
providers: [
{ provide: NotificationsService, useValue: notificationService },
{ provide: ScriptDataService, useValue: scriptService },
{ provide: Router, useValue: router },
{ provide: Location, useValue: locationStub },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
schemas: [NO_ERRORS_SCHEMA],
})
.overrideComponent(MetadataImportPageComponent, {
remove: {
imports: [FileDropzoneNoUploaderComponent],
},
})
.compileComponents();
}));
beforeEach(() => {

View File

@@ -1,19 +1,34 @@
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {
METADATA_IMPORT_SCRIPT_NAME,
ScriptDataService,
} from '../../core/data/processes/script-data.service';
import { RemoteData } from '../../core/data/remote-data';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths';
import { Process } from '../../process-page/processes/process.model';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
import { isNotEmpty } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RemoteData } from '../../core/data/remote-data';
import { Process } from '../../process-page/processes/process.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths';
import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component';
@Component({
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,47 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component';
import { NavigationBreadcrumbResolver } from '../../core/breadcrumbs/navigation-breadcrumb.resolver';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AdminLdnServicesRoutingModule } from './admin-ldn-services-routing.module';
import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component';
import { SharedModule } from '../../shared/shared.module';
import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component';
import { FormsModule } from '@angular/forms';
import { LdnItemfiltersService } from './ldn-services-data/ldn-itemfilters-data.service';
@NgModule({
imports: [
CommonModule,
SharedModule,
AdminLdnServicesRoutingModule,
FormsModule
],
declarations: [
LdnServicesOverviewComponent,
LdnServiceFormComponent,
],
providers: [LdnItemfiltersService]
})
export class AdminLdnServicesModule {
}

View File

@@ -1,23 +1,49 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import {NgbDropdownModule, NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {LdnServiceFormComponent} from './ldn-service-form.component';
import {ChangeDetectorRef, EventEmitter} from '@angular/core';
import { FormArray, FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import {ActivatedRoute, Router} from '@angular/router';
import {TranslateModule, TranslateService} from '@ngx-translate/core';
import {PaginationService} from 'ngx-pagination';
import {NotificationsService} from '../../../shared/notifications/notifications.service';
import {LdnItemfiltersService} from '../ldn-services-data/ldn-itemfilters-data.service';
import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service';
import {RouterStub} from '../../../shared/testing/router.stub';
import {MockActivatedRoute} from '../../../shared/mocks/active-router.mock';
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service.stub';
import { of as observableOf, of } from 'rxjs';
import {RouteService} from '../../../core/services/route.service';
import {provideMockStore} from '@ngrx/store/testing';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import {
ChangeDetectorRef,
EventEmitter,
} from '@angular/core';
import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import {
FormArray,
FormBuilder,
FormControl,
FormGroup,
ReactiveFormsModule,
} from '@angular/forms';
import { By } from '@angular/platform-browser';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import {
NgbDropdownModule,
NgbModal,
} from '@ng-bootstrap/ng-bootstrap';
import { provideMockStore } from '@ngrx/store/testing';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { PaginationService } from 'ngx-pagination';
import {
of as observableOf,
of,
} from 'rxjs';
import { RouteService } from '../../../core/services/route.service';
import { MockActivatedRoute } from '../../../shared/mocks/active-router.mock';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { RouterStub } from '../../../shared/testing/router.stub';
import { LdnItemfiltersService } from '../ldn-services-data/ldn-itemfilters-data.service';
import { LdnServicesService } from '../ldn-services-data/ldn-services-data.service';
import { LdnServiceFormComponent } from './ldn-service-form.component';
describe('LdnServiceFormEditComponent', () => {
let component: LdnServiceFormComponent;
@@ -33,7 +59,7 @@ describe('LdnServiceFormEditComponent', () => {
const routeParams = {
serviceId: testId,
};
const routeUrlSegments = [{path: 'path'}];
const routeUrlSegments = [{ path: 'path' }];
const formMockValue = {
'id': '',
'name': 'name',
@@ -52,9 +78,9 @@ describe('LdnServiceFormEditComponent', () => {
'pattern': '',
'patternLabel': 'Select a pattern',
'constraint': '',
'automatic': false
}
]
'automatic': false,
},
],
};
@@ -63,7 +89,7 @@ describe('LdnServiceFormEditComponent', () => {
instant: () => 'translated-text',
onLangChange: new EventEmitter(),
onTranslationChange: new EventEmitter(),
onDefaultLangChange: new EventEmitter()
onDefaultLangChange: new EventEmitter(),
};
beforeEach(async () => {
@@ -77,33 +103,32 @@ describe('LdnServiceFormEditComponent', () => {
findAll: () => of(['item1', 'item2']),
};
cdRefStub = Object.assign({
detectChanges: () => fixture.detectChanges()
detectChanges: () => fixture.detectChanges(),
});
modalService = {
open: () => {/*comment*/
}
},
};
activatedRoute = new MockActivatedRoute(routeParams, routeUrlSegments);
await TestBed.configureTestingModule({
imports: [ReactiveFormsModule, TranslateModule.forRoot(), NgbDropdownModule],
declarations: [LdnServiceFormComponent],
imports: [ReactiveFormsModule, TranslateModule.forRoot(), NgbDropdownModule, LdnServiceFormComponent],
providers: [
{provide: LdnServicesService, useValue: ldnServicesService},
{provide: LdnItemfiltersService, useValue: ldnItemfiltersService},
{provide: Router, useValue: new RouterStub()},
{provide: ActivatedRoute, useValue: activatedRoute},
{provide: ChangeDetectorRef, useValue: cdRefStub},
{provide: NgbModal, useValue: modalService},
{provide: NotificationsService, useValue: new NotificationsServiceStub()},
{provide: TranslateService, useValue: translateServiceStub},
{provide: PaginationService, useValue: {}},
{ provide: LdnServicesService, useValue: ldnServicesService },
{ provide: LdnItemfiltersService, useValue: ldnItemfiltersService },
{ provide: Router, useValue: new RouterStub() },
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: ChangeDetectorRef, useValue: cdRefStub },
{ provide: NgbModal, useValue: modalService },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: TranslateService, useValue: translateServiceStub },
{ provide: PaginationService, useValue: {} },
FormBuilder,
RouteService,
provideMockStore({}),
]
],
})
.compileComponents();
@@ -139,7 +164,7 @@ describe('LdnServiceFormEditComponent', () => {
it('should handle create service with valid form', () => {
spyOn(component, 'fetchServiceData').and.callFake((a) => a);
component.formModel.addControl('notifyServiceInboundPatterns', (component as any).formBuilder.array([{pattern: 'patternValue'}]));
component.formModel.addControl('notifyServiceInboundPatterns', (component as any).formBuilder.array([{ pattern: 'patternValue' }]));
const nameInput = fixture.debugElement.query(By.css('#name'));
const descriptionInput = fixture.debugElement.query(By.css('#description'));
const urlInput = fixture.debugElement.query(By.css('#url'));
@@ -184,7 +209,7 @@ describe('LdnServiceFormEditComponent', () => {
spyOn(component.formModel, 'markAllAsTouched');
spyOn(component, 'closeModal');
spyOn(component, 'checkPatterns').and.callFake(() => true);
component.formModel.addControl('notifyServiceInboundPatterns', (component as any).formBuilder.array([{pattern: 'patternValue'}]));
component.formModel.addControl('notifyServiceInboundPatterns', (component as any).formBuilder.array([{ pattern: 'patternValue' }]));
component.formModel.patchValue(formMockValue);
component.createService();
@@ -196,13 +221,13 @@ describe('LdnServiceFormEditComponent', () => {
it('should check patterns', () => {
const arrValid = new FormArray([
new FormGroup({
pattern: new FormControl('pattern')
pattern: new FormControl('pattern'),
}),
]);
const arrInvalid = new FormArray([
new FormGroup({
pattern: new FormControl('')
pattern: new FormControl(''),
}),
]);

View File

@@ -1,38 +1,63 @@
import {
animate,
state,
style,
transition,
trigger,
} from '@angular/animations';
import {
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import {
ChangeDetectorRef,
Component,
OnDestroy,
OnInit,
TemplateRef,
ViewChild
ViewChild,
} from '@angular/core';
import {
FormArray,
FormBuilder,
FormGroup,
Validators
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type';
import {ActivatedRoute, Router} from '@angular/router';
import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service';
import {notifyPatterns} from '../ldn-services-patterns/ldn-service-coar-patterns';
import {animate, state, style, transition, trigger} from '@angular/animations';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {NotificationsService} from '../../../shared/notifications/notifications.service';
import {TranslateService} from '@ngx-translate/core';
import {LdnService} from '../ldn-services-model/ldn-services.model';
import {RemoteData} from 'src/app/core/data/remote-data';
import {Operation} from 'fast-json-patch';
import {getFirstCompletedRemoteData} from '../../../core/shared/operators';
import {LdnItemfiltersService} from '../ldn-services-data/ldn-itemfilters-data.service';
import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters';
import {PaginatedList} from '../../../core/data/paginated-list.model';
import {combineLatestWith, Observable, Subscription} from 'rxjs';
import {PaginationService} from '../../../core/pagination/pagination.service';
import {FindListOptions} from '../../../core/data/find-list-options.model';
import {NotifyServicePattern} from '../ldn-services-model/ldn-service-patterns.model';
import { IpV4Validator } from '../../../shared/utils/ipV4.validator';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import {
NgbDropdownModule,
NgbModal,
} from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { Operation } from 'fast-json-patch';
import {
combineLatestWith,
Observable,
Subscription,
} from 'rxjs';
import { RemoteData } from 'src/app/core/data/remote-data';
import { FindListOptions } from '../../../core/data/find-list-options.model';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { IpV4Validator } from '../../../shared/utils/ipV4.validator';
import { LdnItemfiltersService } from '../ldn-services-data/ldn-itemfilters-data.service';
import { LdnServicesService } from '../ldn-services-data/ldn-services-data.service';
import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type';
import { Itemfilter } from '../ldn-services-model/ldn-service-itemfilters';
import { NotifyServicePattern } from '../ldn-services-model/ldn-service-patterns.model';
import { LdnService } from '../ldn-services-model/ldn-services.model';
import { notifyPatterns } from '../ldn-services-patterns/ldn-service-coar-patterns';
/**
* Component for editing LDN service through a form that allows to create or edit the properties of a service
@@ -41,6 +66,7 @@ import { IpV4Validator } from '../../../shared/utils/ipV4.validator';
selector: 'ds-ldn-service-form',
templateUrl: './ldn-service-form.component.html',
styleUrls: ['./ldn-service-form.component.scss'],
standalone: true,
animations: [
trigger('toggleAnimation', [
state('true', style({})),
@@ -48,19 +74,27 @@ import { IpV4Validator } from '../../../shared/utils/ipV4.validator';
transition('true <=> false', animate('300ms ease-in')),
]),
],
imports: [
ReactiveFormsModule,
TranslateModule,
NgIf,
NgbDropdownModule,
NgForOf,
AsyncPipe,
],
})
export class LdnServiceFormComponent implements OnInit, OnDestroy {
formModel: FormGroup;
@ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef<any>;
@ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef<any>;
@ViewChild('confirmModal', { static: true }) confirmModal: TemplateRef<any>;
@ViewChild('resetFormModal', { static: true }) resetFormModal: TemplateRef<any>;
public inboundPatterns: string[] = notifyPatterns;
public isNewService: boolean;
public areControlsInitialized: boolean;
public itemfiltersRD$: Observable<RemoteData<PaginatedList<Itemfilter>>>;
public config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 20
elementsPerPage: 20,
});
public markedForDeletionInboundPattern: number[] = [];
public selectedInboundPatterns: string[];
@@ -83,7 +117,7 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
protected modalService: NgbModal,
private notificationService: NotificationsService,
private translateService: TranslateService,
protected paginationService: PaginationService
protected paginationService: PaginationService,
) {
this.formModel = this.formBuilder.group({
@@ -102,8 +136,8 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.routeSubscription = this.route.params.pipe(
combineLatestWith(this.route.url)
this.routeSubscription = this.route.params.pipe(
combineLatestWith(this.route.url),
).subscribe(([params, segment]) => {
this.serviceId = params.serviceId;
this.isNewService = segment[0].path === 'new';
@@ -154,17 +188,17 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
patternLabel: string,
constraintFormatted: string;
}) => {
const {patternLabel, ...rest} = pattern;
const { patternLabel, ...rest } = pattern;
delete rest.constraintFormatted;
return rest;
});
const values = {...this.formModel.value, enabled: true};
const values = { ...this.formModel.value, enabled: true };
const ldnServiceData = this.ldnServicesService.create(values);
ldnServiceData.pipe(
getFirstCompletedRemoteData()
getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData<LdnService>) => {
if (rd.hasSucceeded) {
this.notificationService.success(this.translateService.get('ldn-service-notification.created.success.title'),
@@ -204,7 +238,7 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
*/
fetchServiceData(serviceId: string): void {
this.ldnServicesService.findById(serviceId).pipe(
getFirstCompletedRemoteData()
getFirstCompletedRemoteData(),
).subscribe(
(data: RemoteData<LdnService>) => {
if (data.hasSucceeded) {
@@ -219,18 +253,18 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
type: this.ldnService.type,
enabled: this.ldnService.enabled,
lowerIp: this.ldnService.lowerIp,
upperIp: this.ldnService.upperIp
upperIp: this.ldnService.upperIp,
});
this.filterPatternObjectsAndAssignLabel('notifyServiceInboundPatterns');
let notifyServiceInboundPatternsFormArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
const notifyServiceInboundPatternsFormArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
notifyServiceInboundPatternsFormArray.controls.forEach(
control => {
const controlFormGroup = control as FormGroup;
const controlConstraint = controlFormGroup.get('constraint').value;
controlFormGroup.patchValue({
constraintFormatted: controlConstraint ? this.translateService.instant((controlConstraint as string) + '.label') : ''
constraintFormatted: controlConstraint ? this.translateService.instant((controlConstraint as string) + '.label') : '',
});
}
},
);
}
},
@@ -245,14 +279,13 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
const PatternsArray = this.formModel.get(formArrayName) as FormArray;
PatternsArray.clear();
let servicesToUse = this.ldnService.notifyServiceInboundPatterns;
const servicesToUse = this.ldnService.notifyServiceInboundPatterns;
servicesToUse.forEach((patternObj: NotifyServicePattern) => {
let patternFormGroup;
patternFormGroup = this.initializeInboundPatternFormGroup();
const patternFormGroup = this.initializeInboundPatternFormGroup();
const newPatternObjWithLabel = Object.assign(new NotifyServicePattern(), {
...patternObj,
patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternObj?.pattern + '.label')
patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternObj?.pattern + '.label'),
});
patternFormGroup.patchValue(newPatternObjWithLabel);
@@ -280,7 +313,7 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
this.deletedInboundPatterns.forEach(index => {
const removeOperation: Operation = {
op: 'remove',
path: `notifyServiceInboundPatterns[${index}]`
path: `notifyServiceInboundPatterns[${index}]`,
};
patchOperations.push(removeOperation);
});
@@ -310,8 +343,8 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
*/
selectInboundPattern(patternValue: string, index: number): void {
const patternArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray);
patternArray.controls[index].patchValue({pattern: patternValue});
patternArray.controls[index].patchValue({patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternValue + '.label')});
patternArray.controls[index].patchValue({ pattern: patternValue });
patternArray.controls[index].patchValue({ patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternValue + '.label') });
}
/**
@@ -323,7 +356,7 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
const filterArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray);
filterArray.controls[index].patchValue({
constraint: filterValue,
constraintFormatted: this.translateService.instant((filterValue !== '' ? filterValue : 'ldn.no-filter') + '.label')
constraintFormatted: this.translateService.instant((filterValue !== '' ? filterValue : 'ldn.no-filter') + '.label'),
});
filterArray.markAllAsTouched();
}
@@ -353,12 +386,12 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
};
this.ldnServicesService.patch(this.ldnService, [patchOperation]).pipe(
getFirstCompletedRemoteData()
getFirstCompletedRemoteData(),
).subscribe(
() => {
this.formModel.get('enabled').setValue(newStatus);
this.cdRef.detectChanges();
}
},
);
}
@@ -404,7 +437,7 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
}
this.ldnServicesService.patch(this.ldnService, patchOperations).pipe(
getFirstCompletedRemoteData()
getFirstCompletedRemoteData(),
).subscribe(
(rd: RemoteData<LdnService>) => {
if (rd.hasSucceeded) {
@@ -542,7 +575,7 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
constraint: '',
constraintFormatted: '',
automatic: false,
isNew: true
isNew: true,
};
if (this.isNewService) {
@@ -573,6 +606,6 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
private setLdnUrlError(): void {
const control = this.formModel.controls.ldnUrl;
const controlErrors = control.errors || {};
control.setErrors({...controlErrors, ldnUrlAlreadyAssociated: true });
control.setErrors({ ...controlErrors, ldnUrlAlreadyAssociated: true });
}
}

View File

@@ -1,9 +1,13 @@
import {LdnService} from '../ldn-services-model/ldn-services.model';
import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type';
import {RemoteData} from '../../../core/data/remote-data';
import {PaginatedList} from '../../../core/data/paginated-list.model';
import {Observable, of} from 'rxjs';
import {createSuccessfulRemoteDataObject$} from '../../../shared/remote-data.utils';
import {
Observable,
of,
} from 'rxjs';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type';
import { LdnService } from '../ldn-services-model/ldn-services.model';
export const mockLdnService: LdnService = {
uuid: '1',
@@ -31,7 +35,7 @@ export const mockLdnService: LdnService = {
type: LDN_SERVICE,
_links: {
self: {
href: 'http://localhost/api/ldn/ldnservices/1'
href: 'http://localhost/api/ldn/ldnservices/1',
},
},
get self(): string {
@@ -68,7 +72,7 @@ export const mockLdnServices: LdnService[] = [{
type: LDN_SERVICE,
_links: {
self: {
href: 'http://localhost/api/ldn/ldnservices/1'
href: 'http://localhost/api/ldn/ldnservices/1',
},
},
get self(): string {
@@ -100,12 +104,12 @@ export const mockLdnServices: LdnService[] = [{
type: LDN_SERVICE,
_links: {
self: {
href: 'http://localhost/api/ldn/ldnservices/1'
href: 'http://localhost/api/ldn/ldnservices/1',
},
},
get self(): string {
return '';
},
}
},
];
export const mockLdnServicesRD$: Observable<RemoteData<PaginatedList<LdnService>>> = of((mockLdnServices as unknown) as RemoteData<PaginatedList<LdnService>>);

View File

@@ -1,19 +1,23 @@
import {
cold,
getTestScheduler,
} from 'jasmine-marbles';
import { of } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { LdnItemfiltersService } from './ldn-itemfilters-data.service';
import { RequestService } from '../../../core/data/request.service';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { RequestEntry } from '../../../core/data/request-entry.model';
import { RemoteData } from '../../../core/data/remote-data';
import { RequestEntryState } from '../../../core/data/request-entry-state.model';
import { cold, getTestScheduler } from 'jasmine-marbles';
import { RestResponse } from '../../../core/cache/response.models';
import { of } from 'rxjs';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { FindAllData } from '../../../core/data/base/find-all-data';
import { testFindAllDataImplementation } from '../../../core/data/base/find-all-data.spec';
import { RemoteData } from '../../../core/data/remote-data';
import { RequestService } from '../../../core/data/request.service';
import { RequestEntry } from '../../../core/data/request-entry.model';
import { RequestEntryState } from '../../../core/data/request-entry-state.model';
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { LdnItemfiltersService } from './ldn-itemfilters-data.service';
describe('LdnItemfiltersService test', () => {
let scheduler: TestScheduler;
@@ -60,12 +64,12 @@ describe('LdnItemfiltersService test', () => {
});
halService = jasmine.createSpyObj('halService', {
getEndpoint: of(endpointURL)
getEndpoint: of(endpointURL),
});
rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: createSuccessfulRemoteDataObject$({}, 500),
buildList: cold('a', { a: remoteDataMocks.Success })
buildList: cold('a', { a: remoteDataMocks.Success }),
});

View File

@@ -1,27 +1,26 @@
import {Injectable} from '@angular/core';
import {dataService} from '../../../core/data/base/data-service.decorator';
import {LDN_SERVICE_CONSTRAINT_FILTERS} from '../ldn-services-model/ldn-service.resource-type';
import {IdentifiableDataService} from '../../../core/data/base/identifiable-data.service';
import {FindAllData, FindAllDataImpl} from '../../../core/data/base/find-all-data';
import {RequestService} from '../../../core/data/request.service';
import {RemoteDataBuildService} from '../../../core/cache/builders/remote-data-build.service';
import {ObjectCacheService} from '../../../core/cache/object-cache.service';
import {HALEndpointService} from '../../../core/shared/hal-endpoint.service';
import {NotificationsService} from '../../../shared/notifications/notifications.service';
import {FindListOptions} from '../../../core/data/find-list-options.model';
import {FollowLinkConfig} from '../../../shared/utils/follow-link-config.model';
import {Observable} from 'rxjs';
import {RemoteData} from '../../../core/data/remote-data';
import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters';
import {PaginatedList} from '../../../core/data/paginated-list.model';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import {
FindAllData,
FindAllDataImpl,
} from '../../../core/data/base/find-all-data';
import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service';
import { FindListOptions } from '../../../core/data/find-list-options.model';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { RequestService } from '../../../core/data/request.service';
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
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
*/
@Injectable()
@dataService(LDN_SERVICE_CONSTRAINT_FILTERS)
@Injectable({ providedIn: 'root' })
export class LdnItemfiltersService extends IdentifiableDataService<Itemfilter> implements FindAllData<Itemfilter> {
private findAllData: FindAllDataImpl<Itemfilter>;

View File

@@ -1,131 +1,134 @@
import { TestScheduler } from 'rxjs/testing';
import { RequestService } from '../../../core/data/request.service';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { RequestEntry } from '../../../core/data/request-entry.model';
import { RemoteData } from '../../../core/data/remote-data';
import { RequestEntryState } from '../../../core/data/request-entry-state.model';
import { cold, getTestScheduler } from 'jasmine-marbles';
import { RestResponse } from '../../../core/cache/response.models';
import {
cold,
getTestScheduler,
} from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { FindAllData } from '../../../core/data/base/find-all-data';
import { testFindAllDataImplementation } from '../../../core/data/base/find-all-data.spec';
import { LdnServicesService } from './ldn-services-data.service';
import { testDeleteDataImplementation } from '../../../core/data/base/delete-data.spec';
import { DeleteData } from '../../../core/data/base/delete-data';
import { testSearchDataImplementation } from '../../../core/data/base/search-data.spec';
import { SearchData } from '../../../core/data/base/search-data';
import { testPatchDataImplementation } from '../../../core/data/base/patch-data.spec';
import { PatchData } from '../../../core/data/base/patch-data';
import { TestScheduler } from 'rxjs/testing';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { RequestParam } from '../../../core/cache/models/request-param.model';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { RestResponse } from '../../../core/cache/response.models';
import { CreateData } from '../../../core/data/base/create-data';
import { testCreateDataImplementation } from '../../../core/data/base/create-data.spec';
import { DeleteData } from '../../../core/data/base/delete-data';
import { testDeleteDataImplementation } from '../../../core/data/base/delete-data.spec';
import { FindAllData } from '../../../core/data/base/find-all-data';
import { testFindAllDataImplementation } from '../../../core/data/base/find-all-data.spec';
import { PatchData } from '../../../core/data/base/patch-data';
import { testPatchDataImplementation } from '../../../core/data/base/patch-data.spec';
import { SearchData } from '../../../core/data/base/search-data';
import { testSearchDataImplementation } from '../../../core/data/base/search-data.spec';
import { FindListOptions } from '../../../core/data/find-list-options.model';
import { RequestParam } from '../../../core/cache/models/request-param.model';
import { mockLdnService } from '../ldn-service-serviceMock/ldnServicesRD$-mock';
import { RemoteData } from '../../../core/data/remote-data';
import { RequestService } from '../../../core/data/request.service';
import { RequestEntry } from '../../../core/data/request-entry.model';
import { RequestEntryState } from '../../../core/data/request-entry-state.model';
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { createPaginatedList } from '../../../shared/testing/utils.test';
import { mockLdnService } from '../ldn-service-serviceMock/ldnServicesRD$-mock';
import { LdnServicesService } from './ldn-services-data.service';
describe('LdnServicesService test', () => {
let scheduler: TestScheduler;
let service: LdnServicesService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
let halService: HALEndpointService;
let notificationsService: NotificationsService;
let responseCacheEntry: RequestEntry;
let scheduler: TestScheduler;
let service: LdnServicesService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
let halService: HALEndpointService;
let notificationsService: NotificationsService;
let responseCacheEntry: RequestEntry;
const endpointURL = `https://rest.api/rest/api/ldn/ldnservices`;
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
const endpointURL = `https://rest.api/rest/api/ldn/ldnservices`;
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
const remoteDataMocks = {
Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200),
};
const remoteDataMocks = {
Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200),
};
function initTestService() {
return new LdnServicesService(
requestService,
rdbService,
objectCache,
halService,
notificationsService,
);
}
function initTestService() {
return new LdnServicesService(
requestService,
rdbService,
objectCache,
halService,
notificationsService,
);
}
beforeEach(() => {
scheduler = getTestScheduler();
beforeEach(() => {
scheduler = getTestScheduler();
objectCache = {} as ObjectCacheService;
notificationsService = {} as NotificationsService;
responseCacheEntry = new RequestEntry();
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
objectCache = {} as ObjectCacheService;
notificationsService = {} as NotificationsService;
responseCacheEntry = new RequestEntry();
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID,
send: true,
removeByHrefSubstring: {},
getByHref: observableOf(responseCacheEntry),
getByUUID: observableOf(responseCacheEntry),
});
halService = jasmine.createSpyObj('halService', {
getEndpoint: observableOf(endpointURL)
});
rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: createSuccessfulRemoteDataObject$({}, 500),
buildFromRequestUUID: createSuccessfulRemoteDataObject$({}, 500),
buildList: cold('a', { a: remoteDataMocks.Success })
});
service = initTestService();
requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID,
send: true,
removeByHrefSubstring: {},
getByHref: observableOf(responseCacheEntry),
getByUUID: observableOf(responseCacheEntry),
});
describe('composition', () => {
const initFindAllService = () => new LdnServicesService(null, null, null, null, null) as unknown as FindAllData<any>;
const initDeleteService = () => new LdnServicesService(null, null, null, null, null) as unknown as DeleteData<any>;
const initSearchService = () => new LdnServicesService(null, null, null, null, null) as unknown as SearchData<any>;
const initPatchService = () => new LdnServicesService(null, null, null, null, null) as unknown as PatchData<any>;
const initCreateService = () => new LdnServicesService(null, null, null, null, null) as unknown as CreateData<any>;
testFindAllDataImplementation(initFindAllService);
testDeleteDataImplementation(initDeleteService);
testSearchDataImplementation(initSearchService);
testPatchDataImplementation(initPatchService);
testCreateDataImplementation(initCreateService);
halService = jasmine.createSpyObj('halService', {
getEndpoint: observableOf(endpointURL),
});
describe('custom methods', () => {
it('should find service by inbound pattern', (done) => {
const params = [new RequestParam('pattern', 'testPattern')];
const findListOptions = Object.assign(new FindListOptions(), {}, {searchParams: params});
spyOn(service, 'searchBy').and.returnValue(observableOf(null));
spyOn((service as any).searchData, 'searchBy').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockLdnService])));
service.findByInboundPattern('testPattern').subscribe(() => {
expect(service.searchBy).toHaveBeenCalledWith('byInboundPattern', findListOptions, undefined, undefined );
done();
});
});
it('should invoke service', (done) => {
const constraints = [{void: true}];
const files = [new File([],'fileName')];
spyOn(service as any, 'getInvocationFormData');
spyOn(service, 'getBrowseEndpoint').and.returnValue(observableOf('testEndpoint'));
service.invoke('serviceName', 'serviceId', constraints, files).subscribe(result => {
expect((service as any).getInvocationFormData).toHaveBeenCalledWith(constraints, files);
expect(service.getBrowseEndpoint).toHaveBeenCalled();
expect(result).toBeInstanceOf(RemoteData);
done();
});
});
rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: createSuccessfulRemoteDataObject$({}, 500),
buildFromRequestUUID: createSuccessfulRemoteDataObject$({}, 500),
buildList: cold('a', { a: remoteDataMocks.Success }),
});
service = initTestService();
});
describe('composition', () => {
const initFindAllService = () => new LdnServicesService(null, null, null, null, null) as unknown as FindAllData<any>;
const initDeleteService = () => new LdnServicesService(null, null, null, null, null) as unknown as DeleteData<any>;
const initSearchService = () => new LdnServicesService(null, null, null, null, null) as unknown as SearchData<any>;
const initPatchService = () => new LdnServicesService(null, null, null, null, null) as unknown as PatchData<any>;
const initCreateService = () => new LdnServicesService(null, null, null, null, null) as unknown as CreateData<any>;
testFindAllDataImplementation(initFindAllService);
testDeleteDataImplementation(initDeleteService);
testSearchDataImplementation(initSearchService);
testPatchDataImplementation(initPatchService);
testCreateDataImplementation(initCreateService);
});
describe('custom methods', () => {
it('should find service by inbound pattern', (done) => {
const params = [new RequestParam('pattern', 'testPattern')];
const findListOptions = Object.assign(new FindListOptions(), {}, { searchParams: params });
spyOn(service, 'searchBy').and.returnValue(observableOf(null));
spyOn((service as any).searchData, 'searchBy').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockLdnService])));
service.findByInboundPattern('testPattern').subscribe(() => {
expect(service.searchBy).toHaveBeenCalledWith('byInboundPattern', findListOptions, undefined, undefined );
done();
});
});
it('should invoke service', (done) => {
const constraints = [{ void: true }];
const files = [new File([],'fileName')];
spyOn(service as any, 'getInvocationFormData');
spyOn(service, 'getBrowseEndpoint').and.returnValue(observableOf('testEndpoint'));
service.invoke('serviceName', 'serviceId', constraints, files).subscribe(result => {
expect((service as any).getInvocationFormData).toHaveBeenCalledWith(constraints, files);
expect(service.getBrowseEndpoint).toHaveBeenCalled();
expect(result).toBeInstanceOf(RemoteData);
done();
});
});
});
});

View File

@@ -1,36 +1,47 @@
import {Injectable} from '@angular/core';
import {dataService} from '../../../core/data/base/data-service.decorator';
import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type';
import {IdentifiableDataService} from '../../../core/data/base/identifiable-data.service';
import {FindAllData, FindAllDataImpl} from '../../../core/data/base/find-all-data';
import {DeleteData, DeleteDataImpl} from '../../../core/data/base/delete-data';
import {RequestService} from '../../../core/data/request.service';
import {RemoteDataBuildService} from '../../../core/cache/builders/remote-data-build.service';
import {ObjectCacheService} from '../../../core/cache/object-cache.service';
import {HALEndpointService} from '../../../core/shared/hal-endpoint.service';
import {NotificationsService} from '../../../shared/notifications/notifications.service';
import {FindListOptions} from '../../../core/data/find-list-options.model';
import {FollowLinkConfig} from '../../../shared/utils/follow-link-config.model';
import {Observable} from 'rxjs';
import {RemoteData} from '../../../core/data/remote-data';
import {PaginatedList} from '../../../core/data/paginated-list.model';
import {NoContent} from '../../../core/shared/NoContent.model';
import {map, take} from 'rxjs/operators';
import {URLCombiner} from '../../../core/url-combiner/url-combiner';
import {MultipartPostRequest} from '../../../core/data/request.models';
import {RestRequest} from '../../../core/data/rest-request.model';
import { Injectable } from '@angular/core';
import { Operation } from 'fast-json-patch';
import { Observable } from 'rxjs';
import {
map,
take,
} from 'rxjs/operators';
import {LdnService} from '../ldn-services-model/ldn-services.model';
import {PatchData, PatchDataImpl} from '../../../core/data/base/patch-data';
import {ChangeAnalyzer} from '../../../core/data/change-analyzer';
import {Operation} from 'fast-json-patch';
import {RestRequestMethod} from '../../../core/data/rest-request-method';
import {CreateData, CreateDataImpl} from '../../../core/data/base/create-data';
import {LdnServiceConstrain} from '../ldn-services-model/ldn-service.constrain.model';
import {SearchDataImpl} from '../../../core/data/base/search-data';
import {RequestParam} from '../../../core/cache/models/request-param.model';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { RequestParam } from '../../../core/cache/models/request-param.model';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import {
CreateData,
CreateDataImpl,
} from '../../../core/data/base/create-data';
import {
DeleteData,
DeleteDataImpl,
} from '../../../core/data/base/delete-data';
import {
FindAllData,
FindAllDataImpl,
} from '../../../core/data/base/find-all-data';
import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service';
import {
PatchData,
PatchDataImpl,
} from '../../../core/data/base/patch-data';
import { SearchDataImpl } from '../../../core/data/base/search-data';
import { ChangeAnalyzer } from '../../../core/data/change-analyzer';
import { FindListOptions } from '../../../core/data/find-list-options.model';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { MultipartPostRequest } from '../../../core/data/request.models';
import { RequestService } from '../../../core/data/request.service';
import { RestRequest } from '../../../core/data/rest-request.model';
import { RestRequestMethod } from '../../../core/data/rest-request-method';
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
import { NoContent } from '../../../core/shared/NoContent.model';
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { LdnServiceConstrain } from '../ldn-services-model/ldn-service.constrain.model';
import { LdnService } from '../ldn-services-model/ldn-services.model';
/**
* Injectable service responsible for fetching/sending data from/to the REST API on the ldnservices endpoint.
@@ -43,8 +54,7 @@ import {RequestParam} from '../../../core/cache/models/request-param.model';
* @implements {PatchData<LdnService>}
* @implements {CreateData<LdnService>}
*/
@Injectable()
@dataService(LDN_SERVICE)
@Injectable({ providedIn: 'root' })
export class LdnServicesService extends IdentifiableDataService<LdnService> implements FindAllData<LdnService>, DeleteData<LdnService>, PatchData<LdnService>, CreateData<LdnService> {
createData: CreateDataImpl<LdnService>;
private findAllData: FindAllDataImpl<LdnService>;
@@ -147,7 +157,7 @@ export class LdnServicesService extends IdentifiableDataService<LdnService> impl
*/
findByInboundPattern(pattern: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<LdnService>[]): Observable<RemoteData<PaginatedList<LdnService>>> {
const params = [new RequestParam('pattern', pattern)];
const findListOptions = Object.assign(new FindListOptions(), options, {searchParams: params});
const findListOptions = Object.assign(new FindListOptions(), options, { searchParams: params });
return this.searchBy(this.findByPatternEndpoint, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
@@ -200,7 +210,7 @@ export class LdnServicesService extends IdentifiableDataService<LdnService> impl
map((endpoint: string) => {
const body = this.getInvocationFormData(parameters, files);
return new MultipartPostRequest(requestId, endpoint, body);
})
}),
).subscribe((request: RestRequest) => this.requestService.send(request));
return this.rdbService.buildFromRequestUUID<LdnService>(requestId);

View File

@@ -1,19 +1,37 @@
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import { ChangeDetectorRef, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
import {NotificationsService} from '../../../shared/notifications/notifications.service';
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service.stub';
import {TranslateModule, TranslateService} from '@ngx-translate/core';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service';
import {PaginationService} from '../../../core/pagination/pagination.service';
import {PaginationServiceStub} from '../../../shared/testing/pagination-service.stub';
import {of} from 'rxjs';
import {LdnService} from '../ldn-services-model/ldn-services.model';
import {PaginatedList} from '../../../core/data/paginated-list.model';
import {RemoteData} from '../../../core/data/remote-data';
import {LdnServicesOverviewComponent} from './ldn-services-directory.component';
import {createSuccessfulRemoteDataObject$} from '../../../shared/remote-data.utils';
import {createPaginatedList} from '../../../shared/testing/utils.test';
import {
ChangeDetectorRef,
EventEmitter,
NO_ERRORS_SCHEMA,
} from '@angular/core';
import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { of } from 'rxjs';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
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 { LdnService } from '../ldn-services-model/ldn-services.model';
import { LdnServicesOverviewComponent } from './ldn-services-directory.component';
describe('LdnServicesOverviewComponent', () => {
let component: LdnServicesOverviewComponent;
@@ -26,7 +44,7 @@ describe('LdnServicesOverviewComponent', () => {
get: () => of('translated-text'),
onLangChange: new EventEmitter(),
onTranslationChange: new EventEmitter(),
onDefaultLangChange: new EventEmitter()
onDefaultLangChange: new EventEmitter(),
};
beforeEach(async () => {
@@ -37,26 +55,37 @@ describe('LdnServicesOverviewComponent', () => {
'patch': createSuccessfulRemoteDataObject$({}),
});
await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [LdnServicesOverviewComponent],
imports: [TranslateModule.forRoot(), LdnServicesOverviewComponent],
providers: [
{
provide: LdnServicesService,
useValue: ldnServicesService
useValue: ldnServicesService,
},
{provide: PaginationService, useValue: paginationService},
{ provide: PaginationService, useValue: paginationService },
{
provide: NgbModal, useValue: {
open: () => { /*comment*/
}
}
open: () => {
//
},
},
},
{provide: ChangeDetectorRef, useValue: {}},
{provide: NotificationsService, useValue: new NotificationsServiceStub()},
{provide: TranslateService, useValue: translateServiceStub},
{ provide: ChangeDetectorRef, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: TranslateService, useValue: translateServiceStub },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
schemas: [NO_ERRORS_SCHEMA],
})
.overrideComponent(LdnServicesOverviewComponent, {
remove: {
imports: [
PaginationComponent,
TruncatableComponent,
TruncatablePartComponent,
],
},
})
.compileComponents();
});
beforeEach(() => {
@@ -65,8 +94,8 @@ describe('LdnServicesOverviewComponent', () => {
ldnServicesService = TestBed.inject(LdnServicesService);
paginationService = TestBed.inject(PaginationService);
modalService = TestBed.inject(NgbModal);
component.modalRef = jasmine.createSpyObj({close: null});
component.isProcessingSub = jasmine.createSpyObj({unsubscribe: null});
component.modalRef = jasmine.createSpyObj({ close: null });
component.isProcessingSub = jasmine.createSpyObj({ unsubscribe: null });
component.ldnServicesRD$ = of({} as RemoteData<PaginatedList<LdnService>>);
fixture.detectChanges();
});
@@ -86,19 +115,17 @@ describe('LdnServicesOverviewComponent', () => {
it('should set ldnServicesRD$ with mock data', fakeAsync(() => {
spyOn(component, 'setLdnServices').and.callThrough();
const testData: LdnService[] = Object.assign([new LdnService()], [
{id: 1, name: 'Service 1', description: 'Description 1', enabled: true},
{id: 2, name: 'Service 2', description: 'Description 2', enabled: false},
{id: 3, name: 'Service 3', description: 'Description 3', enabled: true}]);
{ id: 1, name: 'Service 1', description: 'Description 1', enabled: true },
{ id: 2, name: 'Service 2', description: 'Description 2', enabled: false },
{ id: 3, name: 'Service 3', description: 'Description 3', enabled: true }]);
const mockLdnServicesRD = createPaginatedList(testData);
component.ldnServicesRD$ = createSuccessfulRemoteDataObject$(mockLdnServicesRD);
fixture.detectChanges();
const tableRows = fixture.debugElement.nativeElement.querySelectorAll('tbody tr');
expect(tableRows.length).toBe(testData.length);
const firstRowContent = tableRows[0].textContent;
expect(firstRowContent).toContain('Service 1');
expect(firstRowContent).toContain('Description 1');
component.ldnServicesRD$.subscribe((rd) => {
expect(rd.payload.page).toEqual(mockLdnServicesRD.page);
});
}));
});
@@ -155,7 +182,7 @@ describe('LdnServicesOverviewComponent', () => {
describe('toggleStatus', () => {
it('should toggle status', (() => {
component.toggleStatus({enabled: false}, ldnServicesService);
component.toggleStatus({ enabled: false }, ldnServicesService);
expect(ldnServicesService.patch).toHaveBeenCalled();
}));
});

View File

@@ -1,3 +1,9 @@
import {
AsyncPipe,
NgClass,
NgFor,
NgIf,
} from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
@@ -5,24 +11,37 @@ import {
OnDestroy,
OnInit,
TemplateRef,
ViewChild
ViewChild,
} from '@angular/core';
import {Observable, Subscription} from 'rxjs';
import {RemoteData} from '../../../core/data/remote-data';
import {PaginatedList} from '../../../core/data/paginated-list.model';
import {FindListOptions} from '../../../core/data/find-list-options.model';
import {LdnService} from '../ldn-services-model/ldn-services.model';
import {PaginationComponentOptions} from '../../../shared/pagination/pagination-component-options.model';
import {map, switchMap} from 'rxjs/operators';
import {LdnServicesService} from 'src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service';
import {PaginationService} from 'src/app/core/pagination/pagination.service';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {hasValue} from '../../../shared/empty.util';
import {Operation} from 'fast-json-patch';
import {getFirstCompletedRemoteData} from '../../../core/shared/operators';
import {NotificationsService} from '../../../shared/notifications/notifications.service';
import {TranslateService} from '@ngx-translate/core';
import { RouterLink } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { Operation } from 'fast-json-patch';
import {
Observable,
Subscription,
} from 'rxjs';
import {
map,
switchMap,
} from 'rxjs/operators';
import { LdnServicesService } from 'src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service';
import { PaginationService } from 'src/app/core/pagination/pagination.service';
import { FindListOptions } from '../../../core/data/find-list-options.model';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { 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';
/**
* The `LdnServicesOverviewComponent` is a component that provides an overview of LDN (Linked Data Notifications) services.
@@ -33,20 +52,32 @@ import {TranslateService} from '@ngx-translate/core';
selector: 'ds-ldn-services-directory',
templateUrl: './ldn-services-directory.component.html',
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 {
selectedServiceId: string | number | null = null;
servicesData: any[] = [];
@ViewChild('deleteModal', {static: true}) deleteModal: TemplateRef<any>;
@ViewChild('deleteModal', { static: true }) deleteModal: TemplateRef<any>;
ldnServicesRD$: Observable<RemoteData<PaginatedList<LdnService>>>;
config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 10
elementsPerPage: 10,
});
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'po',
pageSize: 10
pageSize: 10,
});
isProcessingSub: Subscription;
modalRef: any;
@@ -72,8 +103,8 @@ export class LdnServicesOverviewComponent implements OnInit, OnDestroy {
setLdnServices() {
this.ldnServicesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
switchMap((config) => this.ldnServicesService.findAll(config, false, false).pipe(
getFirstCompletedRemoteData()
))
getFirstCompletedRemoteData(),
)),
);
}
@@ -128,7 +159,7 @@ export class LdnServicesOverviewComponent implements OnInit, OnDestroy {
remoteData.payload.page = remoteData.payload.page.filter(service => service.id.toString() !== serviceId);
}
return remoteData;
})
}),
);
this.cdRef.detectChanges();
this.closeModal();
@@ -170,7 +201,7 @@ export class LdnServicesOverviewComponent implements OnInit, OnDestroy {
this.notificationService.error(this.translateService.get('ldn-enable-service.notification.error.title'),
this.translateService.get('ldn-enable-service.notification.error.content'));
}
}
},
);
}
}

View File

@@ -1,9 +1,14 @@
import {autoserialize, deserialize, inheritSerialization} from 'cerialize';
import {LDN_SERVICE_CONSTRAINT_FILTER} from './ldn-service.resource-type';
import {CacheableObject} from '../../../core/cache/cacheable-object.model';
import {typedObject} from '../../../core/cache/builders/build-decorators';
import {excludeFromEquals} from '../../../core/utilities/equals.decorators';
import {ResourceType} from '../../../core/shared/resource-type';
import {
autoserialize,
deserialize,
inheritSerialization,
} from 'cerialize';
import { typedObject } from '../../../core/cache/builders/build-decorators';
import { CacheableObject } from '../../../core/cache/cacheable-object.model';
import { ResourceType } from '../../../core/shared/resource-type';
import { excludeFromEquals } from '../../../core/utilities/equals.decorators';
import { LDN_SERVICE_CONSTRAINT_FILTER } from './ldn-service.resource-type';
/** A single filter value and its properties. */
@typedObject
@@ -13,13 +18,13 @@ export class Itemfilter extends CacheableObject {
@excludeFromEquals
@autoserialize
type: ResourceType;
type: ResourceType;
@autoserialize
id: string;
id: string;
@deserialize
_links: {
_links: {
self: {
href: string;
};

View File

@@ -1,13 +1,13 @@
import {autoserialize} from 'cerialize';
import { autoserialize } from 'cerialize';
/**
* A single notify service pattern and his properties
*/
export class NotifyServicePattern {
@autoserialize
pattern: string;
pattern: string;
@autoserialize
constraint: string;
constraint: string;
@autoserialize
automatic: string;
automatic: string;
}

View File

@@ -4,7 +4,7 @@
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
import {ResourceType} from '../../../core/shared/resource-type';
import { ResourceType } from '../../../core/shared/resource-type';
export const LDN_SERVICE = new ResourceType('ldnservice');
export const LDN_SERVICE_CONSTRAINT_FILTERS = new ResourceType('itemfilters');

View File

@@ -1,11 +1,16 @@
import {ResourceType} from '../../../core/shared/resource-type';
import {CacheableObject} from '../../../core/cache/cacheable-object.model';
import {autoserialize, deserialize, deserializeAs, inheritSerialization} from 'cerialize';
import {LDN_SERVICE} from './ldn-service.resource-type';
import {excludeFromEquals} from '../../../core/utilities/equals.decorators';
import {typedObject} from '../../../core/cache/builders/build-decorators';
import {NotifyServicePattern} from './ldn-service-patterns.model';
import {
autoserialize,
deserialize,
deserializeAs,
inheritSerialization,
} from 'cerialize';
import { typedObject } from '../../../core/cache/builders/build-decorators';
import { CacheableObject } from '../../../core/cache/cacheable-object.model';
import { ResourceType } from '../../../core/shared/resource-type';
import { excludeFromEquals } from '../../../core/utilities/equals.decorators';
import { LDN_SERVICE } from './ldn-service.resource-type';
import { NotifyServicePattern } from './ldn-service-patterns.model';
/**
* LDN Services bounded to each selected pattern, relation set in service creation
@@ -24,43 +29,43 @@ export class LdnService extends CacheableObject {
@excludeFromEquals
@autoserialize
type: ResourceType;
type: ResourceType;
@autoserialize
id: number;
id: number;
@deserializeAs('id')
uuid: string;
uuid: string;
@autoserialize
name: string;
name: string;
@autoserialize
description: string;
description: string;
@autoserialize
url: string;
url: string;
@autoserialize
score: number;
score: number;
@autoserialize
enabled: boolean;
enabled: boolean;
@autoserialize
ldnUrl: string;
ldnUrl: string;
@autoserialize
lowerIp: string;
lowerIp: string;
@autoserialize
upperIp: string;
upperIp: string;
@autoserialize
notifyServiceInboundPatterns?: NotifyServicePattern[];
notifyServiceInboundPatterns?: NotifyServicePattern[];
@deserialize
_links: {
_links: {
self: {
href: string;
};

View File

@@ -1,5 +1,8 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import {
ActivatedRouteSnapshot,
RouterStateSnapshot,
} from '@angular/router';
/**
* Interface for the route parameters.
@@ -13,8 +16,8 @@ export interface NotificationsSuggestionTargetsPageParams {
/**
* This class represents a resolver that retrieve the route data before the route is activated.
*/
@Injectable()
export class NotificationsSuggestionTargetsPageResolver implements Resolve<NotificationsSuggestionTargetsPageParams> {
@Injectable({ providedIn: 'root' })
export class NotificationsSuggestionTargetsPageResolver {
/**
* Method for resolving the parameters in the current route.
@@ -26,7 +29,7 @@ export class NotificationsSuggestionTargetsPageResolver implements Resolve<Notif
return {
pageId: route.queryParams.pageId,
pageSize: parseInt(route.queryParams.pageSize, 10),
currentPage: parseInt(route.queryParams.page, 10)
currentPage: parseInt(route.queryParams.page, 10),
};
}
}

View File

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

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