Merge remote-tracking branch 'upstream/main' into embargo-date

This commit is contained in:
Mark H. Wood
2023-05-25 14:57:25 -04:00
682 changed files with 55915 additions and 37329 deletions

View File

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

View File

@@ -15,3 +15,6 @@ trim_trailing_whitespace = false
[*.ts]
quote_type = single
[*.json5]
ij_json_keep_blank_lines_in_code = 3

View File

@@ -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
}
]
}
}
]
}

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ jobs:
# Only add to project board if issue is flagged as "needs triage" or has no labels
# NOTE: By default we flag new issues as "needs triage" in our issue template
if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '')
uses: actions/add-to-project@v0.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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
}
}

View File

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

View File

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

View 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');
});
});

View File

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

View File

@@ -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',);

View 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');
});
});

View 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');
});
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -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', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,22 +67,27 @@ 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;
}
// Create a fake CSRF Token. Set it in the required server-side cookie
const csrfToken = 'fakeLoginCSRFToken';
cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain });
// Now, send login POST request including that CSRF token
cy.request({
method: 'POST',
url: baseRestUrl + '/api/authn/login',
headers: { 'X-XSRF-TOKEN' : csrfToken},
headers: { [XSRF_REQUEST_HEADER]: csrfToken},
form: true, // indicates the body should be form urlencoded
body: { user: email, password: password }
}).then((resp) => {
@@ -85,14 +104,14 @@ function login(email: string, password: string): void {
// 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
@@ -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);

View File

@@ -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*$).+/;

View File

@@ -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
```

View File

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

View 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

View File

@@ -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
View File

@@ -0,0 +1,11 @@
{
"apps": [
{
"name": "dspace-ui",
"cwd": "/app",
"script": "dist/server/main.js",
"instances": "max",
"exec_mode": "cluster"
}
]
}

View File

@@ -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"
}
}

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

@@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 },

View File

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

View File

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

View File

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

View File

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

View File

@@ -96,11 +96,17 @@ export class BatchImportPageComponent {
if (isNotEmpty(rd.payload)) {
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
}
} else {
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);
}
}
});
}
}

View File

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

View File

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

View File

@@ -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';

View File

@@ -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,
constructor(
public dsoNameService: DSONameService,
protected truncatableService: TruncatableService,
protected bitstreamDataService: BitstreamDataService,
private themeService: ThemeService,
private componentFactoryResolver: ComponentFactoryResolver
private componentFactoryResolver: ComponentFactoryResolver,
) {
super(truncatableService, bitstreamDataService);
super(dsoNameService, truncatableService, bitstreamDataService);
}
/**

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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',

View File

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

View File

@@ -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,
) {
}

View File

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

View File

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

View File

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

View File

@@ -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,11 +50,21 @@ 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)
getFirstItemFor: (definition: string, scope?: string, sortDirection?: SortDirection) => null
};
const mockDsoService = {
@@ -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);
});
});

View File

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

View File

@@ -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', () => {

View File

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

View File

@@ -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: '&quot;' + startsWith + '&quot;' }) : '',
value: (value)? '&quot;' + value + '&quot;': ''
} }}"
parentname="{{(parent$ | async)?.payload?.name || ''}}"
parentname="{{dsoNameService.getName((parent$ | async)?.payload)}}"
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
[paginationConfig]="(currentPagination$ |async)"
[sortConfig]="(currentSort$ |async)"

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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$"

View File

@@ -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 {

View File

@@ -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)"

View File

@@ -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,

View File

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

View File

@@ -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)">

View File

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