mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'upstream/main' into embargo-date
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
@@ -15,3 +15,6 @@ trim_trailing_whitespace = false
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.json5]
|
||||
ij_json_keep_blank_lines_in_code = 3
|
||||
|
@@ -7,7 +7,8 @@
|
||||
"eslint-plugin-jsdoc",
|
||||
"eslint-plugin-deprecation",
|
||||
"unused-imports",
|
||||
"eslint-plugin-lodash"
|
||||
"eslint-plugin-lodash",
|
||||
"eslint-plugin-jsonc"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
@@ -224,6 +225,42 @@
|
||||
"@angular-eslint/template/no-negated-async": "off",
|
||||
"@angular-eslint/template/eqeqeq": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.json5"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:jsonc/recommended-with-jsonc"
|
||||
],
|
||||
"rules": {
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"jsonc/comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"jsonc/indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"jsonc/key-spacing": [
|
||||
"error",
|
||||
{
|
||||
"beforeColon": false,
|
||||
"afterColon": true,
|
||||
"mode": "strict"
|
||||
}
|
||||
],
|
||||
"jsonc/no-dupe-keys": "off",
|
||||
"jsonc/quotes": [
|
||||
"error",
|
||||
"double",
|
||||
{
|
||||
"avoidEscape": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
59
.github/workflows/build.yml
vendored
59
.github/workflows/build.yml
vendored
@@ -15,15 +15,24 @@ jobs:
|
||||
env:
|
||||
# The ci step will test the dspace-angular code against DSpace REST.
|
||||
# Direct that step to utilize a DSpace REST service that has been started in docker.
|
||||
# NOTE: These settings should be kept in sync with those in [src]/docker/docker-compose-ci.yml
|
||||
DSPACE_REST_HOST: 127.0.0.1
|
||||
DSPACE_REST_PORT: 8080
|
||||
DSPACE_REST_NAMESPACE: '/server'
|
||||
DSPACE_REST_SSL: false
|
||||
# Spin up UI on 127.0.0.1 to avoid host resolution issues in e2e tests with Node 18+
|
||||
DSPACE_UI_HOST: 127.0.0.1
|
||||
DSPACE_UI_PORT: 4000
|
||||
# Ensure all SSR caching is disabled in test environment
|
||||
DSPACE_CACHE_SERVERSIDE_BOTCACHE_MAX: 0
|
||||
DSPACE_CACHE_SERVERSIDE_ANONYMOUSCACHE_MAX: 0
|
||||
# Tell Cypress to run e2e tests using the same UI URL
|
||||
CYPRESS_BASE_URL: http://127.0.0.1:4000
|
||||
# When Chrome version is specified, we pin to a specific version of Chrome
|
||||
# Comment this out to use the latest release
|
||||
#CHROME_VERSION: "90.0.4430.212-1"
|
||||
# Bump Node heap size (OOM in CI after upgrading to Angular 15)
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
strategy:
|
||||
# Create a matrix of Node versions to test against (in parallel)
|
||||
matrix:
|
||||
@@ -61,7 +70,7 @@ jobs:
|
||||
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
||||
- name: Get Yarn cache directory
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
- name: Cache Yarn dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
@@ -86,12 +95,16 @@ jobs:
|
||||
- name: Run specs (unit tests)
|
||||
run: yarn run test:headless
|
||||
|
||||
# Upload code coverage report to artifact (for one version of Node only),
|
||||
# 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
|
||||
# Upload coverage reports to Codecov (for one version of Node only)
|
||||
# https://github.com/codecov/codecov-action
|
||||
- name: Upload coverage to Codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
if: matrix.node-version == '16.x'
|
||||
- name: Upload code coverage report to Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
if: matrix.node-version == '18.x'
|
||||
with:
|
||||
name: dspace-angular coverage report
|
||||
path: 'coverage/dspace-angular/lcov.info'
|
||||
retention-days: 14
|
||||
|
||||
# Using docker-compose start backend using CI configuration
|
||||
# and load assetstore from a cached copy
|
||||
@@ -105,11 +118,10 @@ jobs:
|
||||
# https://github.com/cypress-io/github-action
|
||||
# (NOTE: to run these e2e tests locally, just use 'ng e2e')
|
||||
- name: Run e2e tests (integration tests)
|
||||
uses: cypress-io/github-action@v4
|
||||
uses: cypress-io/github-action@v5
|
||||
with:
|
||||
# Run tests in Chrome, headless mode
|
||||
# Run tests in Chrome, headless mode (default)
|
||||
browser: chrome
|
||||
headless: true
|
||||
# Start app before running tests (will be stopped automatically after tests finish)
|
||||
start: yarn run serve:ssr
|
||||
# Wait for backend & frontend to be available
|
||||
@@ -169,3 +181,32 @@ jobs:
|
||||
|
||||
- name: Shutdown Docker containers
|
||||
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.
|
||||
# See https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954
|
||||
codecov:
|
||||
# Must run after 'tests' job above
|
||||
needs: tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Download artifacts from previous 'tests' job
|
||||
- name: Download coverage artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
|
||||
# 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.
|
||||
#
|
||||
# Retry action: https://github.com/marketplace/actions/retry-action
|
||||
# Codecov action: https://github.com/codecov/codecov-action
|
||||
- name: Upload coverage to Codecov.io
|
||||
uses: Wandalen/wretry.action@v1.0.36
|
||||
with:
|
||||
action: codecov/codecov-action@v3
|
||||
# Try upload 5 times max
|
||||
attempt_limit: 5
|
||||
# Run again in 30 seconds
|
||||
attempt_delay: 30000
|
||||
|
30
.github/workflows/docker.yml
vendored
30
.github/workflows/docker.yml
vendored
@@ -88,3 +88,33 @@ jobs:
|
||||
# Use tags / labels provided by 'docker/metadata-action' above
|
||||
tags: ${{ steps.meta_build.outputs.tags }}
|
||||
labels: ${{ steps.meta_build.outputs.labels }}
|
||||
|
||||
#####################################################
|
||||
# Build/Push the 'dspace/dspace-angular' image ('-dist' tag)
|
||||
#####################################################
|
||||
# https://github.com/docker/metadata-action
|
||||
# Get Metadata for docker_build_dist step below
|
||||
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular-dist' image
|
||||
id: meta_build_dist
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: dspace/dspace-angular
|
||||
tags: ${{ env.IMAGE_TAGS }}
|
||||
# As this is a "dist" image, its tags are all suffixed with "-dist". Otherwise, it uses the same
|
||||
# tagging logic as the primary 'dspace/dspace-angular' image above.
|
||||
flavor: ${{ env.TAGS_FLAVOR }}
|
||||
suffix=-dist
|
||||
|
||||
- name: Build and push 'dspace-angular-dist' image
|
||||
id: docker_build_dist
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.dist
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
|
||||
# but we ONLY do an image push to DockerHub if it's NOT a PR
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
# Use tags / labels provided by 'docker/metadata-action' above
|
||||
tags: ${{ steps.meta_build_dist.outputs.tags }}
|
||||
labels: ${{ steps.meta_build_dist.outputs.labels }}
|
||||
|
2
.github/workflows/issue_opened.yml
vendored
2
.github/workflows/issue_opened.yml
vendored
@@ -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.3.0
|
||||
uses: actions/add-to-project@v0.5.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
|
||||
|
2
.github/workflows/label_merge_conflicts.yml
vendored
2
.github/workflows/label_merge_conflicts.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
steps:
|
||||
# See: https://github.com/prince-chrismc/label-merge-conflicts-action
|
||||
- name: Auto-label PRs with merge conflicts
|
||||
uses: prince-chrismc/label-merge-conflicts-action@v2
|
||||
uses: prince-chrismc/label-merge-conflicts-action@v3
|
||||
# Add "merge conflict" label if a merge conflict is detected. Remove it when resolved.
|
||||
# Note, the authentication token is created automatically
|
||||
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token
|
||||
|
15
Dockerfile
15
Dockerfile
@@ -2,20 +2,27 @@
|
||||
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
||||
|
||||
FROM node:18-alpine
|
||||
WORKDIR /app
|
||||
ADD . /app/
|
||||
EXPOSE 4000
|
||||
|
||||
# Ensure Python and other build tools are available
|
||||
# These are needed to install some node modules, especially on linux/arm64
|
||||
RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/*
|
||||
|
||||
WORKDIR /app
|
||||
ADD . /app/
|
||||
EXPOSE 4000
|
||||
|
||||
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
|
||||
# See, for example https://github.com/yarnpkg/yarn/issues/5540
|
||||
RUN yarn install --network-timeout 300000
|
||||
|
||||
# When running in dev mode, 4GB of memory is required to build & launch the app.
|
||||
# This default setting can be overridden as needed in your shell, via an env file or in docker-compose.
|
||||
# See Docker environment var precedence: https://docs.docker.com/compose/environment-variables/envvars-precedence/
|
||||
ENV NODE_OPTIONS="--max_old_space_size=4096"
|
||||
|
||||
# On startup, run in DEVELOPMENT mode (this defaults to live reloading enabled, etc).
|
||||
# Listen / accept connections from all IP addresses.
|
||||
# NOTE: At this time it is only possible to run Docker container in Production mode
|
||||
# if you have a public IP. See https://github.com/DSpace/dspace-angular/issues/1485
|
||||
# if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485
|
||||
ENV NODE_ENV development
|
||||
CMD yarn serve --host 0.0.0.0
|
||||
|
31
Dockerfile.dist
Normal file
31
Dockerfile.dist
Normal file
@@ -0,0 +1,31 @@
|
||||
# This image will be published as dspace/dspace-angular:$DSPACE_VERSION-dist
|
||||
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
||||
|
||||
# Test build:
|
||||
# docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist .
|
||||
|
||||
FROM node:18-alpine as build
|
||||
|
||||
# Ensure Python and other build tools are available
|
||||
# These are needed to install some node modules, especially on linux/arm64
|
||||
RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --network-timeout 300000
|
||||
|
||||
ADD . /app/
|
||||
RUN yarn build:prod
|
||||
|
||||
FROM node:18-alpine
|
||||
RUN npm install --global pm2
|
||||
|
||||
COPY --chown=node:node --from=build /app/dist /app/dist
|
||||
COPY --chown=node:node config /app/config
|
||||
COPY --chown=node:node docker/dspace-ui.json /app/dspace-ui.json
|
||||
|
||||
WORKDIR /app
|
||||
USER node
|
||||
ENV NODE_ENV production
|
||||
EXPOSE 4000
|
||||
CMD pm2-runtime start dspace-ui.json --json
|
16
angular.json
16
angular.json
@@ -266,16 +266,26 @@
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
"src/**/*.html",
|
||||
"src/**/*.json5"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "dspace-angular",
|
||||
"cli": {
|
||||
"analytics": false,
|
||||
"defaultCollection": "@angular-eslint/schematics"
|
||||
"schematicCollections": [
|
||||
"@angular-eslint/schematics"
|
||||
]
|
||||
},
|
||||
"schematics": {
|
||||
"@angular-eslint/schematics:application": {
|
||||
"setParserOptionsProject": true
|
||||
},
|
||||
"@angular-eslint/schematics:library": {
|
||||
"setParserOptionsProject": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -187,6 +187,9 @@ languages:
|
||||
- code: gd
|
||||
label: Gàidhlig
|
||||
active: true
|
||||
- code: it
|
||||
label: Italiano
|
||||
active: true
|
||||
- code: lv
|
||||
label: Latviešu
|
||||
active: true
|
||||
@@ -214,6 +217,9 @@ languages:
|
||||
- code: tr
|
||||
label: Türkçe
|
||||
active: true
|
||||
- code: vi
|
||||
label: Tiếng Việt
|
||||
active: true
|
||||
- code: kk
|
||||
label: Қазақ
|
||||
active: true
|
||||
|
44
cypress.config.ts
Normal file
44
cypress.config.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
videosFolder: 'cypress/videos',
|
||||
screenshotsFolder: 'cypress/screenshots',
|
||||
fixturesFolder: 'cypress/fixtures',
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
},
|
||||
env: {
|
||||
// Global constants used in DSpace e2e tests (see also ./cypress/support/e2e.ts)
|
||||
// May be overridden in our cypress.json config file using specified environment variables.
|
||||
// Default values listed here are all valid for the Demo Entities Data set available at
|
||||
// https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||
// (This is the data set used in our CI environment)
|
||||
|
||||
// Admin account used for administrative tests
|
||||
DSPACE_TEST_ADMIN_USER: 'dspacedemo+admin@gmail.com',
|
||||
DSPACE_TEST_ADMIN_PASSWORD: 'dspace',
|
||||
// Community/collection/publication used for view/edit tests
|
||||
DSPACE_TEST_COMMUNITY: '0958c910-2037-42a9-81c7-dca80e3892b4',
|
||||
DSPACE_TEST_COLLECTION: '282164f5-d325-4740-8dd1-fa4d6d3e7200',
|
||||
DSPACE_TEST_ENTITY_PUBLICATION: 'e98b0f27-5c19-49a0-960d-eb6ad5287067',
|
||||
// Search term (should return results) used in search tests
|
||||
DSPACE_TEST_SEARCH_TERM: 'test',
|
||||
// Collection used for submission tests
|
||||
DSPACE_TEST_SUBMIT_COLLECTION_NAME: 'Sample Collection',
|
||||
DSPACE_TEST_SUBMIT_COLLECTION_UUID: '9d8334e9-25d3-4a67-9cea-3dffdef80144',
|
||||
// Account used to test basic submission process
|
||||
DSPACE_TEST_SUBMIT_USER: 'dspacedemo+submit@gmail.com',
|
||||
DSPACE_TEST_SUBMIT_USER_PASSWORD: 'dspace',
|
||||
},
|
||||
e2e: {
|
||||
// Setup our plugins for e2e tests
|
||||
setupNodeEvents(on, config) {
|
||||
return require('./cypress/plugins/index.ts')(on, config);
|
||||
},
|
||||
// This is the base URL that Cypress will run all tests against
|
||||
// It can be overridden via the CYPRESS_BASE_URL environment variable
|
||||
// (By default we set this to a value which should work in most development environments)
|
||||
baseUrl: 'http://localhost:4000',
|
||||
},
|
||||
});
|
25
cypress.json
25
cypress.json
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"integrationFolder": "cypress/integration",
|
||||
"supportFile": "cypress/support/index.ts",
|
||||
"videosFolder": "cypress/videos",
|
||||
"screenshotsFolder": "cypress/screenshots",
|
||||
"pluginsFile": "cypress/plugins/index.ts",
|
||||
"fixturesFolder": "cypress/fixtures",
|
||||
"baseUrl": "http://127.0.0.1:4000",
|
||||
"retries": {
|
||||
"runMode": 2,
|
||||
"openMode": 0
|
||||
},
|
||||
"env": {
|
||||
"DSPACE_TEST_ADMIN_USER": "dspacedemo+admin@gmail.com",
|
||||
"DSPACE_TEST_ADMIN_PASSWORD": "dspace",
|
||||
"DSPACE_TEST_COMMUNITY": "0958c910-2037-42a9-81c7-dca80e3892b4",
|
||||
"DSPACE_TEST_COLLECTION": "282164f5-d325-4740-8dd1-fa4d6d3e7200",
|
||||
"DSPACE_TEST_ENTITY_PUBLICATION": "e98b0f27-5c19-49a0-960d-eb6ad5287067",
|
||||
"DSPACE_TEST_SEARCH_TERM": "test",
|
||||
"DSPACE_TEST_SUBMIT_COLLECTION_NAME": "Sample Collection",
|
||||
"DSPACE_TEST_SUBMIT_COLLECTION_UUID": "9d8334e9-25d3-4a67-9cea-3dffdef80144",
|
||||
"DSPACE_TEST_SUBMIT_USER": "dspacedemo+submit@gmail.com",
|
||||
"DSPACE_TEST_SUBMIT_USER_PASSWORD": "dspace"
|
||||
}
|
||||
}
|
@@ -1,10 +1,10 @@
|
||||
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
||||
import { TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
||||
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/' + TEST_ENTITY_PUBLICATION);
|
||||
cy.visit('/entities/publication/'.concat(TEST_ENTITY_PUBLICATION));
|
||||
|
||||
// Wait for breadcrumbs to be visible
|
||||
cy.get('ds-breadcrumbs').should('be.visible');
|
@@ -1,13 +1,13 @@
|
||||
import { TEST_COLLECTION } from 'cypress/support';
|
||||
import { TEST_COLLECTION } from 'cypress/support/e2e';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Collection Page', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/collections/' + TEST_COLLECTION);
|
||||
cy.visit('/collections/'.concat(TEST_COLLECTION));
|
||||
|
||||
// <ds-collection-page> tag must be loaded
|
||||
cy.get('ds-collection-page').should('exist');
|
||||
cy.get('ds-collection-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-collection-page> for accessibility issues
|
||||
testA11y('ds-collection-page');
|
37
cypress/e2e/collection-statistics.cy.ts
Normal file
37
cypress/e2e/collection-statistics.cy.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_COLLECTION } from 'cypress/support/e2e';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Collection Statistics Page', () => {
|
||||
const COLLECTIONSTATISTICSPAGE = '/statistics/collections/'.concat(TEST_COLLECTION);
|
||||
|
||||
it('should load if you click on "Statistics" from a Collection page', () => {
|
||||
cy.visit('/collections/'.concat(TEST_COLLECTION));
|
||||
cy.get('ds-navbar ds-link-menu-item a[title="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 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(TEST_COLLECTION).concat('_TotalVisitsPerMonth')).should('exist');
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
// 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');
|
||||
});
|
||||
});
|
@@ -7,10 +7,10 @@ describe('Community List Page', () => {
|
||||
cy.visit('/community-list');
|
||||
|
||||
// <ds-community-list-page> tag must be loaded
|
||||
cy.get('ds-community-list-page').should('exist');
|
||||
cy.get('ds-community-list-page').should('be.visible');
|
||||
|
||||
// Open first Community (to show Collections)...that way we scan sub-elements as well
|
||||
cy.get('ds-community-list :nth-child(1) > .btn-group > .btn').click();
|
||||
// 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
|
||||
// Disable heading-order checks until it is fixed
|
@@ -1,13 +1,13 @@
|
||||
import { TEST_COMMUNITY } from 'cypress/support';
|
||||
import { TEST_COMMUNITY } from 'cypress/support/e2e';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Community Page', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/communities/' + TEST_COMMUNITY);
|
||||
cy.visit('/communities/'.concat(TEST_COMMUNITY));
|
||||
|
||||
// <ds-community-page> tag must be loaded
|
||||
cy.get('ds-community-page').should('exist');
|
||||
cy.get('ds-community-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-community-page> for accessibility issues
|
||||
testA11y('ds-community-page',);
|
37
cypress/e2e/community-statistics.cy.ts
Normal file
37
cypress/e2e/community-statistics.cy.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_COMMUNITY } from 'cypress/support/e2e';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Community Statistics Page', () => {
|
||||
const COMMUNITYSTATISTICSPAGE = '/statistics/communities/'.concat(TEST_COMMUNITY);
|
||||
|
||||
it('should load if you click on "Statistics" from a Community page', () => {
|
||||
cy.visit('/communities/'.concat(TEST_COMMUNITY));
|
||||
cy.get('ds-navbar ds-link-menu-item a[title="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 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(TEST_COMMUNITY).concat('_TotalVisitsPerMonth')).should('exist');
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
// 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');
|
||||
});
|
||||
});
|
31
cypress/e2e/homepage-statistics.cy.ts
Normal file
31
cypress/e2e/homepage-statistics.cy.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
import '../support/commands';
|
||||
|
||||
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[title="Statistics"]').click();
|
||||
cy.location('pathname').should('eq', '/statistics');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
// generate 2 view events on an Item's page
|
||||
cy.generateViewEvent(TEST_ENTITY_PUBLICATION, 'item');
|
||||
cy.generateViewEvent(TEST_ENTITY_PUBLICATION, 'item');
|
||||
|
||||
cy.visit('/statistics');
|
||||
|
||||
// <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);
|
||||
|
||||
// Analyze <ds-site-statistics-page> for accessibility issues
|
||||
testA11y('ds-site-statistics-page');
|
||||
});
|
||||
});
|
@@ -1,10 +1,10 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
||||
import { TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Item Page', () => {
|
||||
const ITEMPAGE = '/items/' + TEST_ENTITY_PUBLICATION;
|
||||
const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION;
|
||||
const ITEMPAGE = '/items/'.concat(TEST_ENTITY_PUBLICATION);
|
||||
const ENTITYPAGE = '/entities/publication/'.concat(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', () => {
|
||||
@@ -16,7 +16,7 @@ describe('Item Page', () => {
|
||||
cy.visit(ENTITYPAGE);
|
||||
|
||||
// <ds-item-page> tag must be loaded
|
||||
cy.get('ds-item-page').should('exist');
|
||||
cy.get('ds-item-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-item-page> for accessibility issues
|
||||
// Disable heading-order checks until it is fixed
|
@@ -1,36 +1,41 @@
|
||||
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
||||
import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Item Statistics Page', () => {
|
||||
const ITEMSTATISTICSPAGE = '/statistics/items/' + TEST_ENTITY_PUBLICATION;
|
||||
const ITEMSTATISTICSPAGE = '/statistics/items/'.concat(TEST_ENTITY_PUBLICATION);
|
||||
|
||||
it('should load if you click on "Statistics" from an Item/Entity page', () => {
|
||||
cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION);
|
||||
cy.visit('/entities/publication/'.concat(TEST_ENTITY_PUBLICATION));
|
||||
cy.get('ds-navbar ds-link-menu-item a[title="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('exist');
|
||||
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('.' + TEST_ENTITY_PUBLICATION + '_TotalVisits').should('exist');
|
||||
cy.get('table[data-test="TotalVisits"]').should('be.visible');
|
||||
});
|
||||
|
||||
it('should contain a "Total visits per month" section', () => {
|
||||
cy.visit(ITEMSTATISTICSPAGE);
|
||||
cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisitsPerMonth').should('exist');
|
||||
// Check just for existence because this table is empty in CI environment as it's historical data
|
||||
cy.get('.'.concat(TEST_ENTITY_PUBLICATION).concat('_TotalVisitsPerMonth')).should('exist');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit(ITEMSTATISTICSPAGE);
|
||||
|
||||
// <ds-item-statistics-page> tag must be loaded
|
||||
cy.get('ds-item-statistics-page').should('exist');
|
||||
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);
|
||||
|
||||
// Analyze <ds-item-statistics-page> for accessibility issues
|
||||
testA11y('ds-item-statistics-page');
|
@@ -1,4 +1,4 @@
|
||||
import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
||||
import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
||||
|
||||
const page = {
|
||||
openLoginMenu() {
|
||||
@@ -36,7 +36,7 @@ const page = {
|
||||
|
||||
describe('Login Modal', () => {
|
||||
it('should login when clicking button & stay on same page', () => {
|
||||
const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION;
|
||||
const ENTITYPAGE = '/entities/publication/'.concat(TEST_ENTITY_PUBLICATION);
|
||||
cy.visit(ENTITYPAGE);
|
||||
|
||||
// Login menu should exist
|
@@ -1,5 +1,5 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME } from 'cypress/support';
|
||||
import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME } from 'cypress/support/e2e';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('My DSpace page', () => {
|
||||
@@ -9,7 +9,7 @@ describe('My DSpace page', () => {
|
||||
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
||||
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||
|
||||
cy.get('ds-my-dspace-page').should('exist');
|
||||
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');
|
||||
@@ -42,12 +42,12 @@ describe('My DSpace page', () => {
|
||||
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
||||
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||
|
||||
cy.get('ds-my-dspace-page').should('exist');
|
||||
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('exist');
|
||||
cy.get('ds-object-detail').should('be.visible');
|
||||
|
||||
// Analyze <ds-search-page> for accessibility issues
|
||||
testA11y('ds-my-dspace-page',
|
||||
@@ -80,7 +80,7 @@ describe('My DSpace page', () => {
|
||||
cy.get('ds-authorized-collection-selector input[type="search"]').type(TEST_SUBMIT_COLLECTION_NAME);
|
||||
|
||||
// Click on the button matching that known Collection name
|
||||
cy.get('ds-authorized-collection-selector button[title="' + TEST_SUBMIT_COLLECTION_NAME + '"]').click();
|
||||
cy.get('ds-authorized-collection-selector button[title="'.concat(TEST_SUBMIT_COLLECTION_NAME).concat('"]')).click();
|
||||
|
||||
// New URL should include /workspaceitems, as we've started a new submission
|
||||
cy.url().should('include', '/workspaceitems');
|
@@ -2,7 +2,7 @@ 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('exist');
|
||||
cy.get('ds-pagenotfound').should('be.visible');
|
||||
});
|
||||
|
||||
it('should not contain element ds-pagenotfound when navigating to existing page', () => {
|
@@ -1,4 +1,4 @@
|
||||
import { TEST_SEARCH_TERM } from 'cypress/support';
|
||||
import { TEST_SEARCH_TERM } from 'cypress/support/e2e';
|
||||
|
||||
const page = {
|
||||
fillOutQueryInNavBar(query) {
|
||||
@@ -27,7 +27,7 @@ describe('Search from Navigation Bar', () => {
|
||||
page.fillOutQueryInNavBar(query);
|
||||
page.submitQueryByPressingEnter();
|
||||
// New URL should include query param
|
||||
cy.url().should('include', 'query=' + query);
|
||||
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
|
||||
@@ -42,7 +42,7 @@ describe('Search from Navigation Bar', () => {
|
||||
page.fillOutQueryInNavBar(query);
|
||||
page.submitQueryByPressingEnter();
|
||||
// New URL should include query param
|
||||
cy.url().should('include', 'query=' + query);
|
||||
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
|
||||
@@ -57,7 +57,7 @@ describe('Search from Navigation Bar', () => {
|
||||
page.fillOutQueryInNavBar(query);
|
||||
page.submitQueryByPressingIcon();
|
||||
// New URL should include query param
|
||||
cy.url().should('include', 'query=' + query);
|
||||
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
|
@@ -1,5 +1,5 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { TEST_SEARCH_TERM } from 'cypress/support';
|
||||
import { TEST_SEARCH_TERM } from 'cypress/support/e2e';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Search Page', () => {
|
||||
@@ -13,11 +13,11 @@ describe('Search Page', () => {
|
||||
});
|
||||
|
||||
it('should load results and pass accessibility tests', () => {
|
||||
cy.visit('/search?query=' + TEST_SEARCH_TERM);
|
||||
cy.visit('/search?query='.concat(TEST_SEARCH_TERM));
|
||||
cy.get('[data-test="search-box"]').should('have.value', TEST_SEARCH_TERM);
|
||||
|
||||
// <ds-search-page> tag must be loaded
|
||||
cy.get('ds-search-page').should('exist');
|
||||
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');
|
||||
@@ -45,13 +45,13 @@ describe('Search Page', () => {
|
||||
});
|
||||
|
||||
it('should have a working grid view that passes accessibility tests', () => {
|
||||
cy.visit('/search?query=' + TEST_SEARCH_TERM);
|
||||
cy.visit('/search?query='.concat(TEST_SEARCH_TERM));
|
||||
|
||||
// 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('exist');
|
||||
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');
|
@@ -1,13 +1,11 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID } from 'cypress/support';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID } from 'cypress/support/e2e';
|
||||
|
||||
describe('New Submission page', () => {
|
||||
// NOTE: We already test that new 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=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
||||
cy.visit('/submit?collection='.concat(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(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||
@@ -35,7 +33,7 @@ describe('New Submission page', () => {
|
||||
|
||||
it('should block submission & show errors if required fields are missing', () => {
|
||||
// Create a new submission
|
||||
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
||||
cy.visit('/submit?collection='.concat(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(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||
@@ -95,7 +93,7 @@ describe('New Submission page', () => {
|
||||
|
||||
it('should allow for deposit if all required fields completed & file uploaded', () => {
|
||||
// Create a new submission
|
||||
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
||||
cy.visit('/submit?collection='.concat(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(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||
@@ -124,8 +122,6 @@ describe('New Submission page', () => {
|
||||
|
||||
// Wait for upload to complete before proceeding
|
||||
cy.wait('@upload');
|
||||
// Close the upload success notice
|
||||
cy.get('[data-dismiss="alert"]').click({multiple: true});
|
||||
|
||||
// Wait for deposit button to not be disabled & click it.
|
||||
cy.get('button#deposit').should('not.be.disabled').click();
|
@@ -1,32 +0,0 @@
|
||||
import { TEST_COLLECTION } from 'cypress/support';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Collection Statistics Page', () => {
|
||||
const COLLECTIONSTATISTICSPAGE = '/statistics/collections/' + TEST_COLLECTION;
|
||||
|
||||
it('should load if you click on "Statistics" from a Collection page', () => {
|
||||
cy.visit('/collections/' + TEST_COLLECTION);
|
||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||
cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
|
||||
});
|
||||
|
||||
it('should contain a "Total visits" section', () => {
|
||||
cy.visit(COLLECTIONSTATISTICSPAGE);
|
||||
cy.get('.' + TEST_COLLECTION + '_TotalVisits').should('exist');
|
||||
});
|
||||
|
||||
it('should contain a "Total visits per month" section', () => {
|
||||
cy.visit(COLLECTIONSTATISTICSPAGE);
|
||||
cy.get('.' + TEST_COLLECTION + '_TotalVisitsPerMonth').should('exist');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit(COLLECTIONSTATISTICSPAGE);
|
||||
|
||||
// <ds-collection-statistics-page> tag must be loaded
|
||||
cy.get('ds-collection-statistics-page').should('exist');
|
||||
|
||||
// Analyze <ds-collection-statistics-page> for accessibility issues
|
||||
testA11y('ds-collection-statistics-page');
|
||||
});
|
||||
});
|
@@ -1,32 +0,0 @@
|
||||
import { TEST_COMMUNITY } from 'cypress/support';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Community Statistics Page', () => {
|
||||
const COMMUNITYSTATISTICSPAGE = '/statistics/communities/' + TEST_COMMUNITY;
|
||||
|
||||
it('should load if you click on "Statistics" from a Community page', () => {
|
||||
cy.visit('/communities/' + TEST_COMMUNITY);
|
||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
|
||||
});
|
||||
|
||||
it('should contain a "Total visits" section', () => {
|
||||
cy.visit(COMMUNITYSTATISTICSPAGE);
|
||||
cy.get('.' + TEST_COMMUNITY + '_TotalVisits').should('exist');
|
||||
});
|
||||
|
||||
it('should contain a "Total visits per month" section', () => {
|
||||
cy.visit(COMMUNITYSTATISTICSPAGE);
|
||||
cy.get('.' + TEST_COMMUNITY + '_TotalVisitsPerMonth').should('exist');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit(COMMUNITYSTATISTICSPAGE);
|
||||
|
||||
// <ds-community-statistics-page> tag must be loaded
|
||||
cy.get('ds-community-statistics-page').should('exist');
|
||||
|
||||
// Analyze <ds-community-statistics-page> for accessibility issues
|
||||
testA11y('ds-community-statistics-page');
|
||||
});
|
||||
});
|
@@ -1,19 +0,0 @@
|
||||
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[title="Statistics"]').click();
|
||||
cy.location('pathname').should('eq', '/statistics');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/statistics');
|
||||
|
||||
// <ds-site-statistics-page> tag must be loaded
|
||||
cy.get('ds-site-statistics-page').should('exist');
|
||||
|
||||
// Analyze <ds-site-statistics-page> for accessibility issues
|
||||
testA11y('ds-site-statistics-page');
|
||||
});
|
||||
});
|
@@ -4,12 +4,17 @@
|
||||
// ***********************************************
|
||||
|
||||
import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
|
||||
import { FALLBACK_TEST_REST_BASE_URL } from '.';
|
||||
import { DSPACE_XSRF_COOKIE, XSRF_REQUEST_HEADER } from 'src/app/core/xsrf/xsrf.constants';
|
||||
|
||||
// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL
|
||||
// from the Angular UI's config.json. See 'login()'.
|
||||
export const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server';
|
||||
export const FALLBACK_TEST_REST_DOMAIN = 'localhost';
|
||||
|
||||
// Declare Cypress namespace to help with Intellisense & code completion in IDEs
|
||||
// ALL custom commands MUST be listed here for code completion to work
|
||||
// tslint:disable-next-line:no-namespace
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable<Subject = any> {
|
||||
/**
|
||||
@@ -27,6 +32,15 @@ declare global {
|
||||
* @param password password to login as
|
||||
*/
|
||||
loginViaForm(email: string, password: string): typeof loginViaForm;
|
||||
|
||||
/**
|
||||
* Generate view event for given object. Useful for testing statistics pages with
|
||||
* pre-generated statistics. This just generates a single "hit", but can be called multiple times to
|
||||
* generate multiple hits.
|
||||
* @param uuid UUID of object
|
||||
* @param dsoType type of DSpace Object (e.g. "item", "collection", "community")
|
||||
*/
|
||||
generateViewEvent(uuid: string, dsoType: string): typeof generateViewEvent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,52 +67,57 @@ function login(email: string, password: string): void {
|
||||
if (!config.rest.baseUrl) {
|
||||
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
|
||||
} else {
|
||||
console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: " + config.rest.baseUrl);
|
||||
//console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: ".concat(config.rest.baseUrl));
|
||||
baseRestUrl = config.rest.baseUrl;
|
||||
}
|
||||
|
||||
// To login via REST, first we have to do a GET to obtain a valid CSRF token
|
||||
cy.request( baseRestUrl + '/api/authn/status' )
|
||||
.then((response) => {
|
||||
// We should receive a CSRF token returned in a response header
|
||||
expect(response.headers).to.have.property('dspace-xsrf-token');
|
||||
const csrfToken = response.headers['dspace-xsrf-token'];
|
||||
// Now find domain of our REST API, again with a fallback.
|
||||
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);
|
||||
} else {
|
||||
baseDomain = config.rest.host;
|
||||
}
|
||||
|
||||
// Now, send login POST request including that CSRF token
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: baseRestUrl + '/api/authn/login',
|
||||
headers: { 'X-XSRF-TOKEN' : 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 Token. Set it in the required server-side cookie
|
||||
const csrfToken = 'fakeLoginCSRFToken';
|
||||
cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain });
|
||||
|
||||
// Initialize our AuthTokenInfo object from the authorization header.
|
||||
const authheader = resp.headers.authorization as string;
|
||||
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
|
||||
// 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');
|
||||
|
||||
// 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));
|
||||
});
|
||||
// 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));
|
||||
});
|
||||
|
||||
// Remove cookie with fake CSRF token, as it's no longer needed
|
||||
cy.clearCookie(DSPACE_XSRF_COOKIE);
|
||||
});
|
||||
}
|
||||
// Add as a Cypress command (i.e. assign to 'cy.login')
|
||||
Cypress.Commands.add('login', login);
|
||||
|
||||
|
||||
/**
|
||||
* Login user via displayed login form
|
||||
* @param email email to login as
|
||||
* @param password password to login as
|
||||
*/
|
||||
function loginViaForm(email: string, password: string): void {
|
||||
function loginViaForm(email: string, password: string): void {
|
||||
// Enter email
|
||||
cy.get('ds-log-in [data-test="email"]').type(email);
|
||||
// Enter password
|
||||
@@ -108,3 +127,68 @@ Cypress.Commands.add('login', login);
|
||||
}
|
||||
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
|
||||
Cypress.Commands.add('loginViaForm', loginViaForm);
|
||||
|
||||
|
||||
/**
|
||||
* Generate statistic view event for given object. Useful for testing statistics pages with
|
||||
* pre-generated statistics. This just generates a single "hit", but can be called multiple times to
|
||||
* generate multiple hits.
|
||||
*
|
||||
* NOTE: This requires that "solr-statistics.autoCommit=false" be set on the DSpace backend
|
||||
* (as it is in our docker-compose-ci.yml used in CI).
|
||||
* Otherwise, by default, new statistical events won't be saved until Solr's autocommit next triggers.
|
||||
* @param uuid UUID of object
|
||||
* @param dsoType type of DSpace Object (e.g. "item", "collection", "community")
|
||||
*/
|
||||
function generateViewEvent(uuid: string, dsoType: string): void {
|
||||
// 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.
|
||||
cy.task('readUIConfig').then((str: string) => {
|
||||
// Parse config into a JSON object
|
||||
const config = JSON.parse(str);
|
||||
|
||||
// Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found.
|
||||
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);
|
||||
} else {
|
||||
baseRestUrl = config.rest.baseUrl;
|
||||
}
|
||||
|
||||
// Now find domain of our REST API, again with a fallback.
|
||||
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);
|
||||
} else {
|
||||
baseDomain = config.rest.host;
|
||||
}
|
||||
|
||||
// Create a fake CSRF Token. Set it in the required server-side cookie
|
||||
const csrfToken = 'fakeGenerateViewEventCSRFToken';
|
||||
cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain });
|
||||
|
||||
// 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',
|
||||
},
|
||||
//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);
|
||||
});
|
||||
|
||||
// Remove cookie with fake CSRF token, as it's no longer needed
|
||||
cy.clearCookie(DSPACE_XSRF_COOKIE);
|
||||
});
|
||||
}
|
||||
// Add as a Cypress command (i.e. assign to 'cy.generateViewEvent')
|
||||
Cypress.Commands.add('generateViewEvent', generateViewEvent);
|
||||
|
||||
|
@@ -30,11 +30,11 @@ beforeEach(() => {
|
||||
// For better stability between tests, we visit "about:blank" (i.e. blank page) after each test.
|
||||
// This ensures any remaining/outstanding XHR requests are killed, so they don't affect the next test.
|
||||
// Borrowed from: https://glebbahmutov.com/blog/visit-blank-page-between-tests/
|
||||
afterEach(() => {
|
||||
/*afterEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.location.href = 'about:blank';
|
||||
});
|
||||
});
|
||||
});*/
|
||||
|
||||
|
||||
// Global constants used in tests
|
||||
@@ -43,10 +43,6 @@ afterEach(() => {
|
||||
// https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||
// (This is the data set used in our CI environment)
|
||||
|
||||
// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL
|
||||
// from the Angular UI's config.json. See 'getBaseRESTUrl()' in commands.ts
|
||||
export const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server';
|
||||
|
||||
// Admin account used for administrative tests
|
||||
export const TEST_ADMIN_USER = Cypress.env('DSPACE_TEST_ADMIN_USER') || 'dspacedemo+admin@gmail.com';
|
||||
export const TEST_ADMIN_PASSWORD = Cypress.env('DSPACE_TEST_ADMIN_PASSWORD') || 'dspace';
|
||||
@@ -61,3 +57,10 @@ export const TEST_SUBMIT_COLLECTION_NAME = Cypress.env('DSPACE_TEST_SUBMIT_COLLE
|
||||
export const TEST_SUBMIT_COLLECTION_UUID = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID') || '9d8334e9-25d3-4a67-9cea-3dffdef80144';
|
||||
export const TEST_SUBMIT_USER = Cypress.env('DSPACE_TEST_SUBMIT_USER') || 'dspacedemo+submit@gmail.com';
|
||||
export const TEST_SUBMIT_USER_PASSWORD = Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD') || 'dspace';
|
||||
|
||||
|
||||
// USEFUL REGEX for testing
|
||||
|
||||
// Match any string that contains at least one non-space character
|
||||
// Can be used with "contains()" to determine if an element has a non-empty text value
|
||||
export const REGEX_MATCH_NON_EMPTY_TEXT = /^(?!\s*$).+/;
|
@@ -6,7 +6,20 @@
|
||||
If you wish to run DSpace on Docker in production, we recommend building your own Docker images. You are welcome to borrow ideas/concepts from the below images in doing so. But, the below images should not be used "as is" in any production scenario.
|
||||
***
|
||||
|
||||
## 'Dockerfile' in root directory
|
||||
## Overview
|
||||
The scripts in this directory can be used to start the DSpace User Interface (frontend) in Docker.
|
||||
Optionally, the backend (REST API) might also be started in Docker.
|
||||
|
||||
For additional options/settings in starting the backend (REST API) in Docker, see the Docker Compose
|
||||
documentation for the backend: https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md
|
||||
|
||||
## Root directory
|
||||
|
||||
The root directory of this project contains all the Dockerfiles which may be referenced by
|
||||
the Docker compose scripts in this 'docker' folder.
|
||||
|
||||
### Dockerfile
|
||||
|
||||
This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular'
|
||||
|
||||
```
|
||||
@@ -20,7 +33,18 @@ Admins to our DockerHub repo can manually publish with the following command.
|
||||
docker push dspace/dspace-angular:dspace-7_x
|
||||
```
|
||||
|
||||
## docker directory
|
||||
### Dockerfile.dist
|
||||
|
||||
The `Dockerfile.dist` is used to generate a *production* build and runtime environment.
|
||||
|
||||
```bash
|
||||
# build the latest image
|
||||
docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist .
|
||||
```
|
||||
|
||||
A default/demo version of this image is built *automatically*.
|
||||
|
||||
## 'docker' directory
|
||||
- docker-compose.yml
|
||||
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
|
||||
- docker-compose-rest.yml
|
||||
@@ -45,23 +69,47 @@ docker-compose -f docker/docker-compose.yml build
|
||||
|
||||
## To start DSpace (REST and Angular) from your branch
|
||||
|
||||
This command provides a quick way to start both the frontend & backend from this single codebase
|
||||
```
|
||||
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
|
||||
```
|
||||
|
||||
Keep in mind, you may also start the backend by cloning the 'DSpace/DSpace' GitHub repository separately. See the next section.
|
||||
|
||||
|
||||
## Run DSpace REST and DSpace Angular from local branches.
|
||||
|
||||
This section assumes that you have clones *both* the 'DSpace/DSpace' and 'DSpace/dspace-angular' GitHub
|
||||
repositories. When both are available locally, you can spin up both in Docker and have them work together.
|
||||
|
||||
_The system will be started in 2 steps. Each step shares the same docker network._
|
||||
|
||||
From DSpace/DSpace (build as needed)
|
||||
From 'DSpace/DSpace' clone (build first as needed):
|
||||
```
|
||||
docker-compose -p d7 up -d
|
||||
```
|
||||
|
||||
From DSpace/DSpace-angular
|
||||
NOTE: More detailed instructions on starting the backend via Docker can be found in the [Docker Compose instructions for the Backend](https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md).
|
||||
|
||||
From 'DSpace/dspace-angular' clone (build first as needed)
|
||||
```
|
||||
docker-compose -p d7 -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
At this point, you should be able to access the UI from http://localhost:4000,
|
||||
and the backend at http://localhost:8080/server/
|
||||
|
||||
## Run DSpace Angular dist build with DSpace Demo site backend
|
||||
|
||||
This allows you to run the Angular UI in *production* mode, pointing it at the demo backend
|
||||
(https://api7.dspace.org/server/).
|
||||
|
||||
```
|
||||
docker-compose -f docker/docker-compose-dist.yml pull
|
||||
docker-compose -f docker/docker-compose-dist.yml build
|
||||
docker-compose -p d7 -f docker/docker-compose-dist.yml up -d
|
||||
```
|
||||
|
||||
## Ingest test data from AIPDIR
|
||||
|
||||
Create an administrator
|
||||
@@ -87,9 +135,10 @@ Load assetstore content and trigger a re-index of the repository
|
||||
docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
|
||||
```
|
||||
|
||||
## End to end testing of the rest api (runs in travis).
|
||||
_In this instance, only the REST api runs in Docker using the Entities dataset. Travis will perform CI testing of Angular using Node to drive the tests._
|
||||
## End to end testing of the REST API (runs in GitHub Actions CI).
|
||||
_In this instance, only the REST api runs in Docker using the Entities dataset. GitHub Actions will perform CI testing of Angular using Node to drive the tests. See `.github/workflows/build.yml` for more details._
|
||||
|
||||
This command is only really useful for testing our Continuous Integration process.
|
||||
```
|
||||
docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d
|
||||
docker-compose -p d7ci -f docker/docker-compose-ci.yml up -d
|
||||
```
|
||||
|
@@ -30,6 +30,9 @@ services:
|
||||
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
||||
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
|
||||
solr__P__server: http://dspacesolr:8983/solr
|
||||
# Tell Statistics to commit all views immediately instead of waiting on Solr's autocommit.
|
||||
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
|
||||
solr__D__statistics__P__autoCommit: 'false'
|
||||
depends_on:
|
||||
- dspacedb
|
||||
image: dspace/dspace:dspace-7_x-test
|
||||
|
40
docker/docker-compose-dist.yml
Normal file
40
docker/docker-compose-dist.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
#
|
||||
# The contents of this file are subject to the license and copyright
|
||||
# detailed in the LICENSE and NOTICE files at the root of the source
|
||||
# tree and available online at
|
||||
#
|
||||
# http://www.dspace.org/license/
|
||||
#
|
||||
|
||||
# Docker Compose for running the DSpace Angular UI dist build
|
||||
# for previewing with the DSpace Demo site backend
|
||||
version: '3.7'
|
||||
networks:
|
||||
dspacenet:
|
||||
services:
|
||||
dspace-angular:
|
||||
container_name: dspace-angular
|
||||
environment:
|
||||
DSPACE_UI_SSL: 'false'
|
||||
DSPACE_UI_HOST: dspace-angular
|
||||
DSPACE_UI_PORT: '4000'
|
||||
DSPACE_UI_NAMESPACE: /
|
||||
# NOTE: When running the UI in production mode (which the -dist image does),
|
||||
# these DSPACE_REST_* variables MUST point at a public, HTTPS URL.
|
||||
# This is because Server Side Rendering (SSR) currently requires a public URL,
|
||||
# see this bug: https://github.com/DSpace/dspace-angular/issues/1485
|
||||
DSPACE_REST_SSL: 'true'
|
||||
DSPACE_REST_HOST: api7.dspace.org
|
||||
DSPACE_REST_PORT: 443
|
||||
DSPACE_REST_NAMESPACE: /server
|
||||
image: dspace/dspace-angular:dspace-7_x-dist
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile.dist
|
||||
networks:
|
||||
dspacenet:
|
||||
ports:
|
||||
- published: 4000
|
||||
target: 4000
|
||||
stdin_open: true
|
||||
tty: true
|
@@ -39,7 +39,7 @@ services:
|
||||
# proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests
|
||||
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
||||
proxies__P__trusted__P__ipranges: '172.23.0'
|
||||
image: dspace/dspace:dspace-7_x-test
|
||||
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}"
|
||||
depends_on:
|
||||
- dspacedb
|
||||
networks:
|
||||
@@ -82,8 +82,7 @@ services:
|
||||
# DSpace Solr container
|
||||
dspacesolr:
|
||||
container_name: dspacesolr
|
||||
# Uses official Solr image at https://hub.docker.com/_/solr/
|
||||
image: solr:8.11-slim
|
||||
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}"
|
||||
# Needs main 'dspace' container to start first to guarantee access to solr_configs
|
||||
depends_on:
|
||||
- dspace
|
||||
@@ -96,28 +95,26 @@ services:
|
||||
tty: true
|
||||
working_dir: /var/solr/data
|
||||
volumes:
|
||||
# Mount our "solr_configs" volume available under the Solr's configsets folder (in a 'dspace' subfolder)
|
||||
# This copies the Solr configs from main 'dspace' container into 'dspacesolr' via that volume
|
||||
- solr_configs:/opt/solr/server/solr/configsets/dspace
|
||||
# Keep Solr data directory between reboots
|
||||
- solr_data:/var/solr/data
|
||||
# Initialize all DSpace Solr cores using the mounted local configsets (see above), then start Solr
|
||||
# * First, run precreate-core to create the core (if it doesn't yet exist). If exists already, this is a no-op
|
||||
# * Second, copy updated configs from mounted configsets to this core. If it already existed, this updates core
|
||||
# to the latest configs. If it's a newly created core, this is a no-op.
|
||||
# * Second, copy configsets to this core:
|
||||
# Updates to Solr configs require the container to be rebuilt/restarted:
|
||||
# `docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr`
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- |
|
||||
init-var-solr
|
||||
precreate-core authority /opt/solr/server/solr/configsets/dspace/authority
|
||||
cp -r -u /opt/solr/server/solr/configsets/dspace/authority/* authority
|
||||
precreate-core oai /opt/solr/server/solr/configsets/dspace/oai
|
||||
cp -r -u /opt/solr/server/solr/configsets/dspace/oai/* oai
|
||||
precreate-core search /opt/solr/server/solr/configsets/dspace/search
|
||||
cp -r -u /opt/solr/server/solr/configsets/dspace/search/* search
|
||||
precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics
|
||||
cp -r -u /opt/solr/server/solr/configsets/dspace/statistics/* statistics
|
||||
precreate-core authority /opt/solr/server/solr/configsets/authority
|
||||
cp -r /opt/solr/server/solr/configsets/authority/* authority
|
||||
precreate-core oai /opt/solr/server/solr/configsets/oai
|
||||
cp -r /opt/solr/server/solr/configsets/oai/* oai
|
||||
precreate-core search /opt/solr/server/solr/configsets/search
|
||||
cp -r /opt/solr/server/solr/configsets/search/* search
|
||||
precreate-core statistics /opt/solr/server/solr/configsets/statistics
|
||||
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
|
||||
exec solr -f
|
||||
volumes:
|
||||
assetstore:
|
||||
|
11
docker/dspace-ui.json
Normal file
11
docker/dspace-ui.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"name": "dspace-ui",
|
||||
"cwd": "/app",
|
||||
"script": "dist/server/main.js",
|
||||
"instances": "max",
|
||||
"exec_mode": "cluster"
|
||||
}
|
||||
]
|
||||
}
|
165
package.json
165
package.json
@@ -17,9 +17,9 @@
|
||||
"build:stats": "ng build --stats-json",
|
||||
"build:prod": "yarn run build:ssr",
|
||||
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
|
||||
"test": "ng test --sourceMap=true --watch=false --configuration test",
|
||||
"test:watch": "nodemon --exec \"ng test --sourceMap=true --watch=true --configuration test\"",
|
||||
"test:headless": "ng test --sourceMap=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
|
||||
"test": "ng test --source-map=true --watch=false --configuration test",
|
||||
"test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"",
|
||||
"test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
|
||||
"lint": "ng lint",
|
||||
"lint-fix": "ng lint --fix=true",
|
||||
"e2e": "ng e2e",
|
||||
@@ -55,135 +55,136 @@
|
||||
"ts-node": "10.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "~13.3.12",
|
||||
"@angular/cdk": "^13.2.6",
|
||||
"@angular/common": "~13.3.12",
|
||||
"@angular/compiler": "~13.3.12",
|
||||
"@angular/core": "~13.3.12",
|
||||
"@angular/forms": "~13.3.12",
|
||||
"@angular/localize": "13.3.12",
|
||||
"@angular/platform-browser": "~13.3.12",
|
||||
"@angular/platform-browser-dynamic": "~13.3.12",
|
||||
"@angular/platform-server": "~13.3.12",
|
||||
"@angular/router": "~13.3.12",
|
||||
"@babel/runtime": "7.17.2",
|
||||
"@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",
|
||||
"@babel/runtime": "7.21.0",
|
||||
"@kolkov/ngx-gallery": "^2.0.1",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@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": "^13.0.2",
|
||||
"@ngrx/router-store": "^13.0.2",
|
||||
"@ngrx/store": "^13.0.2",
|
||||
"@nguniversal/express-engine": "^13.0.2",
|
||||
"@ngx-translate/core": "^13.0.0",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^13.0.0",
|
||||
"@ngrx/effects": "^15.4.0",
|
||||
"@ngrx/router-store": "^15.4.0",
|
||||
"@ngrx/store": "^15.4.0",
|
||||
"@nguniversal/express-engine": "^15.2.1",
|
||||
"@ngx-translate/core": "^14.0.0",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
|
||||
"@types/grecaptcha": "^3.0.4",
|
||||
"angular-idle-preload": "3.0.0",
|
||||
"angulartics2": "^12.0.0",
|
||||
"angulartics2": "^12.2.0",
|
||||
"axios": "^0.27.2",
|
||||
"bootstrap": "^4.6.1",
|
||||
"cerialize": "0.1.18",
|
||||
"cli-progress": "^3.8.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
"colors": "^1.4.0",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "1.4.5",
|
||||
"core-js": "^3.7.0",
|
||||
"cookie-parser": "1.4.6",
|
||||
"core-js": "^3.30.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"date-fns-tz": "^1.3.7",
|
||||
"deepmerge": "^4.2.2",
|
||||
"ejs": "^3.1.8",
|
||||
"express": "^4.17.1",
|
||||
"deepmerge": "^4.3.1",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^5.1.3",
|
||||
"fast-json-patch": "^3.0.0-1",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"filesize": "^6.1.0",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"isbot": "^3.6.5",
|
||||
"isbot": "^3.6.10",
|
||||
"js-cookie": "2.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json5": "^2.2.2",
|
||||
"jsonschema": "1.4.0",
|
||||
"json5": "^2.2.3",
|
||||
"jsonschema": "1.4.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"klaro": "^0.7.18",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^7.14.1",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-mathjax3": "^4.3.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": "^13.1.1",
|
||||
"ng-mocks": "^14.10.0",
|
||||
"ng2-file-upload": "1.4.0",
|
||||
"ng2-nouislider": "^1.8.3",
|
||||
"ngx-infinite-scroll": "^10.0.1",
|
||||
"ngx-pagination": "5.0.0",
|
||||
"ngx-infinite-scroll": "^15.0.0",
|
||||
"ngx-pagination": "6.0.3",
|
||||
"ngx-sortablejs": "^11.1.0",
|
||||
"ngx-ui-switch": "^13.0.2",
|
||||
"ngx-ui-switch": "^14.0.3",
|
||||
"nouislider": "^14.6.3",
|
||||
"pem": "1.14.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"pem": "1.14.7",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.5.5",
|
||||
"sanitize-html": "^2.7.2",
|
||||
"sortablejs": "1.13.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"sanitize-html": "^2.10.0",
|
||||
"sortablejs": "1.15.0",
|
||||
"uuid": "^8.3.2",
|
||||
"webfontloader": "1.6.28",
|
||||
"zone.js": "~0.11.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "~13.1.0",
|
||||
"@angular-devkit/build-angular": "~13.3.10",
|
||||
"@angular-eslint/builder": "13.1.0",
|
||||
"@angular-eslint/eslint-plugin": "13.1.0",
|
||||
"@angular-eslint/eslint-plugin-template": "13.1.0",
|
||||
"@angular-eslint/schematics": "13.1.0",
|
||||
"@angular-eslint/template-parser": "13.1.0",
|
||||
"@angular/cli": "~13.3.10",
|
||||
"@angular/compiler-cli": "~13.3.12",
|
||||
"@angular/language-service": "~13.3.12",
|
||||
"@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": "^15.2.6",
|
||||
"@angular/compiler-cli": "^15.2.8",
|
||||
"@angular/language-service": "^15.2.8",
|
||||
"@cypress/schematic": "^1.5.0",
|
||||
"@fortawesome/fontawesome-free": "^6.2.1",
|
||||
"@ngrx/store-devtools": "^13.0.2",
|
||||
"@ngtools/webpack": "^13.2.6",
|
||||
"@nguniversal/builders": "^13.1.1",
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"@ngrx/store-devtools": "^15.4.0",
|
||||
"@ngtools/webpack": "^15.2.6",
|
||||
"@nguniversal/builders": "^15.2.1",
|
||||
"@types/deep-freeze": "0.1.2",
|
||||
"@types/ejs": "^3.1.1",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/ejs": "^3.1.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/js-cookie": "2.2.6",
|
||||
"@types/lodash": "^4.14.165",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^14.14.9",
|
||||
"@types/sanitize-html": "^2.6.2",
|
||||
"@typescript-eslint/eslint-plugin": "5.11.0",
|
||||
"@typescript-eslint/parser": "5.11.0",
|
||||
"axe-core": "^4.4.3",
|
||||
"@types/sanitize-html": "^2.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||
"@typescript-eslint/parser": "^5.59.1",
|
||||
"axe-core": "^4.7.0",
|
||||
"compression-webpack-plugin": "^9.2.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "9.7.0",
|
||||
"cypress-axe": "^0.14.0",
|
||||
"cypress": "12.10.0",
|
||||
"cypress-axe": "^1.4.0",
|
||||
"deep-freeze": "0.0.1",
|
||||
"eslint": "^8.2.0",
|
||||
"eslint-plugin-deprecation": "^1.3.2",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-plugin-deprecation": "^1.4.1",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsdoc": "^39.6.4",
|
||||
"eslint-plugin-jsonc": "^2.6.0",
|
||||
"eslint-plugin-lodash": "^7.4.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"express-static-gzip": "^2.1.5",
|
||||
"express-static-gzip": "^2.1.7",
|
||||
"jasmine-core": "^3.8.0",
|
||||
"jasmine-marbles": "0.9.2",
|
||||
"karma": "^6.3.14",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma": "^6.4.2",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.3",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"ngx-mask": "^13.1.7",
|
||||
"nodemon": "^2.0.20",
|
||||
"postcss": "^8.1",
|
||||
"nodemon": "^2.0.22",
|
||||
"postcss": "^8.4",
|
||||
"postcss-apply": "0.12.0",
|
||||
"postcss-import": "^14.0.0",
|
||||
"postcss-loader": "^4.0.3",
|
||||
@@ -193,14 +194,14 @@
|
||||
"react-dom": "^16.14.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs-spy": "^8.0.2",
|
||||
"sass": "~1.33.0",
|
||||
"sass": "~1.62.0",
|
||||
"sass-loader": "^12.6.0",
|
||||
"sass-resources-loader": "^2.1.1",
|
||||
"sass-resources-loader": "^2.2.5",
|
||||
"ts-node": "^8.10.2",
|
||||
"typescript": "~4.5.5",
|
||||
"webpack": "^5.69.1",
|
||||
"webpack-bundle-analyzer": "^4.4.0",
|
||||
"typescript": "~4.8.4",
|
||||
"webpack": "5.76.1",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
"webpack-cli": "^4.2.0",
|
||||
"webpack-dev-server": "^4.5.0"
|
||||
"webpack-dev-server": "^4.13.3"
|
||||
}
|
||||
}
|
||||
|
@@ -68,18 +68,18 @@
|
||||
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
|
||||
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}">
|
||||
<td>{{epersonDto.eperson.id}}</td>
|
||||
<td>{{epersonDto.eperson.name}}</td>
|
||||
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
||||
<td>{{epersonDto.eperson.email}}</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="toggleEditEPerson(epersonDto.eperson)"
|
||||
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: {name: epersonDto.eperson.name} }}">
|
||||
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
|
||||
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: {name: epersonDto.eperson.name} }}">
|
||||
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { UntypedFormBuilder } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
|
||||
@@ -21,6 +21,7 @@ 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';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-epeople-registry',
|
||||
@@ -89,11 +90,13 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private formBuilder: FormBuilder,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private router: Router,
|
||||
private modalService: NgbModal,
|
||||
private paginationService: PaginationService,
|
||||
public requestService: RequestService) {
|
||||
public requestService: RequestService,
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
this.currentSearchQuery = '';
|
||||
this.currentSearchScope = 'metadata';
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
@@ -121,7 +124,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
this.subs.push(this.ePeople$.pipe(
|
||||
switchMap((epeople: PaginatedList<EPerson>) => {
|
||||
if (epeople.pageInfo.totalElements > 0) {
|
||||
return combineLatest(...epeople.page.map((eperson) => {
|
||||
return combineLatest([...epeople.page.map((eperson: EPerson) => {
|
||||
return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe(
|
||||
map((authorized) => {
|
||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||
@@ -130,7 +133,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
return epersonDtoModel;
|
||||
})
|
||||
);
|
||||
})).pipe(map((dtos: EpersonDtoModel[]) => {
|
||||
})]).pipe(map((dtos: EpersonDtoModel[]) => {
|
||||
return buildPaginatedList(epeople.pageInfo, dtos);
|
||||
}));
|
||||
} else {
|
||||
@@ -237,7 +240,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: ePerson.name}));
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: this.dsoNameService.getName(ePerson)}));
|
||||
} else {
|
||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
||||
}
|
||||
|
@@ -13,12 +13,13 @@
|
||||
[formGroup]="formGroup"
|
||||
[formLayout]="formLayout"
|
||||
[displayCancel]="false"
|
||||
[submitLabel]="submitLabel"
|
||||
(submitForm)="onSubmit()">
|
||||
<div before class="btn-group">
|
||||
<button (click)="onCancel()"
|
||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||
</div>
|
||||
<div between class="btn-group">
|
||||
<div *ngIf="displayResetPassword" between class="btn-group">
|
||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" (click)="resetPassword()">
|
||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||
</button>
|
||||
@@ -64,9 +65,13 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (groups | async)?.payload?.page">
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle"><a (click)="groupsDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
||||
<td class="align-middle">{{(group.object | async)?.payload?.name}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="groupsDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">
|
||||
{{ dsoNameService.getName(group) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">{{ dsoNameService.getName(undefined) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -2,7 +2,7 @@ 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 { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { BrowserModule, By } from '@angular/platform-browser';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
@@ -116,9 +116,9 @@ describe('EPersonFormComponent', () => {
|
||||
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 FormControl(controlState, controlOptions);
|
||||
controls[model.id] = new UntypedFormControl(controlState, controlOptions);
|
||||
});
|
||||
return new FormGroup(controls, options);
|
||||
return new UntypedFormGroup(controls, options);
|
||||
},
|
||||
createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) {
|
||||
return {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import {
|
||||
DynamicCheckboxModel,
|
||||
DynamicFormControlModel,
|
||||
@@ -37,6 +37,7 @@ 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';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-eperson-form',
|
||||
@@ -108,7 +109,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* A FormGroup that combines all inputs
|
||||
*/
|
||||
formGroup: FormGroup;
|
||||
formGroup: UntypedFormGroup;
|
||||
|
||||
/**
|
||||
* An EventEmitter that's fired whenever the form is being submitted
|
||||
@@ -165,6 +166,15 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
isImpersonated = false;
|
||||
|
||||
/**
|
||||
* A boolean that indicate if to display EPersonForm's Rest password button
|
||||
*/
|
||||
displayResetPassword = false;
|
||||
|
||||
/**
|
||||
* A string that indicate the label of Submit button
|
||||
*/
|
||||
submitLabel = 'form.create';
|
||||
/**
|
||||
* Subscription to email field value change
|
||||
*/
|
||||
@@ -183,11 +193,14 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
private paginationService: PaginationService,
|
||||
public requestService: RequestService,
|
||||
private epersonRegistrationService: EpersonRegistrationService,
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
this.epersonInitial = eperson;
|
||||
if (hasValue(eperson)) {
|
||||
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
||||
this.displayResetPassword = true;
|
||||
this.submitLabel = 'form.submit';
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -201,14 +214,14 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
initialisePage() {
|
||||
|
||||
observableCombineLatest(
|
||||
observableCombineLatest([
|
||||
this.translateService.get(`${this.messagePrefix}.firstName`),
|
||||
this.translateService.get(`${this.messagePrefix}.lastName`),
|
||||
this.translateService.get(`${this.messagePrefix}.email`),
|
||||
this.translateService.get(`${this.messagePrefix}.canLogIn`),
|
||||
this.translateService.get(`${this.messagePrefix}.requireCertificate`),
|
||||
this.translateService.get(`${this.messagePrefix}.emailHint`),
|
||||
).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => {
|
||||
]).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => {
|
||||
this.firstName = new DynamicInputModel({
|
||||
id: 'firstName',
|
||||
label: firstName,
|
||||
@@ -375,10 +388,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((rd: RemoteData<EPerson>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: ePersonToCreate.name }));
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: this.dsoNameService.getName(ePersonToCreate) }));
|
||||
this.submitForm.emit(ePersonToCreate);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: ePersonToCreate.name }));
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: this.dsoNameService.getName(ePersonToCreate) }));
|
||||
this.cancelForm.emit();
|
||||
}
|
||||
});
|
||||
@@ -414,10 +427,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
const response = this.epersonService.updateEPerson(editedEperson);
|
||||
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<EPerson>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name }));
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: this.dsoNameService.getName(editedEperson) }));
|
||||
this.submitForm.emit(editedEperson);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: editedEperson.name }));
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: this.dsoNameService.getName(editedEperson) }));
|
||||
this.cancelForm.emit();
|
||||
}
|
||||
});
|
||||
@@ -465,7 +478,7 @@ export class EPersonFormComponent 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: eperson.name }));
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) }));
|
||||
this.submitForm.emit();
|
||||
} else {
|
||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
||||
@@ -543,7 +556,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
.subscribe((list: PaginatedList<EPerson>) => {
|
||||
if (list.totalElements > 0) {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.' + notificationSection + '.failure.emailInUse', {
|
||||
name: ePerson.name,
|
||||
name: this.dsoNameService.getName(ePerson),
|
||||
email: ePerson.email
|
||||
}));
|
||||
}
|
||||
|
@@ -26,7 +26,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"
|
||||
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: (getLinkedDSO(groupBeingEdited) | async)?.payload?.name, comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })">
|
||||
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName((getLinkedDSO(groupBeingEdited) | async)?.payload), comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })">
|
||||
</ds-alert>
|
||||
|
||||
<ds-form [formId]="formId"
|
||||
|
@@ -2,7 +2,7 @@ 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 { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { BrowserModule, By } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
@@ -36,6 +36,8 @@ import { NotificationsServiceStub } from '../../../shared/testing/notifications-
|
||||
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 { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
describe('GroupFormComponent', () => {
|
||||
let component: GroupFormComponent;
|
||||
@@ -130,9 +132,9 @@ describe('GroupFormComponent', () => {
|
||||
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 FormControl(controlState, controlOptions);
|
||||
controls[model.id] = new UntypedFormControl(controlState, controlOptions);
|
||||
});
|
||||
return new FormGroup(controls, options);
|
||||
return new UntypedFormGroup(controls, options);
|
||||
},
|
||||
createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) {
|
||||
return {
|
||||
@@ -188,7 +190,7 @@ describe('GroupFormComponent', () => {
|
||||
translateService = getMockTranslateService();
|
||||
router = new RouterMock();
|
||||
notificationService = new NotificationsServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
@@ -198,7 +200,8 @@ describe('GroupFormComponent', () => {
|
||||
}),
|
||||
],
|
||||
declarations: [GroupFormComponent],
|
||||
providers: [GroupFormComponent,
|
||||
providers: [
|
||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
||||
{ provide: DSpaceObjectDataService, useValue: dsoDataServiceStub },
|
||||
@@ -240,8 +243,8 @@ describe('GroupFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a new group using the correct values', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
it('should emit a new group using the correct values', (async () => {
|
||||
await fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
}));
|
||||
@@ -303,8 +306,8 @@ describe('GroupFormComponent', () => {
|
||||
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||
});
|
||||
|
||||
it('should emit the existing group using the correct new values', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
it('should emit the existing group using the correct new values', (async () => {
|
||||
await fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
||||
});
|
||||
}));
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
@@ -46,6 +46,7 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { ValidateGroupExists } from './validators/group-exists.validator';
|
||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
@@ -95,7 +96,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* A FormGroup that combines all inputs
|
||||
*/
|
||||
formGroup: FormGroup;
|
||||
formGroup: UntypedFormGroup;
|
||||
|
||||
/**
|
||||
* An EventEmitter that's fired whenever the form is being submitted
|
||||
@@ -134,7 +135,8 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
groupNameValueChangeSubscribe: Subscription;
|
||||
|
||||
|
||||
constructor(public groupDataService: GroupDataService,
|
||||
constructor(
|
||||
public groupDataService: GroupDataService,
|
||||
private ePersonDataService: EPersonDataService,
|
||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
@@ -145,7 +147,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private modalService: NgbModal,
|
||||
public requestService: RequestService,
|
||||
protected changeDetectorRef: ChangeDetectorRef) {
|
||||
protected changeDetectorRef: ChangeDetectorRef,
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -331,7 +335,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
.subscribe((list: PaginatedList<Group>) => {
|
||||
if (list.totalElements > 0) {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.' + notificationSection + '.failure.groupNameInUse', {
|
||||
name: group.name
|
||||
name: this.dsoNameService.getName(group),
|
||||
}));
|
||||
}
|
||||
}));
|
||||
@@ -364,10 +368,10 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((rd: RemoteData<Group>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.edited.success', { name: rd.payload.name }));
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.edited.success', { name: this.dsoNameService.getName(rd.payload) }));
|
||||
this.submitForm.emit(rd.payload);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.edited.failure', { name: group.name }));
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.edited.failure', { name: this.dsoNameService.getName(group) }));
|
||||
this.cancelForm.emit();
|
||||
}
|
||||
});
|
||||
@@ -427,11 +431,11 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
this.groupDataService.delete(group.id).pipe(getFirstCompletedRemoteData())
|
||||
.subscribe((rd: RemoteData<NoContent>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: group.name }));
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: this.dsoNameService.getName(group) }));
|
||||
this.onCancel();
|
||||
} else {
|
||||
this.notificationsService.error(
|
||||
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: group.name }),
|
||||
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: this.dsoNameService.getName(group) }),
|
||||
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.content', { cause: rd.errorMessage }));
|
||||
}
|
||||
});
|
||||
|
@@ -57,8 +57,12 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let ePerson of (ePeopleSearchDtos | async)?.page">
|
||||
<td class="align-middle">{{ePerson.eperson.id}}</td>
|
||||
<td class="align-middle"><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td>
|
||||
<td class="align-middle">
|
||||
<a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
|
||||
{{ dsoNameService.getName(ePerson.eperson) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
|
||||
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
|
||||
@@ -69,7 +73,7 @@
|
||||
(click)="deleteMemberFromGroup(ePerson)"
|
||||
[disabled]="actionConfig.remove.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}">
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||
</button>
|
||||
|
||||
@@ -77,7 +81,7 @@
|
||||
(click)="addMemberToGroup(ePerson)"
|
||||
[disabled]="actionConfig.add.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.eperson.name} }}">
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
||||
<i [ngClass]="actionConfig.add.icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -117,8 +121,12 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let ePerson of (ePeopleMembersOfGroupDtos | async)?.page">
|
||||
<td class="align-middle">{{ePerson.eperson.id}}</td>
|
||||
<td class="align-middle"><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td>
|
||||
<td class="align-middle">
|
||||
<a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
|
||||
{{ dsoNameService.getName(ePerson.eperson) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
|
||||
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
|
||||
@@ -129,14 +137,14 @@
|
||||
(click)="deleteMemberFromGroup(ePerson)"
|
||||
[disabled]="actionConfig.remove.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}">
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||
</button>
|
||||
<button *ngIf="!ePerson.memberOfGroup"
|
||||
(click)="addMemberToGroup(ePerson)"
|
||||
[disabled]="actionConfig.add.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.eperson.name} }}">
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
||||
<i [ngClass]="actionConfig.add.icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -28,6 +28,8 @@ import { NotificationsServiceStub } from '../../../../shared/testing/notificatio
|
||||
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 { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
describe('MembersListComponent', () => {
|
||||
let component: MembersListComponent;
|
||||
@@ -118,7 +120,7 @@ describe('MembersListComponent', () => {
|
||||
translateService = getMockTranslateService();
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
@@ -135,6 +137,7 @@ describe('MembersListComponent', () => {
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
{ provide: Router, useValue: new RouterMock() },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { UntypedFormBuilder } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
@@ -27,6 +27,7 @@ import { NotificationsService } from '../../../../shared/notifications/notificat
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
/**
|
||||
* Keys to keep track of specific subscriptions
|
||||
@@ -141,9 +142,10 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
public ePersonDataService: EPersonDataService,
|
||||
protected translateService: TranslateService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected formBuilder: FormBuilder,
|
||||
protected formBuilder: UntypedFormBuilder,
|
||||
protected paginationService: PaginationService,
|
||||
private router: Router
|
||||
protected router: Router,
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
this.currentSearchQuery = '';
|
||||
this.currentSearchScope = 'metadata';
|
||||
@@ -253,7 +255,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
||||
this.showNotifications('deleteMember', response, ePerson.eperson.name, activeGroup);
|
||||
this.showNotifications('deleteMember', response, this.dsoNameService.getName(ePerson.eperson), activeGroup);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||
}
|
||||
@@ -269,7 +271,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson.eperson);
|
||||
this.showNotifications('addMember', response, ePerson.eperson.name, activeGroup);
|
||||
this.showNotifications('addMember', response, this.dsoNameService.getName(ePerson.eperson), activeGroup);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||
}
|
||||
|
@@ -53,15 +53,19 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (searchResults$ | async)?.payload?.page">
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle"><a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
||||
<td class="align-middle">{{(group.object | async)?.payload?.name}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
|
||||
{{ dsoNameService.getName(group) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload) }}</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button *ngIf="(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
||||
(click)="deleteSubgroupFromGroup(group)"
|
||||
class="btn btn-outline-danger btn-sm deleteButton"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: group.name} }}">
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
|
||||
@@ -70,7 +74,7 @@
|
||||
<button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
||||
(click)="addSubgroupToGroup(group)"
|
||||
class="btn btn-outline-primary btn-sm addButton"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: group.name} }}">
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(group) } }}">
|
||||
<i class="fas fa-plus fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -108,14 +112,18 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page">
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle"><a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
||||
<td class="align-middle">{{(group.object | async)?.payload?.name}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
|
||||
{{ dsoNameService.getName(group) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload)}}</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="deleteSubgroupFromGroup(group)"
|
||||
class="btn btn-outline-danger btn-sm deleteButton"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: group.name} }}">
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -29,6 +29,8 @@ import { NotificationsServiceStub } from '../../../../shared/testing/notificatio
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
describe('SubgroupsListComponent', () => {
|
||||
let component: SubgroupsListComponent;
|
||||
@@ -108,6 +110,7 @@ describe('SubgroupsListComponent', () => {
|
||||
],
|
||||
declarations: [SubgroupsListComponent],
|
||||
providers: [SubgroupsListComponent,
|
||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { UntypedFormBuilder } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
@@ -18,6 +18,7 @@ import { PaginationComponentOptions } from '../../../../shared/pagination/pagina
|
||||
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
|
||||
@@ -86,9 +87,11 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
constructor(public groupDataService: GroupDataService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private paginationService: PaginationService,
|
||||
private router: Router) {
|
||||
private router: Router,
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
this.currentSearchQuery = '';
|
||||
}
|
||||
|
||||
@@ -177,7 +180,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup);
|
||||
this.showNotifications('deleteSubgroup', response, subgroup.name, activeGroup);
|
||||
this.showNotifications('deleteSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||
}
|
||||
@@ -193,7 +196,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
if (activeGroup != null) {
|
||||
if (activeGroup.uuid !== subgroup.uuid) {
|
||||
const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup);
|
||||
this.showNotifications('addSubgroup', response, subgroup.name, activeGroup);
|
||||
this.showNotifications('addSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.subgroupToAddIsActiveGroup'));
|
||||
}
|
||||
|
@@ -56,8 +56,8 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let groupDto of (groupsDto$ | async)?.page">
|
||||
<td>{{groupDto.group.id}}</td>
|
||||
<td>{{groupDto.group.name}}</td>
|
||||
<td>{{(groupDto.group.object | async)?.payload?.name}}</td>
|
||||
<td>{{ dsoNameService.getName(groupDto.group) }}</td>
|
||||
<td>{{ dsoNameService.getName((groupDto.group.object | async)?.payload) }}</td>
|
||||
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
@@ -65,7 +65,7 @@
|
||||
<button *ngSwitchCase="true"
|
||||
[routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
|
||||
class="btn btn-outline-primary btn-sm btn-edit"
|
||||
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: groupDto.group.name} }}"
|
||||
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: dsoNameService.getName(groupDto.group) } }}"
|
||||
>
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
@@ -80,7 +80,7 @@
|
||||
</ng-container>
|
||||
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
|
||||
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm btn-delete"
|
||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">
|
||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: dsoNameService.getName(groupDto.group) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -32,8 +32,10 @@ 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';
|
||||
|
||||
describe('GroupRegistryComponent', () => {
|
||||
describe('GroupsRegistryComponent', () => {
|
||||
let component: GroupsRegistryComponent;
|
||||
let fixture: ComponentFixture<GroupsRegistryComponent>;
|
||||
let ePersonDataServiceStub: any;
|
||||
@@ -160,7 +162,7 @@ describe('GroupRegistryComponent', () => {
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
|
||||
setIsAuthorized(true, true);
|
||||
paginationService = new PaginationServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
@@ -171,6 +173,7 @@ describe('GroupRegistryComponent', () => {
|
||||
],
|
||||
declarations: [GroupsRegistryComponent],
|
||||
providers: [GroupsRegistryComponent,
|
||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
||||
{ provide: DSpaceObjectDataService, useValue: dsoDataServiceStub },
|
||||
@@ -208,7 +211,7 @@ describe('GroupRegistryComponent', () => {
|
||||
it('should display community/collection name if present', () => {
|
||||
const collectionNamesFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(3)'));
|
||||
expect(collectionNamesFound.length).toEqual(2);
|
||||
expect(collectionNamesFound[0].nativeElement.textContent).toEqual('');
|
||||
expect(collectionNamesFound[0].nativeElement.textContent).toEqual(UNDEFINED_NAME);
|
||||
expect(collectionNamesFound[1].nativeElement.textContent).toEqual('testgroupid2objectName');
|
||||
});
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { UntypedFormBuilder } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
@@ -37,6 +37,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
|
||||
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',
|
||||
@@ -99,12 +100,14 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
protected routeService: RouteService,
|
||||
private router: Router,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private paginationService: PaginationService,
|
||||
public requestService: RequestService) {
|
||||
public requestService: RequestService,
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
this.currentSearchQuery = '';
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
query: this.currentSearchQuery,
|
||||
@@ -201,10 +204,10 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
.subscribe((rd: RemoteData<NoContent>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id];
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.group.name }));
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(group.group) }));
|
||||
} else {
|
||||
this.notificationsService.error(
|
||||
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.group.name }),
|
||||
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 }));
|
||||
}
|
||||
});
|
||||
|
@@ -97,9 +97,15 @@ export class BatchImportPageComponent {
|
||||
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
|
||||
}
|
||||
} else {
|
||||
const title = this.translate.get('process.new.notification.error.title');
|
||||
const content = this.translate.get('process.new.notification.error.content');
|
||||
this.notificationsService.error(title, content);
|
||||
if (rd.statusCode === 413) {
|
||||
const title = this.translate.get('process.new.notification.error.title');
|
||||
const content = this.translate.get('process.new.notification.error.max-upload.content');
|
||||
this.notificationsService.error(title, content);
|
||||
} else {
|
||||
const title = this.translate.get('process.new.notification.error.title');
|
||||
const content = this.translate.get('process.new.notification.error.content');
|
||||
this.notificationsService.error(title, content);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import {
|
||||
DynamicFormLayout,
|
||||
DynamicInputModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
@@ -66,7 +66,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* A FormGroup that combines all inputs
|
||||
*/
|
||||
formGroup: FormGroup;
|
||||
formGroup: UntypedFormGroup;
|
||||
|
||||
/**
|
||||
* An EventEmitter that's fired whenever the form is being submitted
|
||||
|
@@ -5,7 +5,7 @@ import {
|
||||
DynamicFormLayout,
|
||||
DynamicInputModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
@@ -82,7 +82,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* A FormGroup that combines all inputs
|
||||
*/
|
||||
formGroup: FormGroup;
|
||||
formGroup: UntypedFormGroup;
|
||||
|
||||
/**
|
||||
* An EventEmitter that's fired whenever the form is being submitted
|
||||
|
@@ -19,7 +19,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-
|
||||
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||
import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service';
|
||||
import { AccessStatusObject } from '../../../../../shared/object-list/access-status-badge/access-status.model';
|
||||
import { AccessStatusObject } from '../../../../../shared/object-collection/shared/badges/access-status-badge/access-status.model';
|
||||
import { AuthService } from '../../../../../core/auth/auth.service';
|
||||
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
|
||||
import { FileService } from '../../../../../core/shared/file.service';
|
||||
|
@@ -13,6 +13,7 @@ import { BitstreamDataService } from '../../../../../core/data/bitstream-data.se
|
||||
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
|
||||
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch)
|
||||
@Component({
|
||||
@@ -28,12 +29,14 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE
|
||||
@ViewChild('badges', { static: true }) badges: ElementRef;
|
||||
@ViewChild('buttons', { static: true }) buttons: ElementRef;
|
||||
|
||||
constructor(protected truncatableService: TruncatableService,
|
||||
protected bitstreamDataService: BitstreamDataService,
|
||||
private themeService: ThemeService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver
|
||||
constructor(
|
||||
public dsoNameService: DSONameService,
|
||||
protected truncatableService: TruncatableService,
|
||||
protected bitstreamDataService: BitstreamDataService,
|
||||
private themeService: ThemeService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
) {
|
||||
super(truncatableService, bitstreamDataService);
|
||||
super(dsoNameService, truncatableService, bitstreamDataService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -2,6 +2,5 @@
|
||||
[viewMode]="viewModes.ListElement"
|
||||
[index]="index"
|
||||
[linkType]="linkType"
|
||||
[listID]="listID"
|
||||
[hideBadges]="true"></ds-listable-object-component-loader>
|
||||
[listID]="listID"></ds-listable-object-component-loader>
|
||||
<ds-item-admin-search-result-actions-element [item]="dso" [small]="false"></ds-item-admin-search-result-actions-element>
|
||||
|
@@ -82,7 +82,7 @@ export class SupervisionOrderGroupSelectorComponent {
|
||||
getFirstCompletedRemoteData(),
|
||||
).subscribe((rd: RemoteData<SupervisionOrder>) => {
|
||||
if (rd.state === 'Success') {
|
||||
this.notificationsService.success(this.translateService.get('supervision-group-selector.notification.create.success.title', { name: this.selectedGroup.name }));
|
||||
this.notificationsService.success(this.translateService.get('supervision-group-selector.notification.create.success.title', { name: this.dsoNameService.getName(this.selectedGroup) }));
|
||||
this.create.emit(rd.payload);
|
||||
this.close();
|
||||
} else {
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<a class="badge badge-primary mr-1 mb-1 text-capitalize mw-100 text-truncate" *ngFor="let supervisionOrder of supervisionOrders" data-test="soBadge"
|
||||
[ngbTooltip]="'workflow-item.search.result.list.element.supervised.remove-tooltip' | translate"
|
||||
(click)="$event.preventDefault(); $event.stopImmediatePropagation(); deleteSupervisionOrder(supervisionOrder)" aria-label="Close">
|
||||
{{supervisionOrder.group.name}}
|
||||
{{ dsoNameService.getName(supervisionOrder.group) }}
|
||||
<span aria-hidden="true"> ×</span>
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -8,6 +8,7 @@ import { Group } from '../../../../../../core/eperson/models/group.model';
|
||||
import { getFirstCompletedRemoteData } from '../../../../../../core/shared/operators';
|
||||
import { isNotEmpty } from '../../../../../../shared/empty.util';
|
||||
import { RemoteData } from '../../../../../../core/data/remote-data';
|
||||
import { DSONameService } from '../../../../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
export interface SupervisionOrderListEntry {
|
||||
supervisionOrder: SupervisionOrder;
|
||||
@@ -33,6 +34,11 @@ export class SupervisionOrderStatusComponent implements OnChanges {
|
||||
|
||||
@Output() delete: EventEmitter<SupervisionOrderListEntry> = new EventEmitter<SupervisionOrderListEntry>();
|
||||
|
||||
constructor(
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes && changes.supervisionOrderList) {
|
||||
this.getSupervisionOrderEntries(changes.supervisionOrderList.currentValue)
|
||||
|
@@ -11,7 +11,7 @@ import { URLCombiner } from '../../../../../core/url-combiner/url-combiner';
|
||||
import { WorkspaceItemAdminWorkflowActionsComponent } from './workspace-item-admin-workflow-actions.component';
|
||||
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||
import {
|
||||
getWorkflowItemDeleteRoute,
|
||||
getWorkspaceItemDeleteRoute,
|
||||
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||
@@ -83,7 +83,7 @@ describe('WorkspaceItemAdminWorkflowActionsComponent', () => {
|
||||
it('should render a delete button with the correct link', () => {
|
||||
const button = fixture.debugElement.query(By.css('a.delete-link'));
|
||||
const link = button.nativeElement.href;
|
||||
expect(link).toContain(new URLCombiner(getWorkflowItemDeleteRoute(wsi.id)).toString());
|
||||
expect(link).toContain(new URLCombiner(getWorkspaceItemDeleteRoute(wsi.id)).toString());
|
||||
});
|
||||
|
||||
it('should render a policies button with the correct link', () => {
|
||||
|
@@ -11,7 +11,7 @@ import {
|
||||
SupervisionOrderGroupSelectorComponent
|
||||
} from './supervision-order-group-selector/supervision-order-group-selector.component';
|
||||
import {
|
||||
getWorkflowItemDeleteRoute
|
||||
getWorkspaceItemDeleteRoute
|
||||
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||
import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../../../item-page/edit-item-page/edit-item-page.routing-paths';
|
||||
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||
@@ -105,10 +105,10 @@ export class WorkspaceItemAdminWorkflowActionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the delete page of this workflow item
|
||||
* Returns the path to the delete page of this workspace item
|
||||
*/
|
||||
getDeleteRoute(): string {
|
||||
return getWorkflowItemDeleteRoute(this.wsi.id);
|
||||
return getWorkspaceItemDeleteRoute(this.wsi.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -23,6 +23,7 @@ import {
|
||||
import { take } from 'rxjs/operators';
|
||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch)
|
||||
@Component({
|
||||
@@ -55,13 +56,14 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S
|
||||
public item$: Observable<Item>;
|
||||
|
||||
constructor(
|
||||
public dsoNameService: DSONameService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private linkService: LinkService,
|
||||
protected truncatableService: TruncatableService,
|
||||
private themeService: ThemeService,
|
||||
protected bitstreamDataService: BitstreamDataService
|
||||
) {
|
||||
super(truncatableService, bitstreamDataService);
|
||||
super(dsoNameService, truncatableService, bitstreamDataService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core';
|
||||
import { Component, ComponentFactoryResolver, ElementRef, ViewChild, OnInit } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { map, mergeMap, take, tap } from 'rxjs/operators';
|
||||
@@ -36,6 +36,7 @@ import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
|
||||
import { SupervisionOrder } from '../../../../../core/supervision-order/models/supervision-order.model';
|
||||
import { PaginatedList } from '../../../../../core/data/paginated-list.model';
|
||||
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@listableObjectComponent(WorkspaceItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch)
|
||||
@Component({
|
||||
@@ -46,7 +47,7 @@ import { SupervisionOrderDataService } from '../../../../../core/supervision-ord
|
||||
/**
|
||||
* The component for displaying a grid element for an workflow item on the admin workflow search page
|
||||
*/
|
||||
export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent<WorkspaceItemSearchResult, WorkspaceItem> {
|
||||
export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent<WorkspaceItemSearchResult, WorkspaceItem> implements OnInit {
|
||||
|
||||
/**
|
||||
* The item linked to the workspace item
|
||||
@@ -79,6 +80,7 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends
|
||||
@ViewChild('buttons', { static: true }) buttons: ElementRef;
|
||||
|
||||
constructor(
|
||||
public dsoNameService: DSONameService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private linkService: LinkService,
|
||||
protected truncatableService: TruncatableService,
|
||||
@@ -86,7 +88,7 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends
|
||||
protected bitstreamDataService: BitstreamDataService,
|
||||
protected supervisionOrderDataService: SupervisionOrderDataService,
|
||||
) {
|
||||
super(truncatableService, bitstreamDataService);
|
||||
super(dsoNameService, truncatableService, bitstreamDataService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -39,7 +39,7 @@ export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends S
|
||||
|
||||
constructor(private linkService: LinkService,
|
||||
protected truncatableService: TruncatableService,
|
||||
protected dsoNameService: DSONameService,
|
||||
public dsoNameService: DSONameService,
|
||||
@Inject(APP_CONFIG) protected appConfig: AppConfig
|
||||
) {
|
||||
super(truncatableService, dsoNameService, appConfig);
|
||||
|
@@ -59,7 +59,7 @@ export class WorkspaceItemSearchResultAdminWorkflowListElementComponent extends
|
||||
public supervisionOrder$: BehaviorSubject<SupervisionOrder[]> = new BehaviorSubject<SupervisionOrder[]>([]);
|
||||
|
||||
constructor(private linkService: LinkService,
|
||||
protected dsoNameService: DSONameService,
|
||||
public dsoNameService: DSONameService,
|
||||
protected supervisionOrderDataService: SupervisionOrderDataService,
|
||||
protected truncatableService: TruncatableService,
|
||||
@Inject(APP_CONFIG) protected appConfig: AppConfig
|
||||
|
@@ -209,7 +209,7 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
|
||||
{
|
||||
path: REQUEST_COPY_MODULE_PATH,
|
||||
loadChildren: () => import('./request-copy/request-copy.module').then((m) => m.RequestCopyModule),
|
||||
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
|
||||
canActivate: [EndUserAgreementCurrentUserGuard]
|
||||
},
|
||||
{
|
||||
path: FORBIDDEN_PATH,
|
||||
@@ -218,7 +218,8 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
|
||||
{
|
||||
path: 'statistics',
|
||||
loadChildren: () => import('./statistics-page/statistics-page-routing.module')
|
||||
.then((m) => m.StatisticsPageRoutingModule)
|
||||
.then((m) => m.StatisticsPageRoutingModule),
|
||||
canActivate: [EndUserAgreementCurrentUserGuard],
|
||||
},
|
||||
{
|
||||
path: HEALTH_PAGE_PATH,
|
||||
@@ -228,7 +229,7 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
|
||||
{
|
||||
path: ACCESS_CONTROL_MODULE_PATH,
|
||||
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
|
||||
canActivate: [GroupAdministratorGuard],
|
||||
canActivate: [GroupAdministratorGuard, EndUserAgreementCurrentUserGuard],
|
||||
},
|
||||
{
|
||||
path: 'subscriptions',
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="container">
|
||||
<h3>{{'bitstream.download.page' | translate:{bitstream: (bitstream$ | async)?.name} }}</h3>
|
||||
<h3>{{'bitstream.download.page' | translate:{ bitstream: dsoNameService.getName((bitstream$ | async)) } }}</h3>
|
||||
<div class="pt-3">
|
||||
<button (click)="back()" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {{'bitstream.download.page.back' | translate}}
|
||||
|
@@ -14,6 +14,7 @@ import { getForbiddenRoute } from '../../app-routing-paths';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
||||
import { Location } from '@angular/common';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-bitstream-download-page',
|
||||
@@ -36,6 +37,7 @@ export class BitstreamDownloadPageComponent implements OnInit {
|
||||
private fileService: FileService,
|
||||
private hardRedirectService: HardRedirectService,
|
||||
private location: Location,
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
|
||||
}
|
||||
|
@@ -2,13 +2,13 @@
|
||||
<div class="container" *ngVar="(bitstreamFormatsRD$ | async) as formatsRD">
|
||||
<div class="row" *ngIf="bitstreamRD?.hasSucceeded && formatsRD?.hasSucceeded">
|
||||
<div class="col-md-2">
|
||||
<ds-thumbnail [thumbnail]="bitstreamRD?.payload"></ds-thumbnail>
|
||||
<ds-themed-thumbnail [thumbnail]="bitstreamRD?.payload"></ds-themed-thumbnail>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h3>{{bitstreamRD?.payload?.name}} <span class="text-muted">({{bitstreamRD?.payload?.sizeBytes | dsFileSize}})</span></h3>
|
||||
<h3>{{dsoNameService.getName(bitstreamRD?.payload)}} <span class="text-muted">({{bitstreamRD?.payload?.sizeBytes | dsFileSize}})</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -15,7 +15,7 @@ import { INotification, Notification } from '../../shared/notifications/models/n
|
||||
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
||||
import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
||||
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
@@ -84,9 +84,9 @@ describe('EditBitstreamPageComponent', () => {
|
||||
const controls = {};
|
||||
if (hasValue(fModel)) {
|
||||
fModel.forEach((controlModel) => {
|
||||
controls[controlModel.id] = new FormControl((controlModel as any).value);
|
||||
controls[controlModel.id] = new UntypedFormControl((controlModel as any).value);
|
||||
});
|
||||
return new FormGroup(controls);
|
||||
return new UntypedFormGroup(controls);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ import {
|
||||
DynamicInputModel,
|
||||
DynamicSelectModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
@@ -359,7 +359,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* The form group of this form
|
||||
*/
|
||||
formGroup: FormGroup;
|
||||
formGroup: UntypedFormGroup;
|
||||
|
||||
/**
|
||||
* The ID of the item the bitstream originates from
|
||||
@@ -395,7 +395,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
private formService: DynamicFormService,
|
||||
private translate: TranslateService,
|
||||
private bitstreamService: BitstreamDataService,
|
||||
private dsoNameService: DSONameService,
|
||||
public dsoNameService: DSONameService,
|
||||
private notificationsService: NotificationsService,
|
||||
private bitstreamFormatService: BitstreamFormatDataService) {
|
||||
}
|
||||
@@ -618,7 +618,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
// TODO: Set bitstream to primary when supported
|
||||
const primary = rawForm.fileNamePrimaryContainer.primaryBitstream;
|
||||
Metadata.setFirstValue(newMetadata, 'dc.title', rawForm.fileNamePrimaryContainer.fileName);
|
||||
Metadata.setFirstValue(newMetadata, 'dc.description', rawForm.descriptionContainer.description);
|
||||
if (isEmpty(rawForm.descriptionContainer.description)) {
|
||||
delete newMetadata['dc.description'];
|
||||
} else {
|
||||
Metadata.setFirstValue(newMetadata, 'dc.description', rawForm.descriptionContainer.description);
|
||||
}
|
||||
if (this.isIIIF) {
|
||||
// It's helpful to remove these metadata elements entirely when the form value is empty.
|
||||
// This avoids potential issues on the REST side and makes it possible to do things like
|
||||
|
@@ -22,6 +22,7 @@ import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
import { APP_CONFIG } from '../../../config/app-config.interface';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
||||
|
||||
describe('BrowseByDatePageComponent', () => {
|
||||
let comp: BrowseByDatePageComponent;
|
||||
@@ -49,12 +50,22 @@ describe('BrowseByDatePageComponent', () => {
|
||||
]
|
||||
}
|
||||
});
|
||||
const lastItem = Object.assign(new Item(), {
|
||||
id: 'last-item-id',
|
||||
metadata: {
|
||||
'dc.date.issued': [
|
||||
{
|
||||
value: '1960-01-01'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const mockBrowseService = {
|
||||
getBrowseEntriesFor: (options: BrowseEntrySearchOptions) => toRemoteData([]),
|
||||
getBrowseItemsFor: (value: string, options: BrowseEntrySearchOptions) => toRemoteData([firstItem]),
|
||||
getFirstItemFor: () => createSuccessfulRemoteDataObject$(firstItem)
|
||||
};
|
||||
const mockBrowseService = {
|
||||
getBrowseEntriesFor: (options: BrowseEntrySearchOptions) => toRemoteData([]),
|
||||
getBrowseItemsFor: (value: string, options: BrowseEntrySearchOptions) => toRemoteData([firstItem]),
|
||||
getFirstItemFor: (definition: string, scope?: string, sortDirection?: SortDirection) => null
|
||||
};
|
||||
|
||||
const mockDsoService = {
|
||||
findById: () => createSuccessfulRemoteDataObject$(mockCommunity)
|
||||
@@ -91,9 +102,14 @@ describe('BrowseByDatePageComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BrowseByDatePageComponent);
|
||||
const browseService = fixture.debugElement.injector.get(BrowseService);
|
||||
spyOn(browseService, 'getFirstItemFor')
|
||||
// ok to expect the default browse as first param since we just need the mock items obtained via sort direction.
|
||||
.withArgs('author', undefined, SortDirection.ASC).and.returnValue(createSuccessfulRemoteDataObject$(firstItem))
|
||||
.withArgs('author', undefined, SortDirection.DESC).and.returnValue(createSuccessfulRemoteDataObject$(lastItem));
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
route = (comp as any).route;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should initialize the list of items', () => {
|
||||
@@ -107,6 +123,7 @@ describe('BrowseByDatePageComponent', () => {
|
||||
});
|
||||
|
||||
it('should create a list of startsWith options with the current year first', () => {
|
||||
expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear());
|
||||
//expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear());
|
||||
expect(comp.startsWithOptions[0]).toEqual(1960);
|
||||
});
|
||||
});
|
||||
|
@@ -1,11 +1,10 @@
|
||||
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
|
||||
import {
|
||||
BrowseByMetadataPageComponent,
|
||||
browseParamsToOptions, getBrowseSearchOptions
|
||||
browseParamsToOptions,
|
||||
getBrowseSearchOptions
|
||||
} from '../browse-by-metadata-page/browse-by-metadata-page.component';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
@@ -16,7 +15,10 @@ import { map } from 'rxjs/operators';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { isValidDate } from '../../shared/date.util';
|
||||
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
|
||||
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-date-page',
|
||||
@@ -41,8 +43,10 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
protected router: Router,
|
||||
protected paginationService: PaginationService,
|
||||
protected cdRef: ChangeDetectorRef,
|
||||
@Inject(APP_CONFIG) public appConfig: AppConfig) {
|
||||
super(route, browseService, dsoService, paginationService, router, appConfig);
|
||||
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -72,30 +76,24 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
|
||||
/**
|
||||
* Update the StartsWith options
|
||||
* In this implementation, it creates a list of years starting from now, going all the way back to the earliest
|
||||
* date found on an item within this scope. The further back in time, the bigger the change in years become to avoid
|
||||
* extremely long lists with a one-year difference.
|
||||
* In this implementation, it creates a list of years starting from the most recent item or the current year, going
|
||||
* all the way back to the earliest date found on an item within this scope. The further back in time, the bigger
|
||||
* the change in years become to avoid extremely long lists with a one-year difference.
|
||||
* To determine the change in years, the config found under GlobalConfig.BrowseBy is used for this.
|
||||
* @param definition The metadata definition to fetch the first item for
|
||||
* @param metadataKeys The metadata fields to fetch the earliest date from (expects a date field)
|
||||
* @param scope The scope under which to fetch the earliest item for
|
||||
*/
|
||||
updateStartsWithOptions(definition: string, metadataKeys: string[], scope?: string) {
|
||||
const firstItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.ASC);
|
||||
const lastItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.DESC);
|
||||
this.subs.push(
|
||||
this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData<Item>) => {
|
||||
let lowerLimit = this.appConfig.browseBy.defaultLowerLimit;
|
||||
if (hasValue(firstItemRD.payload)) {
|
||||
const date = firstItemRD.payload.firstMetadataValue(metadataKeys);
|
||||
if (isNotEmpty(date) && isValidDate(date)) {
|
||||
const dateObj = new Date(date);
|
||||
// TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC.
|
||||
lowerLimit = isNaN(dateObj.getUTCFullYear()) ? lowerLimit : dateObj.getUTCFullYear();
|
||||
}
|
||||
}
|
||||
observableCombineLatest([firstItemRD, lastItemRD]).subscribe(([firstItem, lastItem]) => {
|
||||
let lowerLimit = this.getLimit(firstItem, metadataKeys, this.appConfig.browseBy.defaultLowerLimit);
|
||||
let upperLimit = this.getLimit(lastItem, metadataKeys, new Date().getUTCFullYear());
|
||||
const options = [];
|
||||
const currentYear = new Date().getUTCFullYear();
|
||||
const oneYearBreak = Math.floor((currentYear - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
|
||||
const fiveYearBreak = Math.floor((currentYear - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
|
||||
const oneYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
|
||||
const fiveYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
|
||||
if (lowerLimit <= fiveYearBreak) {
|
||||
lowerLimit -= 10;
|
||||
} else if (lowerLimit <= oneYearBreak) {
|
||||
@@ -103,7 +101,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
} else {
|
||||
lowerLimit -= 1;
|
||||
}
|
||||
let i = currentYear;
|
||||
let i = upperLimit;
|
||||
while (i > lowerLimit) {
|
||||
options.push(i);
|
||||
if (i <= fiveYearBreak) {
|
||||
@@ -121,4 +119,24 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the year from the item metadata field or the limit.
|
||||
* @param itemRD the item remote data
|
||||
* @param metadataKeys The metadata fields to fetch the earliest date from (expects a date field)
|
||||
* @param limit the limit to use if the year can't be found in metadata
|
||||
* @private
|
||||
*/
|
||||
private getLimit(itemRD: RemoteData<Item>, metadataKeys: string[], limit: number): number {
|
||||
if (hasValue(itemRD.payload)) {
|
||||
const date = itemRD.payload.firstMetadataValue(metadataKeys);
|
||||
if (isNotEmpty(date) && isValidDate(date)) {
|
||||
const dateObj = new Date(date);
|
||||
// TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC.
|
||||
return isNaN(dateObj.getUTCFullYear()) ? limit : dateObj.getUTCFullYear();
|
||||
} else {
|
||||
return new Date().getUTCFullYear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||
import { BrowseDefinition } from '../core/shared/browse-definition.model';
|
||||
import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator';
|
||||
import { DSONameServiceMock } from '../shared/mocks/dso-name.service.mock';
|
||||
import { DSONameService } from '../core/breadcrumbs/dso-name.service';
|
||||
|
||||
describe('BrowseByGuard', () => {
|
||||
describe('canActivate', () => {
|
||||
@@ -33,7 +35,7 @@ describe('BrowseByGuard', () => {
|
||||
findById: () => createSuccessfulRemoteDataObject$(browseDefinition)
|
||||
};
|
||||
|
||||
guard = new BrowseByGuard(dsoService, translateService, browseDefinitionService);
|
||||
guard = new BrowseByGuard(dsoService, translateService, browseDefinitionService, new DSONameServiceMock() as DSONameService);
|
||||
});
|
||||
|
||||
it('should return true, and sets up the data correctly, with a scope and value', () => {
|
||||
|
@@ -3,11 +3,13 @@ import { Injectable } from '@angular/core';
|
||||
import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service';
|
||||
import { hasNoValue, hasValue } from '../shared/empty.util';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service';
|
||||
import { BrowseDefinition } from '../core/shared/browse-definition.model';
|
||||
import { DSONameService } from '../core/breadcrumbs/dso-name.service';
|
||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||
|
||||
@Injectable()
|
||||
/**
|
||||
@@ -17,7 +19,9 @@ export class BrowseByGuard implements CanActivate {
|
||||
|
||||
constructor(protected dsoService: DSpaceObjectDataService,
|
||||
protected translate: TranslateService,
|
||||
protected browseDefinitionService: BrowseDefinitionDataService) {
|
||||
protected browseDefinitionService: BrowseDefinitionDataService,
|
||||
protected dsoNameService: DSONameService,
|
||||
) {
|
||||
}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
@@ -31,14 +35,14 @@ export class BrowseByGuard implements CanActivate {
|
||||
}
|
||||
const scope = route.queryParams.scope;
|
||||
const value = route.queryParams.value;
|
||||
const metadataTranslated = this.translate.instant('browse.metadata.' + id);
|
||||
const metadataTranslated = this.translate.instant(`browse.metadata.${id}`);
|
||||
return browseDefinition$.pipe(
|
||||
switchMap((browseDefinition) => {
|
||||
switchMap((browseDefinition: BrowseDefinition) => {
|
||||
if (hasValue(scope)) {
|
||||
const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteData());
|
||||
return dsoAndMetadata$.pipe(
|
||||
map((dsoRD) => {
|
||||
const name = dsoRD.payload.name;
|
||||
const dso$: Observable<DSpaceObject> = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteDataPayload());
|
||||
return dso$.pipe(
|
||||
map((dso: DSpaceObject) => {
|
||||
const name = this.dsoNameService.getName(dso);
|
||||
route.data = this.createData(title, id, browseDefinition, name, metadataTranslated, value, route);
|
||||
return true;
|
||||
})
|
||||
|
@@ -5,7 +5,7 @@
|
||||
|
||||
<header class="comcol-header mr-auto">
|
||||
<!-- Parent Name -->
|
||||
<ds-comcol-page-header [name]="parentContext.name">
|
||||
<ds-comcol-page-header [name]="dsoNameService.getName(parentContext)">
|
||||
</ds-comcol-page-header>
|
||||
<!-- Collection logo -->
|
||||
<ds-comcol-page-logo *ngIf="logo$"
|
||||
@@ -35,12 +35,12 @@
|
||||
<ds-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
|
||||
title="{{'browse.title' | translate:
|
||||
{
|
||||
collection: (parent$ | async)?.payload?.name || '',
|
||||
collection: dsoNameService.getName((parent$ | async)?.payload),
|
||||
field: 'browse.metadata.' + browseId | translate,
|
||||
startsWith: (startsWith)? ('browse.startsWith' | translate: { startsWith: '"' + startsWith + '"' }) : '',
|
||||
value: (value)? '"' + value + '"': ''
|
||||
} }}"
|
||||
parentname="{{(parent$ | async)?.payload?.name || ''}}"
|
||||
parentname="{{dsoNameService.getName((parent$ | async)?.payload)}}"
|
||||
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
|
||||
[paginationConfig]="(currentPagination$ |async)"
|
||||
[sortConfig]="(currentSort$ |async)"
|
||||
|
@@ -21,6 +21,7 @@ import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
export const BBM_PAGINATION_ID = 'bbm';
|
||||
|
||||
@@ -126,7 +127,9 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
||||
protected dsoService: DSpaceObjectDataService,
|
||||
protected paginationService: PaginationService,
|
||||
protected router: Router,
|
||||
@Inject(APP_CONFIG) public appConfig: AppConfig) {
|
||||
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
|
||||
this.fetchThumbnails = this.appConfig.browseBy.showThumbnails;
|
||||
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
||||
@@ -151,8 +154,17 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.authority = params.authority;
|
||||
this.value = +params.value || params.value || '';
|
||||
this.startsWith = +params.startsWith || params.startsWith;
|
||||
|
||||
if (typeof params.value === 'string'){
|
||||
this.value = params.value.trim();
|
||||
} else {
|
||||
this.value = '';
|
||||
}
|
||||
|
||||
if (typeof params.startsWith === 'string'){
|
||||
this.startsWith = params.startsWith.trim();
|
||||
}
|
||||
|
||||
if (isNotEmpty(this.value)) {
|
||||
this.updatePageWithItems(
|
||||
browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), this.value, this.authority);
|
||||
@@ -305,7 +317,7 @@ export function browseParamsToOptions(params: any,
|
||||
metadata,
|
||||
paginationConfig,
|
||||
sortConfig,
|
||||
+params.startsWith || params.startsWith,
|
||||
params.startsWith,
|
||||
params.scope,
|
||||
fetchThumbnail
|
||||
);
|
||||
|
@@ -13,6 +13,7 @@ import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-title-page',
|
||||
@@ -29,8 +30,10 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
||||
protected dsoService: DSpaceObjectDataService,
|
||||
protected paginationService: PaginationService,
|
||||
protected router: Router,
|
||||
@Inject(APP_CONFIG) public appConfig: AppConfig) {
|
||||
super(route, browseService, dsoService, paginationService, router, appConfig);
|
||||
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@@ -28,14 +28,14 @@
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row mt-2">
|
||||
<div class="col-12 col-lg-6">
|
||||
<ds-search-form id="search-form"
|
||||
<ds-themed-search-form id="search-form"
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="'./'"
|
||||
[inPlaceSearch]="true"
|
||||
[searchPlaceholder]="'collection.edit.item-mapper.search-form.placeholder' | translate"
|
||||
(submitSearch)="performedSearch = true">
|
||||
</ds-search-form>
|
||||
</ds-themed-search-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -157,7 +157,7 @@ export class CollectionItemMapperComponent implements OnInit {
|
||||
scope: undefined,
|
||||
dsoTypes: [DSpaceObjectType.ITEM],
|
||||
sort: this.defaultSortOptions
|
||||
}), 10000).pipe(
|
||||
}), 10000, undefined, undefined, followLink('owningCollection')).pipe(
|
||||
toDSpaceObjectListRD(),
|
||||
startWith(undefined)
|
||||
);
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<header class="comcol-header mr-auto">
|
||||
<!-- Collection Name -->
|
||||
<ds-comcol-page-header
|
||||
[name]="collection.name">
|
||||
[name]="dsoNameService.getName(collection)">
|
||||
</ds-comcol-page-header>
|
||||
<!-- Collection logo -->
|
||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, OnInit, Inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs';
|
||||
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
|
||||
@@ -29,6 +29,8 @@ import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||
import { getCollectionPageRoute } from './collection-page-routing-paths';
|
||||
import { redirectOn4xx } from '../core/shared/authorized.operators';
|
||||
import { BROWSE_LINKS_TO_FOLLOW } from '../core/browse/browse.service';
|
||||
import { DSONameService } from '../core/breadcrumbs/dso-name.service';
|
||||
import { APP_CONFIG, AppConfig } from '../../../src/config/app-config.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-collection-page',
|
||||
@@ -69,13 +71,16 @@ export class CollectionPageComponent implements OnInit {
|
||||
private authService: AuthService,
|
||||
private paginationService: PaginationService,
|
||||
private authorizationDataService: AuthorizationDataService,
|
||||
public dsoNameService: DSONameService,
|
||||
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||
) {
|
||||
this.paginationConfig = new PaginationComponentOptions();
|
||||
this.paginationConfig.id = 'cp';
|
||||
this.paginationConfig.pageSize = 5;
|
||||
this.paginationConfig.currentPage = 1;
|
||||
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
|
||||
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'cp',
|
||||
currentPage: 1,
|
||||
pageSize: this.appConfig.browseBy.pageSize,
|
||||
});
|
||||
|
||||
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="sub-header" class="border-bottom pb-2">{{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}</h2>
|
||||
<h2 id="sub-header" class="border-bottom pb-2">{{'collection.create.sub-head' | translate:{ parent: dsoNameService.getName((parentRD$| async)?.payload) } }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<ds-collection-form (submitForm)="onSubmit($event)"
|
||||
|
@@ -13,16 +13,19 @@ import { CreateCollectionPageComponent } from './create-collection-page.componen
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
describe('CreateCollectionPageComponent', () => {
|
||||
let comp: CreateCollectionPageComponent;
|
||||
let fixture: ComponentFixture<CreateCollectionPageComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [CreateCollectionPageComponent],
|
||||
providers: [
|
||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||
{ provide: CollectionDataService, useValue: {} },
|
||||
{
|
||||
provide: CommunityDataService,
|
||||
|
@@ -8,6 +8,7 @@ import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can create a new Collection
|
||||
@@ -22,6 +23,7 @@ export class CreateCollectionPageComponent extends CreateComColPageComponent<Col
|
||||
protected type = Collection.type;
|
||||
|
||||
public constructor(
|
||||
public dsoNameService: DSONameService,
|
||||
protected communityDataService: CommunityDataService,
|
||||
protected collectionDataService: CollectionDataService,
|
||||
protected routeService: RouteService,
|
||||
@@ -30,6 +32,6 @@ export class CreateCollectionPageComponent extends CreateComColPageComponent<Col
|
||||
protected translate: TranslateService,
|
||||
protected requestService: RequestService
|
||||
) {
|
||||
super(collectionDataService, communityDataService, routeService, router, notificationsService, translate, requestService);
|
||||
super(collectionDataService, dsoNameService, communityDataService, routeService, router, notificationsService, translate, requestService);
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'collection.delete.head' | translate}}</h2>
|
||||
<p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||
<p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p>
|
||||
<div class="form-group row">
|
||||
<div class="col text-right space-children-mr">
|
||||
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)">
|
||||
|
@@ -10,6 +10,8 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page.component';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
describe('DeleteCollectionPageComponent', () => {
|
||||
let comp: DeleteCollectionPageComponent;
|
||||
@@ -20,6 +22,7 @@ describe('DeleteCollectionPageComponent', () => {
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [DeleteCollectionPageComponent],
|
||||
providers: [
|
||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||
{ provide: CollectionDataService, useValue: {} },
|
||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||
{ provide: NotificationsService, useValue: {} },
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user