mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-15 14:03:06 +00:00
Merge branch 'main' into w2p-97184_theme-feedback_contribute-main
This commit is contained in:
@@ -15,3 +15,6 @@ trim_trailing_whitespace = false
|
|||||||
|
|
||||||
[*.ts]
|
[*.ts]
|
||||||
quote_type = single
|
quote_type = single
|
||||||
|
|
||||||
|
[*.json5]
|
||||||
|
ij_json_keep_blank_lines_in_code = 3
|
||||||
|
@@ -7,7 +7,8 @@
|
|||||||
"eslint-plugin-jsdoc",
|
"eslint-plugin-jsdoc",
|
||||||
"eslint-plugin-deprecation",
|
"eslint-plugin-deprecation",
|
||||||
"unused-imports",
|
"unused-imports",
|
||||||
"eslint-plugin-lodash"
|
"eslint-plugin-lodash",
|
||||||
|
"eslint-plugin-jsonc"
|
||||||
],
|
],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
@@ -224,6 +225,42 @@
|
|||||||
"@angular-eslint/template/no-negated-async": "off",
|
"@angular-eslint/template/no-negated-async": "off",
|
||||||
"@angular-eslint/template/eqeqeq": "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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -15,12 +15,19 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
# The ci step will test the dspace-angular code against DSpace REST.
|
# 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.
|
# 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_HOST: 127.0.0.1
|
||||||
DSPACE_REST_PORT: 8080
|
DSPACE_REST_PORT: 8080
|
||||||
DSPACE_REST_NAMESPACE: '/server'
|
DSPACE_REST_NAMESPACE: '/server'
|
||||||
DSPACE_REST_SSL: false
|
DSPACE_REST_SSL: false
|
||||||
# Spin up UI on 127.0.0.1 to avoid host resolution issues in e2e tests with Node 18+
|
# 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_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
|
# When Chrome version is specified, we pin to a specific version of Chrome
|
||||||
# Comment this out to use the latest release
|
# Comment this out to use the latest release
|
||||||
#CHROME_VERSION: "90.0.4430.212-1"
|
#CHROME_VERSION: "90.0.4430.212-1"
|
||||||
|
30
.github/workflows/docker.yml
vendored
30
.github/workflows/docker.yml
vendored
@@ -88,3 +88,33 @@ jobs:
|
|||||||
# Use tags / labels provided by 'docker/metadata-action' above
|
# Use tags / labels provided by 'docker/metadata-action' above
|
||||||
tags: ${{ steps.meta_build.outputs.tags }}
|
tags: ${{ steps.meta_build.outputs.tags }}
|
||||||
labels: ${{ steps.meta_build.outputs.labels }}
|
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 }}
|
||||||
|
15
Dockerfile
15
Dockerfile
@@ -2,20 +2,27 @@
|
|||||||
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
||||||
|
|
||||||
FROM node:18-alpine
|
FROM node:18-alpine
|
||||||
WORKDIR /app
|
|
||||||
ADD . /app/
|
|
||||||
EXPOSE 4000
|
|
||||||
|
|
||||||
# Ensure Python and other build tools are available
|
# Ensure Python and other build tools are available
|
||||||
# These are needed to install some node modules, especially on linux/arm64
|
# These are needed to install some node modules, especially on linux/arm64
|
||||||
RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/*
|
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
|
# 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
|
# See, for example https://github.com/yarnpkg/yarn/issues/5540
|
||||||
RUN yarn install --network-timeout 300000
|
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).
|
# On startup, run in DEVELOPMENT mode (this defaults to live reloading enabled, etc).
|
||||||
# Listen / accept connections from all IP addresses.
|
# Listen / accept connections from all IP addresses.
|
||||||
# NOTE: At this time it is only possible to run Docker container in Production mode
|
# 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
|
CMD yarn serve --host 0.0.0.0
|
||||||
|
31
Dockerfile.dist
Normal file
31
Dockerfile.dist
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# This image will be published as dspace/dspace-angular:$DSPACE_VERSION-dist
|
||||||
|
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
||||||
|
|
||||||
|
# Test build:
|
||||||
|
# docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist .
|
||||||
|
|
||||||
|
FROM node:18-alpine as build
|
||||||
|
|
||||||
|
# Ensure Python and other build tools are available
|
||||||
|
# These are needed to install some node modules, especially on linux/arm64
|
||||||
|
RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN yarn install --network-timeout 300000
|
||||||
|
|
||||||
|
ADD . /app/
|
||||||
|
RUN yarn build:prod
|
||||||
|
|
||||||
|
FROM node:18-alpine
|
||||||
|
RUN npm install --global pm2
|
||||||
|
|
||||||
|
COPY --chown=node:node --from=build /app/dist /app/dist
|
||||||
|
COPY --chown=node:node config /app/config
|
||||||
|
COPY --chown=node:node docker/dspace-ui.json /app/dspace-ui.json
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
USER node
|
||||||
|
ENV NODE_ENV production
|
||||||
|
EXPOSE 4000
|
||||||
|
CMD pm2-runtime start dspace-ui.json --json
|
@@ -266,7 +266,8 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"lintFilePatterns": [
|
"lintFilePatterns": [
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"src/**/*.html"
|
"src/**/*.html",
|
||||||
|
"src/**/*.json5"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -369,3 +369,8 @@ vocabularies:
|
|||||||
- filter: 'subject'
|
- filter: 'subject'
|
||||||
vocabulary: 'srsc'
|
vocabulary: 'srsc'
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query.
|
||||||
|
comcolSelectionSort:
|
||||||
|
sortField: 'dc.title'
|
||||||
|
sortDirection: 'ASC'
|
44
cypress.config.ts
Normal file
44
cypress.config.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { defineConfig } from 'cypress';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
videosFolder: 'cypress/videos',
|
||||||
|
screenshotsFolder: 'cypress/screenshots',
|
||||||
|
fixturesFolder: 'cypress/fixtures',
|
||||||
|
retries: {
|
||||||
|
runMode: 2,
|
||||||
|
openMode: 0,
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
// Global constants used in DSpace e2e tests (see also ./cypress/support/e2e.ts)
|
||||||
|
// May be overridden in our cypress.json config file using specified environment variables.
|
||||||
|
// Default values listed here are all valid for the Demo Entities Data set available at
|
||||||
|
// https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||||
|
// (This is the data set used in our CI environment)
|
||||||
|
|
||||||
|
// Admin account used for administrative tests
|
||||||
|
DSPACE_TEST_ADMIN_USER: 'dspacedemo+admin@gmail.com',
|
||||||
|
DSPACE_TEST_ADMIN_PASSWORD: 'dspace',
|
||||||
|
// Community/collection/publication used for view/edit tests
|
||||||
|
DSPACE_TEST_COMMUNITY: '0958c910-2037-42a9-81c7-dca80e3892b4',
|
||||||
|
DSPACE_TEST_COLLECTION: '282164f5-d325-4740-8dd1-fa4d6d3e7200',
|
||||||
|
DSPACE_TEST_ENTITY_PUBLICATION: 'e98b0f27-5c19-49a0-960d-eb6ad5287067',
|
||||||
|
// Search term (should return results) used in search tests
|
||||||
|
DSPACE_TEST_SEARCH_TERM: 'test',
|
||||||
|
// Collection used for submission tests
|
||||||
|
DSPACE_TEST_SUBMIT_COLLECTION_NAME: 'Sample Collection',
|
||||||
|
DSPACE_TEST_SUBMIT_COLLECTION_UUID: '9d8334e9-25d3-4a67-9cea-3dffdef80144',
|
||||||
|
// Account used to test basic submission process
|
||||||
|
DSPACE_TEST_SUBMIT_USER: 'dspacedemo+submit@gmail.com',
|
||||||
|
DSPACE_TEST_SUBMIT_USER_PASSWORD: 'dspace',
|
||||||
|
},
|
||||||
|
e2e: {
|
||||||
|
// Setup our plugins for e2e tests
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
return require('./cypress/plugins/index.ts')(on, config);
|
||||||
|
},
|
||||||
|
// This is the base URL that Cypress will run all tests against
|
||||||
|
// It can be overridden via the CYPRESS_BASE_URL environment variable
|
||||||
|
// (By default we set this to a value which should work in most development environments)
|
||||||
|
baseUrl: 'http://localhost:4000',
|
||||||
|
},
|
||||||
|
});
|
25
cypress.json
25
cypress.json
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"integrationFolder": "cypress/integration",
|
|
||||||
"supportFile": "cypress/support/index.ts",
|
|
||||||
"videosFolder": "cypress/videos",
|
|
||||||
"screenshotsFolder": "cypress/screenshots",
|
|
||||||
"pluginsFile": "cypress/plugins/index.ts",
|
|
||||||
"fixturesFolder": "cypress/fixtures",
|
|
||||||
"baseUrl": "http://127.0.0.1:4000",
|
|
||||||
"retries": {
|
|
||||||
"runMode": 2,
|
|
||||||
"openMode": 0
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"DSPACE_TEST_ADMIN_USER": "dspacedemo+admin@gmail.com",
|
|
||||||
"DSPACE_TEST_ADMIN_PASSWORD": "dspace",
|
|
||||||
"DSPACE_TEST_COMMUNITY": "0958c910-2037-42a9-81c7-dca80e3892b4",
|
|
||||||
"DSPACE_TEST_COLLECTION": "282164f5-d325-4740-8dd1-fa4d6d3e7200",
|
|
||||||
"DSPACE_TEST_ENTITY_PUBLICATION": "e98b0f27-5c19-49a0-960d-eb6ad5287067",
|
|
||||||
"DSPACE_TEST_SEARCH_TERM": "test",
|
|
||||||
"DSPACE_TEST_SUBMIT_COLLECTION_NAME": "Sample Collection",
|
|
||||||
"DSPACE_TEST_SUBMIT_COLLECTION_UUID": "9d8334e9-25d3-4a67-9cea-3dffdef80144",
|
|
||||||
"DSPACE_TEST_SUBMIT_USER": "dspacedemo+submit@gmail.com",
|
|
||||||
"DSPACE_TEST_SUBMIT_USER_PASSWORD": "dspace"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,10 +1,10 @@
|
|||||||
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
import { TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
||||||
import { testA11y } from 'cypress/support/utils';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('Breadcrumbs', () => {
|
describe('Breadcrumbs', () => {
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
// Visit an Item, as those have more breadcrumbs
|
// Visit an Item, as those have more breadcrumbs
|
||||||
cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION);
|
cy.visit('/entities/publication/'.concat(TEST_ENTITY_PUBLICATION));
|
||||||
|
|
||||||
// Wait for breadcrumbs to be visible
|
// Wait for breadcrumbs to be visible
|
||||||
cy.get('ds-breadcrumbs').should('be.visible');
|
cy.get('ds-breadcrumbs').should('be.visible');
|
@@ -1,13 +1,13 @@
|
|||||||
import { TEST_COLLECTION } from 'cypress/support';
|
import { TEST_COLLECTION } from 'cypress/support/e2e';
|
||||||
import { testA11y } from 'cypress/support/utils';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('Collection Page', () => {
|
describe('Collection Page', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
cy.visit('/collections/' + TEST_COLLECTION);
|
cy.visit('/collections/'.concat(TEST_COLLECTION));
|
||||||
|
|
||||||
// <ds-collection-page> tag must be loaded
|
// <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
|
// Analyze <ds-collection-page> for accessibility issues
|
||||||
testA11y('ds-collection-page');
|
testA11y('ds-collection-page');
|
37
cypress/e2e/collection-statistics.cy.ts
Normal file
37
cypress/e2e/collection-statistics.cy.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_COLLECTION } from 'cypress/support/e2e';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Collection Statistics Page', () => {
|
||||||
|
const COLLECTIONSTATISTICSPAGE = '/statistics/collections/'.concat(TEST_COLLECTION);
|
||||||
|
|
||||||
|
it('should load if you click on "Statistics" from a Collection page', () => {
|
||||||
|
cy.visit('/collections/'.concat(TEST_COLLECTION));
|
||||||
|
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||||
|
cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a "Total visits" section', () => {
|
||||||
|
cy.visit(COLLECTIONSTATISTICSPAGE);
|
||||||
|
cy.get('table[data-test="TotalVisits"]').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a "Total visits per month" section', () => {
|
||||||
|
cy.visit(COLLECTIONSTATISTICSPAGE);
|
||||||
|
// Check just for existence because this table is empty in CI environment as it's historical data
|
||||||
|
cy.get('.'.concat(TEST_COLLECTION).concat('_TotalVisitsPerMonth')).should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit(COLLECTIONSTATISTICSPAGE);
|
||||||
|
|
||||||
|
// <ds-collection-statistics-page> tag must be loaded
|
||||||
|
cy.get('ds-collection-statistics-page').should('be.visible');
|
||||||
|
|
||||||
|
// Verify / wait until "Total Visits" table's label is non-empty
|
||||||
|
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
|
||||||
|
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
|
||||||
|
|
||||||
|
// Analyze <ds-collection-statistics-page> for accessibility issues
|
||||||
|
testA11y('ds-collection-statistics-page');
|
||||||
|
});
|
||||||
|
});
|
@@ -7,10 +7,10 @@ describe('Community List Page', () => {
|
|||||||
cy.visit('/community-list');
|
cy.visit('/community-list');
|
||||||
|
|
||||||
// <ds-community-list-page> tag must be loaded
|
// <ds-community-list-page> tag must be loaded
|
||||||
cy.get('ds-community-list-page').should('exist');
|
cy.get('ds-community-list-page').should('be.visible');
|
||||||
|
|
||||||
// Open first Community (to show Collections)...that way we scan sub-elements as well
|
// Open every expand button on page, so that we can scan sub-elements as well
|
||||||
cy.get('ds-community-list :nth-child(1) > .btn-group > .btn').click();
|
cy.get('[data-test="expand-button"]').click({ multiple: true });
|
||||||
|
|
||||||
// Analyze <ds-community-list-page> for accessibility issues
|
// Analyze <ds-community-list-page> for accessibility issues
|
||||||
// Disable heading-order checks until it is fixed
|
// Disable heading-order checks until it is fixed
|
@@ -1,13 +1,13 @@
|
|||||||
import { TEST_COMMUNITY } from 'cypress/support';
|
import { TEST_COMMUNITY } from 'cypress/support/e2e';
|
||||||
import { testA11y } from 'cypress/support/utils';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('Community Page', () => {
|
describe('Community Page', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
cy.visit('/communities/' + TEST_COMMUNITY);
|
cy.visit('/communities/'.concat(TEST_COMMUNITY));
|
||||||
|
|
||||||
// <ds-community-page> tag must be loaded
|
// <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
|
// Analyze <ds-community-page> for accessibility issues
|
||||||
testA11y('ds-community-page',);
|
testA11y('ds-community-page',);
|
37
cypress/e2e/community-statistics.cy.ts
Normal file
37
cypress/e2e/community-statistics.cy.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_COMMUNITY } from 'cypress/support/e2e';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Community Statistics Page', () => {
|
||||||
|
const COMMUNITYSTATISTICSPAGE = '/statistics/communities/'.concat(TEST_COMMUNITY);
|
||||||
|
|
||||||
|
it('should load if you click on "Statistics" from a Community page', () => {
|
||||||
|
cy.visit('/communities/'.concat(TEST_COMMUNITY));
|
||||||
|
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||||
|
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a "Total visits" section', () => {
|
||||||
|
cy.visit(COMMUNITYSTATISTICSPAGE);
|
||||||
|
cy.get('table[data-test="TotalVisits"]').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a "Total visits per month" section', () => {
|
||||||
|
cy.visit(COMMUNITYSTATISTICSPAGE);
|
||||||
|
// Check just for existence because this table is empty in CI environment as it's historical data
|
||||||
|
cy.get('.'.concat(TEST_COMMUNITY).concat('_TotalVisitsPerMonth')).should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit(COMMUNITYSTATISTICSPAGE);
|
||||||
|
|
||||||
|
// <ds-community-statistics-page> tag must be loaded
|
||||||
|
cy.get('ds-community-statistics-page').should('be.visible');
|
||||||
|
|
||||||
|
// Verify / wait until "Total Visits" table's label is non-empty
|
||||||
|
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
|
||||||
|
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
|
||||||
|
|
||||||
|
// Analyze <ds-community-statistics-page> for accessibility issues
|
||||||
|
testA11y('ds-community-statistics-page');
|
||||||
|
});
|
||||||
|
});
|
31
cypress/e2e/homepage-statistics.cy.ts
Normal file
31
cypress/e2e/homepage-statistics.cy.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
import '../support/commands';
|
||||||
|
|
||||||
|
describe('Site Statistics Page', () => {
|
||||||
|
it('should load if you click on "Statistics" from homepage', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||||
|
cy.location('pathname').should('eq', '/statistics');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// generate 2 view events on an Item's page
|
||||||
|
cy.generateViewEvent(TEST_ENTITY_PUBLICATION, 'item');
|
||||||
|
cy.generateViewEvent(TEST_ENTITY_PUBLICATION, 'item');
|
||||||
|
|
||||||
|
cy.visit('/statistics');
|
||||||
|
|
||||||
|
// <ds-site-statistics-page> tag must be visable
|
||||||
|
cy.get('ds-site-statistics-page').should('be.visible');
|
||||||
|
|
||||||
|
// Verify / wait until "Total Visits" table's *last* label is non-empty
|
||||||
|
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
|
||||||
|
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').last().contains(REGEX_MATCH_NON_EMPTY_TEXT);
|
||||||
|
// Wait an extra 500ms, just so all entries in Total Visits have loaded.
|
||||||
|
cy.wait(500);
|
||||||
|
|
||||||
|
// Analyze <ds-site-statistics-page> for accessibility issues
|
||||||
|
testA11y('ds-site-statistics-page');
|
||||||
|
});
|
||||||
|
});
|
@@ -1,10 +1,10 @@
|
|||||||
import { Options } from 'cypress-axe';
|
import { 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';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('Item Page', () => {
|
describe('Item Page', () => {
|
||||||
const ITEMPAGE = '/items/' + TEST_ENTITY_PUBLICATION;
|
const ITEMPAGE = '/items/'.concat(TEST_ENTITY_PUBLICATION);
|
||||||
const ENTITYPAGE = '/entities/publication/' + 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]
|
// Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid]
|
||||||
it('should redirect to the entity page when navigating to an item page', () => {
|
it('should redirect to the entity page when navigating to an item page', () => {
|
||||||
@@ -16,7 +16,7 @@ describe('Item Page', () => {
|
|||||||
cy.visit(ENTITYPAGE);
|
cy.visit(ENTITYPAGE);
|
||||||
|
|
||||||
// <ds-item-page> tag must be loaded
|
// <ds-item-page> tag must be loaded
|
||||||
cy.get('ds-item-page').should('exist');
|
cy.get('ds-item-page').should('be.visible');
|
||||||
|
|
||||||
// Analyze <ds-item-page> for accessibility issues
|
// Analyze <ds-item-page> for accessibility issues
|
||||||
// Disable heading-order checks until it is fixed
|
// Disable heading-order checks until it is fixed
|
@@ -1,36 +1,41 @@
|
|||||||
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
||||||
import { testA11y } from 'cypress/support/utils';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('Item Statistics Page', () => {
|
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', () => {
|
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.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||||
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
|
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
|
||||||
cy.visit(ITEMSTATISTICSPAGE);
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
cy.get('ds-item-statistics-page').should('exist');
|
cy.get('ds-item-statistics-page').should('be.visible');
|
||||||
cy.get('ds-item-page').should('not.exist');
|
cy.get('ds-item-page').should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain a "Total visits" section', () => {
|
it('should contain a "Total visits" section', () => {
|
||||||
cy.visit(ITEMSTATISTICSPAGE);
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisits').should('exist');
|
cy.get('table[data-test="TotalVisits"]').should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain a "Total visits per month" section', () => {
|
it('should contain a "Total visits per month" section', () => {
|
||||||
cy.visit(ITEMSTATISTICSPAGE);
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
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', () => {
|
it('should pass accessibility tests', () => {
|
||||||
cy.visit(ITEMSTATISTICSPAGE);
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
|
|
||||||
// <ds-item-statistics-page> tag must be loaded
|
// <ds-item-statistics-page> tag must be loaded
|
||||||
cy.get('ds-item-statistics-page').should('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
|
// Analyze <ds-item-statistics-page> for accessibility issues
|
||||||
testA11y('ds-item-statistics-page');
|
testA11y('ds-item-statistics-page');
|
@@ -1,4 +1,4 @@
|
|||||||
import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
||||||
|
|
||||||
const page = {
|
const page = {
|
||||||
openLoginMenu() {
|
openLoginMenu() {
|
||||||
@@ -36,7 +36,7 @@ const page = {
|
|||||||
|
|
||||||
describe('Login Modal', () => {
|
describe('Login Modal', () => {
|
||||||
it('should login when clicking button & stay on same page', () => {
|
it('should login when clicking button & stay on same page', () => {
|
||||||
const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION;
|
const ENTITYPAGE = '/entities/publication/'.concat(TEST_ENTITY_PUBLICATION);
|
||||||
cy.visit(ENTITYPAGE);
|
cy.visit(ENTITYPAGE);
|
||||||
|
|
||||||
// Login menu should exist
|
// Login menu should exist
|
@@ -1,5 +1,5 @@
|
|||||||
import { Options } from 'cypress-axe';
|
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';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('My DSpace page', () => {
|
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.
|
// 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.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
|
// At least one recent submission should be displayed
|
||||||
cy.get('[data-test="list-object"]').should('be.visible');
|
cy.get('[data-test="list-object"]').should('be.visible');
|
||||||
@@ -42,12 +42,12 @@ describe('My DSpace page', () => {
|
|||||||
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
||||||
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
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
|
// Click button in sidebar to display detailed view
|
||||||
cy.get('ds-search-sidebar [data-test="detail-view"]').click();
|
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
|
// Analyze <ds-search-page> for accessibility issues
|
||||||
testA11y('ds-my-dspace-page',
|
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);
|
cy.get('ds-authorized-collection-selector input[type="search"]').type(TEST_SUBMIT_COLLECTION_NAME);
|
||||||
|
|
||||||
// Click on the button matching that known 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
|
// New URL should include /workspaceitems, as we've started a new submission
|
||||||
cy.url().should('include', '/workspaceitems');
|
cy.url().should('include', '/workspaceitems');
|
@@ -2,7 +2,7 @@ describe('PageNotFound', () => {
|
|||||||
it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => {
|
it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => {
|
||||||
// request an invalid page (UUIDs at root path aren't valid)
|
// request an invalid page (UUIDs at root path aren't valid)
|
||||||
cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false });
|
cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false });
|
||||||
cy.get('ds-pagenotfound').should('exist');
|
cy.get('ds-pagenotfound').should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not contain element ds-pagenotfound when navigating to existing page', () => {
|
it('should not contain element ds-pagenotfound when navigating to existing page', () => {
|
@@ -1,4 +1,4 @@
|
|||||||
import { TEST_SEARCH_TERM } from 'cypress/support';
|
import { TEST_SEARCH_TERM } from 'cypress/support/e2e';
|
||||||
|
|
||||||
const page = {
|
const page = {
|
||||||
fillOutQueryInNavBar(query) {
|
fillOutQueryInNavBar(query) {
|
||||||
@@ -27,7 +27,7 @@ describe('Search from Navigation Bar', () => {
|
|||||||
page.fillOutQueryInNavBar(query);
|
page.fillOutQueryInNavBar(query);
|
||||||
page.submitQueryByPressingEnter();
|
page.submitQueryByPressingEnter();
|
||||||
// New URL should include query param
|
// New URL should include query param
|
||||||
cy.url().should('include', 'query=' + query);
|
cy.url().should('include', 'query='.concat(query));
|
||||||
// Wait for search results to come back from the above GET command
|
// Wait for search results to come back from the above GET command
|
||||||
cy.wait('@search-results');
|
cy.wait('@search-results');
|
||||||
// At least one search result should be displayed
|
// At least one search result should be displayed
|
||||||
@@ -42,7 +42,7 @@ describe('Search from Navigation Bar', () => {
|
|||||||
page.fillOutQueryInNavBar(query);
|
page.fillOutQueryInNavBar(query);
|
||||||
page.submitQueryByPressingEnter();
|
page.submitQueryByPressingEnter();
|
||||||
// New URL should include query param
|
// New URL should include query param
|
||||||
cy.url().should('include', 'query=' + query);
|
cy.url().should('include', 'query='.concat(query));
|
||||||
// Wait for search results to come back from the above GET command
|
// Wait for search results to come back from the above GET command
|
||||||
cy.wait('@search-results');
|
cy.wait('@search-results');
|
||||||
// At least one search result should be displayed
|
// At least one search result should be displayed
|
||||||
@@ -57,7 +57,7 @@ describe('Search from Navigation Bar', () => {
|
|||||||
page.fillOutQueryInNavBar(query);
|
page.fillOutQueryInNavBar(query);
|
||||||
page.submitQueryByPressingIcon();
|
page.submitQueryByPressingIcon();
|
||||||
// New URL should include query param
|
// New URL should include query param
|
||||||
cy.url().should('include', 'query=' + query);
|
cy.url().should('include', 'query='.concat(query));
|
||||||
// Wait for search results to come back from the above GET command
|
// Wait for search results to come back from the above GET command
|
||||||
cy.wait('@search-results');
|
cy.wait('@search-results');
|
||||||
// At least one search result should be displayed
|
// At least one search result should be displayed
|
@@ -1,5 +1,5 @@
|
|||||||
import { Options } from 'cypress-axe';
|
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';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('Search Page', () => {
|
describe('Search Page', () => {
|
||||||
@@ -13,11 +13,11 @@ describe('Search Page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should load results and pass accessibility tests', () => {
|
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);
|
cy.get('[data-test="search-box"]').should('have.value', TEST_SEARCH_TERM);
|
||||||
|
|
||||||
// <ds-search-page> tag must be loaded
|
// <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
|
// At least one search result should be displayed
|
||||||
cy.get('[data-test="list-object"]').should('be.visible');
|
cy.get('[data-test="list-object"]').should('be.visible');
|
||||||
@@ -45,13 +45,13 @@ describe('Search Page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have a working grid view that passes accessibility tests', () => {
|
it('should have a working grid view that passes accessibility tests', () => {
|
||||||
cy.visit('/search?query=' + TEST_SEARCH_TERM);
|
cy.visit('/search?query='.concat(TEST_SEARCH_TERM));
|
||||||
|
|
||||||
// Click button in sidebar to display grid view
|
// Click button in sidebar to display grid view
|
||||||
cy.get('ds-search-sidebar [data-test="grid-view"]').click();
|
cy.get('ds-search-sidebar [data-test="grid-view"]').click();
|
||||||
|
|
||||||
// <ds-search-page> tag must be loaded
|
// <ds-search-page> tag must be loaded
|
||||||
cy.get('ds-search-page').should('exist');
|
cy.get('ds-search-page').should('be.visible');
|
||||||
|
|
||||||
// At least one grid object (card) should be displayed
|
// At least one grid object (card) should be displayed
|
||||||
cy.get('[data-test="grid-object"]').should('be.visible');
|
cy.get('[data-test="grid-object"]').should('be.visible');
|
@@ -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/e2e';
|
||||||
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';
|
|
||||||
|
|
||||||
describe('New Submission page', () => {
|
describe('New Submission page', () => {
|
||||||
// NOTE: We already test that new submissions can be started from MyDSpace in my-dspace.spec.ts
|
// 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', () => {
|
it('should create a new submission when using /submit path & pass accessibility', () => {
|
||||||
// Test that calling /submit with collection & entityType will create a new submission
|
// Test that calling /submit with collection & entityType will create a new submission
|
||||||
cy.visit('/submit?collection=' + 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.
|
// 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.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', () => {
|
it('should block submission & show errors if required fields are missing', () => {
|
||||||
// Create a new submission
|
// 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.
|
// 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.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', () => {
|
it('should allow for deposit if all required fields completed & file uploaded', () => {
|
||||||
// Create a new submission
|
// 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.
|
// 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.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
@@ -124,8 +122,6 @@ describe('New Submission page', () => {
|
|||||||
|
|
||||||
// Wait for upload to complete before proceeding
|
// Wait for upload to complete before proceeding
|
||||||
cy.wait('@upload');
|
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.
|
// Wait for deposit button to not be disabled & click it.
|
||||||
cy.get('button#deposit').should('not.be.disabled').click();
|
cy.get('button#deposit').should('not.be.disabled').click();
|
@@ -1,32 +0,0 @@
|
|||||||
import { TEST_COLLECTION } from 'cypress/support';
|
|
||||||
import { testA11y } from 'cypress/support/utils';
|
|
||||||
|
|
||||||
describe('Collection Statistics Page', () => {
|
|
||||||
const COLLECTIONSTATISTICSPAGE = '/statistics/collections/' + TEST_COLLECTION;
|
|
||||||
|
|
||||||
it('should load if you click on "Statistics" from a Collection page', () => {
|
|
||||||
cy.visit('/collections/' + TEST_COLLECTION);
|
|
||||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
|
||||||
cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain a "Total visits" section', () => {
|
|
||||||
cy.visit(COLLECTIONSTATISTICSPAGE);
|
|
||||||
cy.get('.' + TEST_COLLECTION + '_TotalVisits').should('exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain a "Total visits per month" section', () => {
|
|
||||||
cy.visit(COLLECTIONSTATISTICSPAGE);
|
|
||||||
cy.get('.' + TEST_COLLECTION + '_TotalVisitsPerMonth').should('exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
|
||||||
cy.visit(COLLECTIONSTATISTICSPAGE);
|
|
||||||
|
|
||||||
// <ds-collection-statistics-page> tag must be loaded
|
|
||||||
cy.get('ds-collection-statistics-page').should('exist');
|
|
||||||
|
|
||||||
// Analyze <ds-collection-statistics-page> for accessibility issues
|
|
||||||
testA11y('ds-collection-statistics-page');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,32 +0,0 @@
|
|||||||
import { TEST_COMMUNITY } from 'cypress/support';
|
|
||||||
import { testA11y } from 'cypress/support/utils';
|
|
||||||
|
|
||||||
describe('Community Statistics Page', () => {
|
|
||||||
const COMMUNITYSTATISTICSPAGE = '/statistics/communities/' + TEST_COMMUNITY;
|
|
||||||
|
|
||||||
it('should load if you click on "Statistics" from a Community page', () => {
|
|
||||||
cy.visit('/communities/' + TEST_COMMUNITY);
|
|
||||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
|
||||||
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain a "Total visits" section', () => {
|
|
||||||
cy.visit(COMMUNITYSTATISTICSPAGE);
|
|
||||||
cy.get('.' + TEST_COMMUNITY + '_TotalVisits').should('exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain a "Total visits per month" section', () => {
|
|
||||||
cy.visit(COMMUNITYSTATISTICSPAGE);
|
|
||||||
cy.get('.' + TEST_COMMUNITY + '_TotalVisitsPerMonth').should('exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
|
||||||
cy.visit(COMMUNITYSTATISTICSPAGE);
|
|
||||||
|
|
||||||
// <ds-community-statistics-page> tag must be loaded
|
|
||||||
cy.get('ds-community-statistics-page').should('exist');
|
|
||||||
|
|
||||||
// Analyze <ds-community-statistics-page> for accessibility issues
|
|
||||||
testA11y('ds-community-statistics-page');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,19 +0,0 @@
|
|||||||
import { testA11y } from 'cypress/support/utils';
|
|
||||||
|
|
||||||
describe('Site Statistics Page', () => {
|
|
||||||
it('should load if you click on "Statistics" from homepage', () => {
|
|
||||||
cy.visit('/');
|
|
||||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
|
||||||
cy.location('pathname').should('eq', '/statistics');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
|
||||||
cy.visit('/statistics');
|
|
||||||
|
|
||||||
// <ds-site-statistics-page> tag must be loaded
|
|
||||||
cy.get('ds-site-statistics-page').should('exist');
|
|
||||||
|
|
||||||
// Analyze <ds-site-statistics-page> for accessibility issues
|
|
||||||
testA11y('ds-site-statistics-page');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -4,12 +4,17 @@
|
|||||||
// ***********************************************
|
// ***********************************************
|
||||||
|
|
||||||
import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
|
import { 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
|
// Declare Cypress namespace to help with Intellisense & code completion in IDEs
|
||||||
// ALL custom commands MUST be listed here for code completion to work
|
// ALL custom commands MUST be listed here for code completion to work
|
||||||
// tslint:disable-next-line:no-namespace
|
|
||||||
declare global {
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
interface Chainable<Subject = any> {
|
interface Chainable<Subject = any> {
|
||||||
/**
|
/**
|
||||||
@@ -27,6 +32,15 @@ declare global {
|
|||||||
* @param password password to login as
|
* @param password password to login as
|
||||||
*/
|
*/
|
||||||
loginViaForm(email: string, password: string): typeof loginViaForm;
|
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) {
|
if (!config.rest.baseUrl) {
|
||||||
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
|
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
|
||||||
} else {
|
} else {
|
||||||
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;
|
baseRestUrl = config.rest.baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// To login via REST, first we have to do a GET to obtain a valid CSRF token
|
// Now find domain of our REST API, again with a fallback.
|
||||||
cy.request( baseRestUrl + '/api/authn/status' )
|
let baseDomain = FALLBACK_TEST_REST_DOMAIN;
|
||||||
.then((response) => {
|
if (!config.rest.host) {
|
||||||
// We should receive a CSRF token returned in a response header
|
console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN);
|
||||||
expect(response.headers).to.have.property('dspace-xsrf-token');
|
} else {
|
||||||
const csrfToken = response.headers['dspace-xsrf-token'];
|
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
|
// Now, send login POST request including that CSRF token
|
||||||
cy.request({
|
cy.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: baseRestUrl + '/api/authn/login',
|
url: baseRestUrl + '/api/authn/login',
|
||||||
headers: { 'X-XSRF-TOKEN' : csrfToken},
|
headers: { [XSRF_REQUEST_HEADER]: csrfToken},
|
||||||
form: true, // indicates the body should be form urlencoded
|
form: true, // indicates the body should be form urlencoded
|
||||||
body: { user: email, password: password }
|
body: { user: email, password: password }
|
||||||
}).then((resp) => {
|
}).then((resp) => {
|
||||||
@@ -85,20 +104,20 @@ function login(email: string, password: string): void {
|
|||||||
// This ensures the UI will recognize we are logged in on next "visit()"
|
// This ensures the UI will recognize we are logged in on next "visit()"
|
||||||
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
|
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
// 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')
|
// Add as a Cypress command (i.e. assign to 'cy.login')
|
||||||
Cypress.Commands.add('login', login);
|
Cypress.Commands.add('login', login);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login user via displayed login form
|
* Login user via displayed login form
|
||||||
* @param email email to login as
|
* @param email email to login as
|
||||||
* @param password password to login as
|
* @param password password to login as
|
||||||
*/
|
*/
|
||||||
function loginViaForm(email: string, password: string): void {
|
function loginViaForm(email: string, password: string): void {
|
||||||
// Enter email
|
// Enter email
|
||||||
cy.get('ds-log-in [data-test="email"]').type(email);
|
cy.get('ds-log-in [data-test="email"]').type(email);
|
||||||
// Enter password
|
// Enter password
|
||||||
@@ -108,3 +127,68 @@ Cypress.Commands.add('login', login);
|
|||||||
}
|
}
|
||||||
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
|
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
|
||||||
Cypress.Commands.add('loginViaForm', loginViaForm);
|
Cypress.Commands.add('loginViaForm', loginViaForm);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate statistic view event for given object. Useful for testing statistics pages with
|
||||||
|
* pre-generated statistics. This just generates a single "hit", but can be called multiple times to
|
||||||
|
* generate multiple hits.
|
||||||
|
*
|
||||||
|
* NOTE: This requires that "solr-statistics.autoCommit=false" be set on the DSpace backend
|
||||||
|
* (as it is in our docker-compose-ci.yml used in CI).
|
||||||
|
* Otherwise, by default, new statistical events won't be saved until Solr's autocommit next triggers.
|
||||||
|
* @param uuid UUID of object
|
||||||
|
* @param dsoType type of DSpace Object (e.g. "item", "collection", "community")
|
||||||
|
*/
|
||||||
|
function generateViewEvent(uuid: string, dsoType: string): void {
|
||||||
|
// Cypress doesn't have access to the running application in Node.js.
|
||||||
|
// So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
|
||||||
|
// Instead, we'll read our running application's config.json, which contains the configs &
|
||||||
|
// is regenerated at runtime each time the Angular UI application starts up.
|
||||||
|
cy.task('readUIConfig').then((str: string) => {
|
||||||
|
// Parse config into a JSON object
|
||||||
|
const config = JSON.parse(str);
|
||||||
|
|
||||||
|
// Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found.
|
||||||
|
let baseRestUrl = FALLBACK_TEST_REST_BASE_URL;
|
||||||
|
if (!config.rest.baseUrl) {
|
||||||
|
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
|
||||||
|
} else {
|
||||||
|
baseRestUrl = config.rest.baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now find domain of our REST API, again with a fallback.
|
||||||
|
let baseDomain = FALLBACK_TEST_REST_DOMAIN;
|
||||||
|
if (!config.rest.host) {
|
||||||
|
console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN);
|
||||||
|
} else {
|
||||||
|
baseDomain = config.rest.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a fake CSRF Token. Set it in the required server-side cookie
|
||||||
|
const csrfToken = 'fakeGenerateViewEventCSRFToken';
|
||||||
|
cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain });
|
||||||
|
|
||||||
|
// Now, send 'statistics/viewevents' POST request including that fake CSRF token in required header
|
||||||
|
cy.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: baseRestUrl + '/api/statistics/viewevents',
|
||||||
|
headers: {
|
||||||
|
[XSRF_REQUEST_HEADER] : csrfToken,
|
||||||
|
// use a known public IP address to avoid being seen as a "bot"
|
||||||
|
'X-Forwarded-For': '1.1.1.1',
|
||||||
|
},
|
||||||
|
//form: true, // indicates the body should be form urlencoded
|
||||||
|
body: { targetId: uuid, targetType: dsoType },
|
||||||
|
}).then((resp) => {
|
||||||
|
// We expect a 201 (which means statistics event was created)
|
||||||
|
expect(resp.status).to.eq(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove cookie with fake CSRF token, as it's no longer needed
|
||||||
|
cy.clearCookie(DSPACE_XSRF_COOKIE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Add as a Cypress command (i.e. assign to 'cy.generateViewEvent')
|
||||||
|
Cypress.Commands.add('generateViewEvent', generateViewEvent);
|
||||||
|
|
||||||
|
@@ -30,11 +30,11 @@ beforeEach(() => {
|
|||||||
// For better stability between tests, we visit "about:blank" (i.e. blank page) after each test.
|
// 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.
|
// 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/
|
// Borrowed from: https://glebbahmutov.com/blog/visit-blank-page-between-tests/
|
||||||
afterEach(() => {
|
/*afterEach(() => {
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
win.location.href = 'about:blank';
|
win.location.href = 'about:blank';
|
||||||
});
|
});
|
||||||
});
|
});*/
|
||||||
|
|
||||||
|
|
||||||
// Global constants used in tests
|
// Global constants used in tests
|
||||||
@@ -43,10 +43,6 @@ afterEach(() => {
|
|||||||
// https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
// https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||||
// (This is the data set used in our CI environment)
|
// (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
|
// 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_USER = Cypress.env('DSPACE_TEST_ADMIN_USER') || 'dspacedemo+admin@gmail.com';
|
||||||
export const TEST_ADMIN_PASSWORD = Cypress.env('DSPACE_TEST_ADMIN_PASSWORD') || 'dspace';
|
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_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 = Cypress.env('DSPACE_TEST_SUBMIT_USER') || 'dspacedemo+submit@gmail.com';
|
||||||
export const TEST_SUBMIT_USER_PASSWORD = Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD') || 'dspace';
|
export const TEST_SUBMIT_USER_PASSWORD = Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD') || 'dspace';
|
||||||
|
|
||||||
|
|
||||||
|
// USEFUL REGEX for testing
|
||||||
|
|
||||||
|
// Match any string that contains at least one non-space character
|
||||||
|
// Can be used with "contains()" to determine if an element has a non-empty text value
|
||||||
|
export const REGEX_MATCH_NON_EMPTY_TEXT = /^(?!\s*$).+/;
|
@@ -6,7 +6,20 @@
|
|||||||
If you wish to run DSpace on Docker in production, we recommend building your own Docker images. You are welcome to borrow ideas/concepts from the below images in doing so. But, the below images should not be used "as is" in any production scenario.
|
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'
|
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 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
|
- docker-compose.yml
|
||||||
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
|
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
|
||||||
- docker-compose-rest.yml
|
- 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
|
## 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
|
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.
|
## 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._
|
_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
|
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
|
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
|
## Ingest test data from AIPDIR
|
||||||
|
|
||||||
Create an administrator
|
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
|
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).
|
## 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. Travis will perform CI testing of Angular using Node to drive the tests._
|
_In this instance, only the REST api runs in Docker using the Entities dataset. GitHub Actions will perform CI testing of Angular using Node to drive the tests. See `.github/workflows/build.yml` for more details._
|
||||||
|
|
||||||
|
This command is only really useful for testing our Continuous Integration process.
|
||||||
```
|
```
|
||||||
docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d
|
docker-compose -p d7ci -f docker/docker-compose-ci.yml up -d
|
||||||
```
|
```
|
||||||
|
@@ -30,6 +30,9 @@ services:
|
|||||||
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
||||||
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
|
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
|
||||||
solr__P__server: http://dspacesolr:8983/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:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
image: dspace/dspace:dspace-7_x-test
|
image: dspace/dspace:dspace-7_x-test
|
||||||
|
40
docker/docker-compose-dist.yml
Normal file
40
docker/docker-compose-dist.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#
|
||||||
|
# The contents of this file are subject to the license and copyright
|
||||||
|
# detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
# tree and available online at
|
||||||
|
#
|
||||||
|
# http://www.dspace.org/license/
|
||||||
|
#
|
||||||
|
|
||||||
|
# Docker Compose for running the DSpace Angular UI dist build
|
||||||
|
# for previewing with the DSpace Demo site backend
|
||||||
|
version: '3.7'
|
||||||
|
networks:
|
||||||
|
dspacenet:
|
||||||
|
services:
|
||||||
|
dspace-angular:
|
||||||
|
container_name: dspace-angular
|
||||||
|
environment:
|
||||||
|
DSPACE_UI_SSL: 'false'
|
||||||
|
DSPACE_UI_HOST: dspace-angular
|
||||||
|
DSPACE_UI_PORT: '4000'
|
||||||
|
DSPACE_UI_NAMESPACE: /
|
||||||
|
# NOTE: When running the UI in production mode (which the -dist image does),
|
||||||
|
# these DSPACE_REST_* variables MUST point at a public, HTTPS URL.
|
||||||
|
# This is because Server Side Rendering (SSR) currently requires a public URL,
|
||||||
|
# see this bug: https://github.com/DSpace/dspace-angular/issues/1485
|
||||||
|
DSPACE_REST_SSL: 'true'
|
||||||
|
DSPACE_REST_HOST: api7.dspace.org
|
||||||
|
DSPACE_REST_PORT: 443
|
||||||
|
DSPACE_REST_NAMESPACE: /server
|
||||||
|
image: dspace/dspace-angular:dspace-7_x-dist
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: Dockerfile.dist
|
||||||
|
networks:
|
||||||
|
dspacenet:
|
||||||
|
ports:
|
||||||
|
- published: 4000
|
||||||
|
target: 4000
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
@@ -39,7 +39,7 @@ services:
|
|||||||
# proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests
|
# proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests
|
||||||
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
||||||
proxies__P__trusted__P__ipranges: '172.23.0'
|
proxies__P__trusted__P__ipranges: '172.23.0'
|
||||||
image: dspace/dspace:dspace-7_x-test
|
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
networks:
|
networks:
|
||||||
@@ -82,8 +82,7 @@ services:
|
|||||||
# DSpace Solr container
|
# DSpace Solr container
|
||||||
dspacesolr:
|
dspacesolr:
|
||||||
container_name: dspacesolr
|
container_name: dspacesolr
|
||||||
# Uses official Solr image at https://hub.docker.com/_/solr/
|
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}"
|
||||||
image: solr:8.11-slim
|
|
||||||
# Needs main 'dspace' container to start first to guarantee access to solr_configs
|
# Needs main 'dspace' container to start first to guarantee access to solr_configs
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspace
|
- dspace
|
||||||
@@ -96,28 +95,26 @@ services:
|
|||||||
tty: true
|
tty: true
|
||||||
working_dir: /var/solr/data
|
working_dir: /var/solr/data
|
||||||
volumes:
|
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
|
# Keep Solr data directory between reboots
|
||||||
- solr_data:/var/solr/data
|
- solr_data:/var/solr/data
|
||||||
# Initialize all DSpace Solr cores using the mounted local configsets (see above), then start Solr
|
# 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
|
# * 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
|
# * Second, copy configsets to this core:
|
||||||
# to the latest configs. If it's a newly created core, this is a no-op.
|
# 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:
|
entrypoint:
|
||||||
- /bin/bash
|
- /bin/bash
|
||||||
- '-c'
|
- '-c'
|
||||||
- |
|
- |
|
||||||
init-var-solr
|
init-var-solr
|
||||||
precreate-core authority /opt/solr/server/solr/configsets/dspace/authority
|
precreate-core authority /opt/solr/server/solr/configsets/dspace/authority
|
||||||
cp -r -u /opt/solr/server/solr/configsets/dspace/authority/* authority
|
cp -r /opt/solr/server/solr/configsets/dspace/authority/* authority
|
||||||
precreate-core oai /opt/solr/server/solr/configsets/dspace/oai
|
precreate-core oai /opt/solr/server/solr/configsets/dspace/oai
|
||||||
cp -r -u /opt/solr/server/solr/configsets/dspace/oai/* oai
|
cp -r /opt/solr/server/solr/configsets/dspace/oai/* oai
|
||||||
precreate-core search /opt/solr/server/solr/configsets/dspace/search
|
precreate-core search /opt/solr/server/solr/configsets/dspace/search
|
||||||
cp -r -u /opt/solr/server/solr/configsets/dspace/search/* search
|
cp -r /opt/solr/server/solr/configsets/dspace/search/* search
|
||||||
precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics
|
precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics
|
||||||
cp -r -u /opt/solr/server/solr/configsets/dspace/statistics/* statistics
|
cp -r /opt/solr/server/solr/configsets/dspace/statistics/* statistics
|
||||||
exec solr -f
|
exec solr -f
|
||||||
volumes:
|
volumes:
|
||||||
assetstore:
|
assetstore:
|
||||||
|
11
docker/dspace-ui.json
Normal file
11
docker/dspace-ui.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"name": "dspace-ui",
|
||||||
|
"cwd": "/app",
|
||||||
|
"script": "dist/server/main.js",
|
||||||
|
"instances": "max",
|
||||||
|
"exec_mode": "cluster"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -163,13 +163,14 @@
|
|||||||
"compression-webpack-plugin": "^9.2.0",
|
"compression-webpack-plugin": "^9.2.0",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "9.7.0",
|
"cypress": "12.9.0",
|
||||||
"cypress-axe": "^0.14.0",
|
"cypress-axe": "^1.1.0",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"eslint": "^8.2.0",
|
"eslint": "^8.2.0",
|
||||||
"eslint-plugin-deprecation": "^1.3.2",
|
"eslint-plugin-deprecation": "^1.3.2",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-plugin-import": "^2.25.4",
|
||||||
"eslint-plugin-jsdoc": "^39.6.4",
|
"eslint-plugin-jsdoc": "^39.6.4",
|
||||||
|
"eslint-plugin-jsonc": "^2.6.0",
|
||||||
"eslint-plugin-lodash": "^7.4.0",
|
"eslint-plugin-lodash": "^7.4.0",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"express-static-gzip": "^2.1.5",
|
"express-static-gzip": "^2.1.5",
|
||||||
@@ -198,7 +199,7 @@
|
|||||||
"sass-resources-loader": "^2.1.1",
|
"sass-resources-loader": "^2.1.1",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
"typescript": "~4.5.5",
|
"typescript": "~4.5.5",
|
||||||
"webpack": "^5.69.1",
|
"webpack": "^5.76.0",
|
||||||
"webpack-bundle-analyzer": "^4.4.0",
|
"webpack-bundle-analyzer": "^4.4.0",
|
||||||
"webpack-cli": "^4.2.0",
|
"webpack-cli": "^4.2.0",
|
||||||
"webpack-dev-server": "^4.5.0"
|
"webpack-dev-server": "^4.5.0"
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
<button class="mr-auto btn btn-success addEPerson-button"
|
<button class="mr-auto btn btn-success addEPerson-button"
|
||||||
(click)="isEPersonFormShown = true">
|
(click)="isEPersonFormShown = true">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline">{{labelPrefix + 'button.add' | translate}}</span>
|
<span class="d-none d-sm-inline ml-1">{{labelPrefix + 'button.add' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<div class="flex-grow-1 mr-3 ml-3">
|
<div class="flex-grow-1 mr-3 ml-3">
|
||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" name="query" id="query" formControlName="query"
|
<input type="text" name="query" id="query" formControlName="query"
|
||||||
class="form-control" attr.aria-label="{{labelPrefix + 'search.placeholder' | translate}}"
|
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
|
||||||
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
|
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
<button type="submit" class="search-button btn btn-primary">
|
<button type="submit" class="search-button btn btn-primary">
|
||||||
@@ -72,13 +72,13 @@
|
|||||||
<td>{{epersonDto.eperson.email}}</td>
|
<td>{{epersonDto.eperson.email}}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button class="delete-button" (click)="toggleEditEPerson(epersonDto.eperson)"
|
<button (click)="toggleEditEPerson(epersonDto.eperson)"
|
||||||
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
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: epersonDto.eperson.name} }}">
|
||||||
<i class="fas fa-edit fa-fw"></i>
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
|
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
|
||||||
class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
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: epersonDto.eperson.name} }}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
@@ -42,6 +42,7 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
jasmine.getEnv().allowRespy(true);
|
||||||
mockEPeople = [EPersonMock, EPersonMock2];
|
mockEPeople = [EPersonMock, EPersonMock2];
|
||||||
ePersonDataServiceStub = {
|
ePersonDataServiceStub = {
|
||||||
activeEPerson: null,
|
activeEPerson: null,
|
||||||
@@ -260,17 +261,16 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
describe('delete EPerson button when the isAuthorized returns false', () => {
|
describe('delete EPerson button when the isAuthorized returns false', () => {
|
||||||
let ePeopleDeleteButton;
|
let ePeopleDeleteButton;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false));
|
||||||
isAuthorized: observableOf(false)
|
component.initialisePage();
|
||||||
});
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be disabled', () => {
|
it('should be disabled', () => {
|
||||||
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
|
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
|
||||||
ePeopleDeleteButton.forEach((deleteButton) => {
|
ePeopleDeleteButton.forEach((deleteButton: DebugElement) => {
|
||||||
expect(deleteButton.nativeElement.disabled).toBe(true);
|
expect(deleteButton.nativeElement.disabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -13,12 +13,13 @@
|
|||||||
[formGroup]="formGroup"
|
[formGroup]="formGroup"
|
||||||
[formLayout]="formLayout"
|
[formLayout]="formLayout"
|
||||||
[displayCancel]="false"
|
[displayCancel]="false"
|
||||||
|
[submitLabel]="submitLabel"
|
||||||
(submitForm)="onSubmit()">
|
(submitForm)="onSubmit()">
|
||||||
<div before class="btn-group">
|
<div before class="btn-group">
|
||||||
<button (click)="onCancel()"
|
<button (click)="onCancel()"
|
||||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div between class="btn-group">
|
<div *ngIf="displayResetPassword" between class="btn-group">
|
||||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" (click)="resetPassword()">
|
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" (click)="resetPassword()">
|
||||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -165,6 +165,15 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
isImpersonated = false;
|
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
|
* Subscription to email field value change
|
||||||
*/
|
*/
|
||||||
@@ -188,6 +197,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
this.epersonInitial = eperson;
|
this.epersonInitial = eperson;
|
||||||
if (hasValue(eperson)) {
|
if (hasValue(eperson)) {
|
||||||
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
||||||
|
this.displayResetPassword = true;
|
||||||
|
this.submitLabel = 'form.submit';
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
@@ -37,10 +37,10 @@ describe('MembersListComponent', () => {
|
|||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let groupsDataServiceStub: any;
|
let groupsDataServiceStub: any;
|
||||||
let activeGroup;
|
let activeGroup;
|
||||||
let allEPersons;
|
let allEPersons: EPerson[];
|
||||||
let allGroups;
|
let allGroups: Group[];
|
||||||
let epersonMembers;
|
let epersonMembers: EPerson[];
|
||||||
let subgroupMembers;
|
let subgroupMembers: Group[];
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -53,7 +53,7 @@ describe('MembersListComponent', () => {
|
|||||||
activeGroup: activeGroup,
|
activeGroup: activeGroup,
|
||||||
epersonMembers: epersonMembers,
|
epersonMembers: epersonMembers,
|
||||||
subgroupMembers: subgroupMembers,
|
subgroupMembers: subgroupMembers,
|
||||||
findListByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
findListByHref(_href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
||||||
},
|
},
|
||||||
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
@@ -147,6 +147,7 @@ describe('MembersListComponent', () => {
|
|||||||
});
|
});
|
||||||
afterEach(fakeAsync(() => {
|
afterEach(fakeAsync(() => {
|
||||||
fixture.destroy();
|
fixture.destroy();
|
||||||
|
fixture.debugElement.nativeElement.remove();
|
||||||
flush();
|
flush();
|
||||||
component = null;
|
component = null;
|
||||||
fixture.debugElement.nativeElement.remove();
|
fixture.debugElement.nativeElement.remove();
|
||||||
@@ -168,12 +169,19 @@ describe('MembersListComponent', () => {
|
|||||||
|
|
||||||
describe('search', () => {
|
describe('search', () => {
|
||||||
describe('when searching without query', () => {
|
describe('when searching without query', () => {
|
||||||
let epersonsFound;
|
let epersonsFound: DebugElement[];
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
|
spyOn(component, 'isMemberOfGroup').and.callFake((ePerson: EPerson) => {
|
||||||
|
return observableOf(activeGroup.epersons.includes(ePerson));
|
||||||
|
});
|
||||||
component.search({ scope: 'metadata', query: '' });
|
component.search({ scope: 'metadata', query: '' });
|
||||||
tick();
|
tick();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||||
|
// Stop using the fake spy function (because otherwise the clicking on the buttons will not change anything
|
||||||
|
// because they don't change the value of activeGroup.epersons)
|
||||||
|
jasmine.getEnv().allowRespy(true);
|
||||||
|
spyOn(component, 'isMemberOfGroup').and.callThrough();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should display all epersons', () => {
|
it('should display all epersons', () => {
|
||||||
@@ -182,62 +190,56 @@ describe('MembersListComponent', () => {
|
|||||||
|
|
||||||
describe('if eperson is already a eperson', () => {
|
describe('if eperson is already a eperson', () => {
|
||||||
it('should have delete button, else it should have add button', () => {
|
it('should have delete button, else it should have add button', () => {
|
||||||
activeGroup.epersons.map((eperson: EPerson) => {
|
const memberIds: string[] = activeGroup.epersons.map((ePerson: EPerson) => ePerson.id);
|
||||||
epersonsFound.map((foundEPersonRowElement) => {
|
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||||
if (foundEPersonRowElement.debugElement !== undefined) {
|
const epersonId: DebugElement = foundEPersonRowElement.query(By.css('td:first-child'));
|
||||||
const epersonId = foundEPersonRowElement.debugElement.query(By.css('td:first-child'));
|
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
if (memberIds.includes(epersonId.nativeElement.textContent)) {
|
||||||
if (epersonId.nativeElement.textContent === eperson.id) {
|
expect(addButton).toBeNull();
|
||||||
expect(addButton).toBeUndefined();
|
expect(deleteButton).not.toBeNull();
|
||||||
expect(deleteButton).toBeDefined();
|
|
||||||
} else {
|
} else {
|
||||||
expect(deleteButton).toBeUndefined();
|
expect(deleteButton).toBeNull();
|
||||||
expect(addButton).toBeDefined();
|
expect(addButton).not.toBeNull();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if first add button is pressed', () => {
|
describe('if first add button is pressed', () => {
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
|
const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
|
||||||
addButton.nativeElement.click();
|
addButton.nativeElement.click();
|
||||||
tick();
|
tick();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
it('all groups in search member of selected group', () => {
|
it('then all the ePersons are member of the active group', () => {
|
||||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||||
expect(epersonsFound.length).toEqual(2);
|
expect(epersonsFound.length).toEqual(2);
|
||||||
epersonsFound.map((foundEPersonRowElement) => {
|
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||||
if (foundEPersonRowElement.debugElement !== undefined) {
|
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
expect(addButton).toBeNull();
|
||||||
expect(addButton).toBeUndefined();
|
expect(deleteButton).not.toBeNull();
|
||||||
expect(deleteButton).toBeDefined();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if first delete button is pressed', () => {
|
describe('if first delete button is pressed', () => {
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt'));
|
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt'));
|
||||||
addButton.nativeElement.click();
|
deleteButton.nativeElement.click();
|
||||||
tick();
|
tick();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
it('first eperson in search delete button, because now member', () => {
|
it('then no ePerson is member of the active group', () => {
|
||||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||||
epersonsFound.map((foundEPersonRowElement) => {
|
expect(epersonsFound.length).toEqual(2);
|
||||||
if (foundEPersonRowElement.debugElement !== undefined) {
|
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||||
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
expect(deleteButton).toBeUndefined();
|
expect(deleteButton).toBeNull();
|
||||||
expect(addButton).toBeDefined();
|
expect(addButton).not.toBeNull();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -249,6 +249,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
* @param ePerson EPerson we want to delete as member from group that is currently being edited
|
* @param ePerson EPerson we want to delete as member from group that is currently being edited
|
||||||
*/
|
*/
|
||||||
deleteMemberFromGroup(ePerson: EpersonDtoModel) {
|
deleteMemberFromGroup(ePerson: EpersonDtoModel) {
|
||||||
|
ePerson.memberOfGroup = false;
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
||||||
|
@@ -65,7 +65,7 @@
|
|||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p *ngIf="(isActiveGroup(group) | async)">{{ messagePrefix + '.table.edit.currentGroup' | translate }}</p>
|
<span *ngIf="(isActiveGroup(group) | async)">{{ messagePrefix + '.table.edit.currentGroup' | translate }}</span>
|
||||||
|
|
||||||
<button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
<button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
||||||
(click)="addSubgroupToGroup(group)"
|
(click)="addSubgroupToGroup(group)"
|
||||||
|
@@ -1,14 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
|
||||||
import {
|
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
ComponentFixture,
|
|
||||||
fakeAsync,
|
|
||||||
flush,
|
|
||||||
inject,
|
|
||||||
TestBed,
|
|
||||||
tick,
|
|
||||||
waitForAsync
|
|
||||||
} from '@angular/core/testing';
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -46,8 +38,8 @@ describe('SubgroupsListComponent', () => {
|
|||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let groupsDataServiceStub: any;
|
let groupsDataServiceStub: any;
|
||||||
let activeGroup;
|
let activeGroup;
|
||||||
let subgroups;
|
let subgroups: Group[];
|
||||||
let allGroups;
|
let allGroups: Group[];
|
||||||
let routerStub;
|
let routerStub;
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
@@ -65,7 +57,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
getSubgroups(): Group {
|
getSubgroups(): Group {
|
||||||
return this.activeGroup;
|
return this.activeGroup;
|
||||||
},
|
},
|
||||||
findListByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
findListByHref(_href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
return this.subgroups$.pipe(
|
return this.subgroups$.pipe(
|
||||||
map((currentGroups: Group[]) => {
|
map((currentGroups: Group[]) => {
|
||||||
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));
|
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));
|
||||||
@@ -133,6 +125,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
});
|
});
|
||||||
afterEach(fakeAsync(() => {
|
afterEach(fakeAsync(() => {
|
||||||
fixture.destroy();
|
fixture.destroy();
|
||||||
|
fixture.debugElement.nativeElement.remove();
|
||||||
flush();
|
flush();
|
||||||
component = null;
|
component = null;
|
||||||
}));
|
}));
|
||||||
@@ -152,7 +145,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('if first group delete button is pressed', () => {
|
describe('if first group delete button is pressed', () => {
|
||||||
let groupsFound;
|
let groupsFound: DebugElement[];
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
|
const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
|
||||||
addButton.triggerEventHandler('click', {
|
addButton.triggerEventHandler('click', {
|
||||||
@@ -170,7 +163,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
|
|
||||||
describe('search', () => {
|
describe('search', () => {
|
||||||
describe('when searching with empty query', () => {
|
describe('when searching with empty query', () => {
|
||||||
let groupsFound;
|
let groupsFound: DebugElement[];
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
component.search({ query: '' });
|
component.search({ query: '' });
|
||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
@@ -181,9 +174,9 @@ describe('SubgroupsListComponent', () => {
|
|||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
expect(groupsFound.length).toEqual(2);
|
expect(groupsFound.length).toEqual(2);
|
||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
const groupIdsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
|
const groupIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
|
||||||
allGroups.map((group: Group) => {
|
allGroups.map((group: Group) => {
|
||||||
expect(groupIdsFound.find((foundEl) => {
|
expect(groupIdsFound.find((foundEl: DebugElement) => {
|
||||||
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
||||||
})).toBeTruthy();
|
})).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -195,30 +188,30 @@ describe('SubgroupsListComponent', () => {
|
|||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups;
|
const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups;
|
||||||
if (getSubgroups !== undefined && getSubgroups.length > 0) {
|
if (getSubgroups !== undefined && getSubgroups.length > 0) {
|
||||||
groupsFound.map((foundGroupRowElement) => {
|
groupsFound.map((foundGroupRowElement: DebugElement) => {
|
||||||
if (foundGroupRowElement.debugElement !== undefined) {
|
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
|
||||||
const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
expect(addButton).toBeUndefined();
|
expect(addButton).toBeNull();
|
||||||
expect(deleteButton).toBeDefined();
|
if (activeGroup.id === groupId.nativeElement.textContent) {
|
||||||
|
expect(deleteButton).toBeNull();
|
||||||
|
} else {
|
||||||
|
expect(deleteButton).not.toBeNull();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
getSubgroups.map((group: Group) => {
|
const subgroupIds: string[] = activeGroup.subgroups.map((group: Group) => group.id);
|
||||||
groupsFound.map((foundGroupRowElement) => {
|
groupsFound.map((foundGroupRowElement: DebugElement) => {
|
||||||
if (foundGroupRowElement.debugElement !== undefined) {
|
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
|
||||||
const groupId = foundGroupRowElement.debugElement.query(By.css('td:first-child'));
|
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
if (subgroupIds.includes(groupId.nativeElement.textContent)) {
|
||||||
if (groupId.nativeElement.textContent === group.id) {
|
expect(addButton).toBeNull();
|
||||||
expect(addButton).toBeUndefined();
|
expect(deleteButton).not.toBeNull();
|
||||||
expect(deleteButton).toBeDefined();
|
|
||||||
} else {
|
} else {
|
||||||
expect(deleteButton).toBeUndefined();
|
expect(deleteButton).toBeNull();
|
||||||
expect(addButton).toBeDefined();
|
expect(addButton).not.toBeNull();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<button class="mr-auto btn btn-success"
|
<button class="mr-auto btn btn-success"
|
||||||
[routerLink]="['newGroup']">
|
[routerLink]="['newGroup']">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline">{{messagePrefix + 'button.add' | translate}}</span>
|
<span class="d-none d-sm-inline ml-1">{{messagePrefix + 'button.add' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="flex-grow-1 mr-3">
|
<div class="flex-grow-1 mr-3">
|
||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" name="query" id="query" formControlName="query"
|
<input type="text" name="query" id="query" formControlName="query"
|
||||||
class="form-control" attr.aria-label="{{messagePrefix + 'search.placeholder' | translate}}"
|
class="form-control" [attr.aria-label]="messagePrefix + 'search.placeholder' | translate"
|
||||||
[placeholder]="(messagePrefix + 'search.placeholder' | translate)" >
|
[placeholder]="(messagePrefix + 'search.placeholder' | translate)" >
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
<button type="submit" class="search-button btn btn-primary">
|
<button type="submit" class="search-button btn btn-primary">
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
||||||
<td>
|
<td>
|
||||||
<label>
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[checked]="isSelected(bitstreamFormat) | async"
|
[checked]="isSelected(bitstreamFormat) | async"
|
||||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(schema) | async}">
|
[ngClass]="{'table-primary' : isActive(schema) | async}">
|
||||||
<td>
|
<td>
|
||||||
<label>
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[checked]="isSelected(schema) | async"
|
[checked]="isSelected(schema) | async"
|
||||||
(change)="selectMetadataSchema(schema, $event)"
|
(change)="selectMetadataSchema(schema, $event)"
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
.selectable-row:hover {
|
.selectable-row:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep #metadatadataschemagroup {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
@@ -34,14 +34,14 @@
|
|||||||
<tr *ngFor="let field of fields?.page"
|
<tr *ngFor="let field of fields?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
[ngClass]="{'table-primary' : isActive(field) | async}">
|
||||||
<td>
|
<td>
|
||||||
<label>
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[checked]="isSelected(field) | async"
|
[checked]="isSelected(field) | async"
|
||||||
(change)="selectMetadataField(field, $event)">
|
(change)="selectMetadataField(field, $event)">
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier" class="mb-0">.</label>{{field.qualifier}}</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@@ -1,3 +1,8 @@
|
|||||||
.selectable-row:hover {
|
.selectable-row:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep #metadatadatafieldgroup {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
@@ -218,7 +218,8 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
|
|||||||
{
|
{
|
||||||
path: 'statistics',
|
path: 'statistics',
|
||||||
loadChildren: () => import('./statistics-page/statistics-page-routing.module')
|
loadChildren: () => import('./statistics-page/statistics-page-routing.module')
|
||||||
.then((m) => m.StatisticsPageRoutingModule)
|
.then((m) => m.StatisticsPageRoutingModule),
|
||||||
|
canActivate: [EndUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: HEALTH_PAGE_PATH,
|
path: HEALTH_PAGE_PATH,
|
||||||
@@ -228,7 +229,7 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
|
|||||||
{
|
{
|
||||||
path: ACCESS_CONTROL_MODULE_PATH,
|
path: ACCESS_CONTROL_MODULE_PATH,
|
||||||
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
|
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
|
||||||
canActivate: [GroupAdministratorGuard],
|
canActivate: [GroupAdministratorGuard, EndUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'subscriptions',
|
path: 'subscriptions',
|
||||||
|
@@ -618,7 +618,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
// TODO: Set bitstream to primary when supported
|
// TODO: Set bitstream to primary when supported
|
||||||
const primary = rawForm.fileNamePrimaryContainer.primaryBitstream;
|
const primary = rawForm.fileNamePrimaryContainer.primaryBitstream;
|
||||||
Metadata.setFirstValue(newMetadata, 'dc.title', rawForm.fileNamePrimaryContainer.fileName);
|
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);
|
Metadata.setFirstValue(newMetadata, 'dc.description', rawForm.descriptionContainer.description);
|
||||||
|
}
|
||||||
if (this.isIIIF) {
|
if (this.isIIIF) {
|
||||||
// It's helpful to remove these metadata elements entirely when the form value is empty.
|
// 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
|
// This avoids potential issues on the REST side and makes it possible to do things like
|
||||||
|
@@ -22,6 +22,7 @@ import { PaginationService } from '../../core/pagination/pagination.service';
|
|||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||||
import { APP_CONFIG } from '../../../config/app-config.interface';
|
import { APP_CONFIG } from '../../../config/app-config.interface';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
describe('BrowseByDatePageComponent', () => {
|
describe('BrowseByDatePageComponent', () => {
|
||||||
let comp: 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 = {
|
const mockBrowseService = {
|
||||||
getBrowseEntriesFor: (options: BrowseEntrySearchOptions) => toRemoteData([]),
|
getBrowseEntriesFor: (options: BrowseEntrySearchOptions) => toRemoteData([]),
|
||||||
getBrowseItemsFor: (value: string, options: BrowseEntrySearchOptions) => toRemoteData([firstItem]),
|
getBrowseItemsFor: (value: string, options: BrowseEntrySearchOptions) => toRemoteData([firstItem]),
|
||||||
getFirstItemFor: () => createSuccessfulRemoteDataObject$(firstItem)
|
getFirstItemFor: (definition: string, scope?: string, sortDirection?: SortDirection) => null
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockDsoService = {
|
const mockDsoService = {
|
||||||
@@ -91,9 +102,14 @@ describe('BrowseByDatePageComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(BrowseByDatePageComponent);
|
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;
|
comp = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
|
||||||
route = (comp as any).route;
|
route = (comp as any).route;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize the list of items', () => {
|
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', () => {
|
it('should create a list of startsWith options with the current year first', () => {
|
||||||
expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear());
|
//expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear());
|
||||||
|
expect(comp.startsWithOptions[0]).toEqual(1960);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
|
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
BrowseByMetadataPageComponent,
|
BrowseByMetadataPageComponent,
|
||||||
browseParamsToOptions, getBrowseSearchOptions
|
browseParamsToOptions,
|
||||||
|
getBrowseSearchOptions
|
||||||
} from '../browse-by-metadata-page/browse-by-metadata-page.component';
|
} from '../browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
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 { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||||
import { BrowseService } from '../../core/browse/browse.service';
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
@@ -16,7 +15,9 @@ import { map } from 'rxjs/operators';
|
|||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { isValidDate } from '../../shared/date.util';
|
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';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-by-date-page',
|
selector: 'ds-browse-by-date-page',
|
||||||
@@ -72,30 +73,24 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the StartsWith options
|
* Update the StartsWith options
|
||||||
* In this implementation, it creates a list of years starting from now, going all the way back to the earliest
|
* In this implementation, it creates a list of years starting from the most recent item or the current year, going
|
||||||
* date found on an item within this scope. The further back in time, the bigger the change in years become to avoid
|
* all the way back to the earliest date found on an item within this scope. The further back in time, the bigger
|
||||||
* extremely long lists with a one-year difference.
|
* 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.
|
* 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 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 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
|
* @param scope The scope under which to fetch the earliest item for
|
||||||
*/
|
*/
|
||||||
updateStartsWithOptions(definition: string, metadataKeys: string[], scope?: string) {
|
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.subs.push(
|
||||||
this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData<Item>) => {
|
observableCombineLatest([firstItemRD, lastItemRD]).subscribe(([firstItem, lastItem]) => {
|
||||||
let lowerLimit = this.appConfig.browseBy.defaultLowerLimit;
|
let lowerLimit = this.getLimit(firstItem, metadataKeys, this.appConfig.browseBy.defaultLowerLimit);
|
||||||
if (hasValue(firstItemRD.payload)) {
|
let upperLimit = this.getLimit(lastItem, metadataKeys, new Date().getUTCFullYear());
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const options = [];
|
const options = [];
|
||||||
const currentYear = new Date().getUTCFullYear();
|
const oneYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
|
||||||
const oneYearBreak = Math.floor((currentYear - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
|
const fiveYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
|
||||||
const fiveYearBreak = Math.floor((currentYear - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
|
|
||||||
if (lowerLimit <= fiveYearBreak) {
|
if (lowerLimit <= fiveYearBreak) {
|
||||||
lowerLimit -= 10;
|
lowerLimit -= 10;
|
||||||
} else if (lowerLimit <= oneYearBreak) {
|
} else if (lowerLimit <= oneYearBreak) {
|
||||||
@@ -103,7 +98,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
} else {
|
} else {
|
||||||
lowerLimit -= 1;
|
lowerLimit -= 1;
|
||||||
}
|
}
|
||||||
let i = currentYear;
|
let i = upperLimit;
|
||||||
while (i > lowerLimit) {
|
while (i > lowerLimit) {
|
||||||
options.push(i);
|
options.push(i);
|
||||||
if (i <= fiveYearBreak) {
|
if (i <= fiveYearBreak) {
|
||||||
@@ -121,4 +116,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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -151,8 +151,17 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
|||||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||||
this.browseId = params.id || this.defaultBrowseId;
|
this.browseId = params.id || this.defaultBrowseId;
|
||||||
this.authority = params.authority;
|
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)) {
|
if (isNotEmpty(this.value)) {
|
||||||
this.updatePageWithItems(
|
this.updatePageWithItems(
|
||||||
browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), this.value, this.authority);
|
browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), this.value, this.authority);
|
||||||
@@ -305,7 +314,7 @@ export function browseParamsToOptions(params: any,
|
|||||||
metadata,
|
metadata,
|
||||||
paginationConfig,
|
paginationConfig,
|
||||||
sortConfig,
|
sortConfig,
|
||||||
+params.startsWith || params.startsWith,
|
params.startsWith,
|
||||||
params.scope,
|
params.scope,
|
||||||
fetchThumbnail
|
fetchThumbnail
|
||||||
);
|
);
|
||||||
|
@@ -28,14 +28,14 @@
|
|||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
<ds-search-form id="search-form"
|
<ds-themed-search-form id="search-form"
|
||||||
[query]="(searchOptions$ | async)?.query"
|
[query]="(searchOptions$ | async)?.query"
|
||||||
[scope]="(searchOptions$ | async)?.scope"
|
[scope]="(searchOptions$ | async)?.scope"
|
||||||
[currentUrl]="'./'"
|
[currentUrl]="'./'"
|
||||||
[inPlaceSearch]="true"
|
[inPlaceSearch]="true"
|
||||||
[searchPlaceholder]="'collection.edit.item-mapper.search-form.placeholder' | translate"
|
[searchPlaceholder]="'collection.edit.item-mapper.search-form.placeholder' | translate"
|
||||||
(submitSearch)="performedSearch = true">
|
(submitSearch)="performedSearch = true">
|
||||||
</ds-search-form>
|
</ds-themed-search-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -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 { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs';
|
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs';
|
||||||
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
|
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
@@ -29,6 +29,7 @@ import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
|||||||
import { getCollectionPageRoute } from './collection-page-routing-paths';
|
import { getCollectionPageRoute } from './collection-page-routing-paths';
|
||||||
import { redirectOn4xx } from '../core/shared/authorized.operators';
|
import { redirectOn4xx } from '../core/shared/authorized.operators';
|
||||||
import { BROWSE_LINKS_TO_FOLLOW } from '../core/browse/browse.service';
|
import { BROWSE_LINKS_TO_FOLLOW } from '../core/browse/browse.service';
|
||||||
|
import { APP_CONFIG, AppConfig } from '../../../src/config/app-config.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-collection-page',
|
selector: 'ds-collection-page',
|
||||||
@@ -69,13 +70,15 @@ export class CollectionPageComponent implements OnInit {
|
|||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private paginationService: PaginationService,
|
private paginationService: PaginationService,
|
||||||
private authorizationDataService: AuthorizationDataService,
|
private authorizationDataService: AuthorizationDataService,
|
||||||
|
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||||
) {
|
) {
|
||||||
this.paginationConfig = new PaginationComponentOptions();
|
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
||||||
this.paginationConfig.id = 'cp';
|
id: 'cp',
|
||||||
this.paginationConfig.pageSize = 5;
|
currentPage: 1,
|
||||||
this.paginationConfig.currentPage = 1;
|
pageSize: this.appConfig.browseBy.pageSize,
|
||||||
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
|
});
|
||||||
|
|
||||||
|
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@@ -28,7 +28,8 @@
|
|||||||
[title]="'toggle ' + node.name"
|
[title]="'toggle ' + node.name"
|
||||||
[attr.aria-label]="'toggle ' + node.name"
|
[attr.aria-label]="'toggle ' + node.name"
|
||||||
(click)="toggleExpanded(node)"
|
(click)="toggleExpanded(node)"
|
||||||
[ngClass]="(hasChild(null, node)| async) ? 'visible' : 'invisible'">
|
[ngClass]="(hasChild(null, node)| async) ? 'visible' : 'invisible'"
|
||||||
|
[attr.data-test]="(hasChild(null, node)| async) ? 'expand-button' : ''">
|
||||||
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
|
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
|
||||||
aria-hidden="true"></span>
|
aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -8,7 +8,7 @@ import { PostRequest } from '../data/request.models';
|
|||||||
import {
|
import {
|
||||||
XSRF_REQUEST_HEADER,
|
XSRF_REQUEST_HEADER,
|
||||||
XSRF_RESPONSE_HEADER
|
XSRF_RESPONSE_HEADER
|
||||||
} from '../xsrf/xsrf.interceptor';
|
} from '../xsrf/xsrf.constants';
|
||||||
|
|
||||||
describe(`ServerAuthRequestService`, () => {
|
describe(`ServerAuthRequestService`, () => {
|
||||||
let href: string;
|
let href: string;
|
||||||
|
@@ -13,7 +13,7 @@ import {
|
|||||||
XSRF_REQUEST_HEADER,
|
XSRF_REQUEST_HEADER,
|
||||||
XSRF_RESPONSE_HEADER,
|
XSRF_RESPONSE_HEADER,
|
||||||
DSPACE_XSRF_COOKIE
|
DSPACE_XSRF_COOKIE
|
||||||
} from '../xsrf/xsrf.interceptor';
|
} from '../xsrf/xsrf.constants';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@@ -22,6 +22,7 @@ import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
|||||||
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
||||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { BrowseDefinitionDataService } from './browse-definition-data.service';
|
import { BrowseDefinitionDataService } from './browse-definition-data.service';
|
||||||
|
import { SortDirection } from '../cache/models/sort-options.model';
|
||||||
|
|
||||||
|
|
||||||
export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig<BrowseEntry | Item>[] = [
|
export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig<BrowseEntry | Item>[] = [
|
||||||
@@ -160,8 +161,9 @@ export class BrowseService {
|
|||||||
* Get the first item for a metadata definition in an optional scope
|
* Get the first item for a metadata definition in an optional scope
|
||||||
* @param definition
|
* @param definition
|
||||||
* @param scope
|
* @param scope
|
||||||
|
* @param sortDirection optional sort parameter
|
||||||
*/
|
*/
|
||||||
getFirstItemFor(definition: string, scope?: string): Observable<RemoteData<Item>> {
|
getFirstItemFor(definition: string, scope?: string, sortDirection?: SortDirection): Observable<RemoteData<Item>> {
|
||||||
const href$ = this.getBrowseDefinitions().pipe(
|
const href$ = this.getBrowseDefinitions().pipe(
|
||||||
getBrowseDefinitionLinks(definition),
|
getBrowseDefinitionLinks(definition),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
@@ -177,6 +179,9 @@ export class BrowseService {
|
|||||||
}
|
}
|
||||||
args.push('page=0');
|
args.push('page=0');
|
||||||
args.push('size=1');
|
args.push('size=1');
|
||||||
|
if (sortDirection) {
|
||||||
|
args.push('sort=default,' + sortDirection);
|
||||||
|
}
|
||||||
if (isNotEmpty(args)) {
|
if (isNotEmpty(args)) {
|
||||||
href = new URLCombiner(href, `?${args.join('&')}`).toString();
|
href = new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||||
}
|
}
|
||||||
|
@@ -52,7 +52,7 @@ describe(`LocaleInterceptor`, () => {
|
|||||||
|
|
||||||
expect(httpRequest.request.headers.has('Accept-Language'));
|
expect(httpRequest.request.headers.has('Accept-Language'));
|
||||||
const lang = httpRequest.request.headers.get('Accept-Language');
|
const lang = httpRequest.request.headers.get('Accept-Language');
|
||||||
expect(lang).toBeDefined();
|
expect(lang).not.toBeNull();
|
||||||
expect(lang).toBe(languageList.toString());
|
expect(lang).toBe(languageList.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -62,14 +62,23 @@ describe('LocaleService test suite', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getCurrentLanguageCode', () => {
|
describe('getCurrentLanguageCode', () => {
|
||||||
it('should return language saved on cookie', () => {
|
beforeEach(() => {
|
||||||
|
spyOn(translateService, 'getLangs').and.returnValue(langList);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the language saved on cookie if it\'s a valid & active language', () => {
|
||||||
spyOnGet.and.returnValue('de');
|
spyOnGet.and.returnValue('de');
|
||||||
expect(service.getCurrentLanguageCode()).toBe('de');
|
expect(service.getCurrentLanguageCode()).toBe('de');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('', () => {
|
it('should return the default language if the cookie language is disabled', () => {
|
||||||
beforeEach(() => {
|
spyOnGet.and.returnValue('disabled');
|
||||||
spyOn(translateService, 'getLangs').and.returnValue(langList);
|
expect(service.getCurrentLanguageCode()).toBe('en');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the default language if the cookie language does not exist', () => {
|
||||||
|
spyOnGet.and.returnValue('does-not-exist');
|
||||||
|
expect(service.getCurrentLanguageCode()).toBe('en');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return language from browser setting', () => {
|
it('should return language from browser setting', () => {
|
||||||
@@ -82,7 +91,6 @@ describe('LocaleService test suite', () => {
|
|||||||
expect(service.getCurrentLanguageCode()).toBe('en');
|
expect(service.getCurrentLanguageCode()).toBe('en');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('getLanguageCodeFromCookie', () => {
|
describe('getLanguageCodeFromCookie', () => {
|
||||||
it('should return language from cookie', () => {
|
it('should return language from cookie', () => {
|
||||||
|
@@ -11,6 +11,7 @@ import { map, mergeMap, take } from 'rxjs/operators';
|
|||||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
import { RouteService } from '../services/route.service';
|
import { RouteService } from '../services/route.service';
|
||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { LangConfig } from '../../../config/lang-config.interface';
|
||||||
|
|
||||||
export const LANG_COOKIE = 'dsLanguage';
|
export const LANG_COOKIE = 'dsLanguage';
|
||||||
|
|
||||||
@@ -52,8 +53,7 @@ export class LocaleService {
|
|||||||
getCurrentLanguageCode(): string {
|
getCurrentLanguageCode(): string {
|
||||||
// Attempt to get the language from a cookie
|
// Attempt to get the language from a cookie
|
||||||
let lang = this.getLanguageCodeFromCookie();
|
let lang = this.getLanguageCodeFromCookie();
|
||||||
if (isEmpty(lang)) {
|
if (isEmpty(lang) || environment.languages.find((langConfig: LangConfig) => langConfig.code === lang && langConfig.active) === undefined) {
|
||||||
// Cookie not found
|
|
||||||
// Attempt to get the browser language from the user
|
// Attempt to get the browser language from the user
|
||||||
if (this.translate.getLangs().includes(this.translate.getBrowserLang())) {
|
if (this.translate.getLangs().includes(this.translate.getBrowserLang())) {
|
||||||
lang = this.translate.getBrowserLang();
|
lang = this.translate.getBrowserLang();
|
||||||
|
33
src/app/core/xsrf/xsrf.constants.ts
Normal file
33
src/app/core/xsrf/xsrf.constants.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* XSRF / CSRF related constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of CSRF/XSRF header we (client) may SEND in requests to backend.
|
||||||
|
* (This is a standard header name for XSRF/CSRF defined by Angular)
|
||||||
|
*/
|
||||||
|
export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of CSRF/XSRF header we (client) may RECEIVE in responses from backend
|
||||||
|
* This header is defined by DSpace backend, see https://github.com/DSpace/RestContract/blob/main/csrf-tokens.md
|
||||||
|
*/
|
||||||
|
export const XSRF_RESPONSE_HEADER = 'DSPACE-XSRF-TOKEN';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of client-side Cookie where we store the CSRF/XSRF token between requests.
|
||||||
|
* This cookie is only available to client, and should be updated whenever a new XSRF_RESPONSE_HEADER
|
||||||
|
* is found in a response from the backend.
|
||||||
|
*/
|
||||||
|
export const XSRF_COOKIE = 'XSRF-TOKEN';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of server-side cookie the backend expects the XSRF token to be in.
|
||||||
|
* When the backend receives a modifying request, it will validate the CSRF/XSRF token by looking
|
||||||
|
* for a match between the XSRF_REQUEST_HEADER and this Cookie. For more details see
|
||||||
|
* https://github.com/DSpace/RestContract/blob/main/csrf-tokens.md
|
||||||
|
*
|
||||||
|
* NOTE: This Cookie is NOT readable to the client/UI. It is only readable to the backend and will
|
||||||
|
* be sent along automatically by the user's browser.
|
||||||
|
*/
|
||||||
|
export const DSPACE_XSRF_COOKIE = 'DSPACE-XSRF-COOKIE';
|
@@ -67,7 +67,7 @@ describe(`XsrfInterceptor`, () => {
|
|||||||
expect(httpRequest.request.headers.has('X-XSRF-TOKEN')).toBeTrue();
|
expect(httpRequest.request.headers.has('X-XSRF-TOKEN')).toBeTrue();
|
||||||
expect(httpRequest.request.withCredentials).toBeTrue();
|
expect(httpRequest.request.withCredentials).toBeTrue();
|
||||||
const token = httpRequest.request.headers.get('X-XSRF-TOKEN');
|
const token = httpRequest.request.headers.get('X-XSRF-TOKEN');
|
||||||
expect(token).toBeDefined();
|
expect(token).not.toBeNull();
|
||||||
expect(token).toBe(testToken.toString());
|
expect(token).toBe(testToken.toString());
|
||||||
|
|
||||||
httpRequest.flush(mockPayload, { status: mockStatusCode, statusText: mockStatusText });
|
httpRequest.flush(mockPayload, { status: mockStatusCode, statusText: mockStatusText });
|
||||||
@@ -116,11 +116,11 @@ describe(`XsrfInterceptor`, () => {
|
|||||||
// ensure mock XSRF token is in response
|
// ensure mock XSRF token is in response
|
||||||
expect(response.headers.has('DSPACE-XSRF-TOKEN')).toBeTrue();
|
expect(response.headers.has('DSPACE-XSRF-TOKEN')).toBeTrue();
|
||||||
const token = response.headers.get('DSPACE-XSRF-TOKEN');
|
const token = response.headers.get('DSPACE-XSRF-TOKEN');
|
||||||
expect(token).toBeDefined();
|
expect(token).not.toBeNull();
|
||||||
expect(token).toBe(mockNewXSRFToken.toString());
|
expect(token).toBe(mockNewXSRFToken.toString());
|
||||||
|
|
||||||
// ensure our XSRF-TOKEN cookie exists & has the same value as the new DSPACE-XSRF-TOKEN header
|
// ensure our XSRF-TOKEN cookie exists & has the same value as the new DSPACE-XSRF-TOKEN header
|
||||||
expect(cookieService.get('XSRF-TOKEN')).toBeDefined();
|
expect(cookieService.get('XSRF-TOKEN')).not.toBeNull();
|
||||||
expect(cookieService.get('XSRF-TOKEN')).toBe(mockNewXSRFToken.toString());
|
expect(cookieService.get('XSRF-TOKEN')).toBe(mockNewXSRFToken.toString());
|
||||||
|
|
||||||
done();
|
done();
|
||||||
@@ -153,7 +153,7 @@ describe(`XsrfInterceptor`, () => {
|
|||||||
expect(error.statusText).toBe(mockErrorText);
|
expect(error.statusText).toBe(mockErrorText);
|
||||||
|
|
||||||
// ensure our XSRF-TOKEN cookie exists & has the same value as the new DSPACE-XSRF-TOKEN header
|
// ensure our XSRF-TOKEN cookie exists & has the same value as the new DSPACE-XSRF-TOKEN header
|
||||||
expect(cookieService.get('XSRF-TOKEN')).toBeDefined();
|
expect(cookieService.get('XSRF-TOKEN')).not.toBeNull();
|
||||||
expect(cookieService.get('XSRF-TOKEN')).toBe(mockNewXSRFToken.toString());
|
expect(cookieService.get('XSRF-TOKEN')).toBe(mockNewXSRFToken.toString());
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
@@ -12,15 +12,7 @@ import { Observable, throwError } from 'rxjs';
|
|||||||
import { tap, catchError } from 'rxjs/operators';
|
import { tap, catchError } from 'rxjs/operators';
|
||||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||||
import { CookieService } from '../services/cookie.service';
|
import { CookieService } from '../services/cookie.service';
|
||||||
|
import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from './xsrf.constants';
|
||||||
// Name of XSRF header we may send in requests to backend (this is a standard name defined by Angular)
|
|
||||||
export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN';
|
|
||||||
// Name of XSRF header we may receive in responses from backend
|
|
||||||
export const XSRF_RESPONSE_HEADER = 'DSPACE-XSRF-TOKEN';
|
|
||||||
// Name of cookie where we store the XSRF token
|
|
||||||
export const XSRF_COOKIE = 'XSRF-TOKEN';
|
|
||||||
// Name of cookie the backend expects the XSRF token to be in
|
|
||||||
export const DSPACE_XSRF_COOKIE = 'DSPACE-XSRF-COOKIE';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Http Interceptor intercepting Http Requests & Responses to
|
* Custom Http Interceptor intercepting Http Requests & Responses to
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<ds-item-page-title-field [item]="object" class="mr-auto">
|
<ds-themed-item-page-title-field [item]="object" class="mr-auto">
|
||||||
</ds-item-page-title-field>
|
</ds-themed-item-page-title-field>
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<ds-item-page-title-field [item]="object" class="mr-auto">
|
<ds-themed-item-page-title-field [item]="object" class="mr-auto">
|
||||||
</ds-item-page-title-field>
|
</ds-themed-item-page-title-field>
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<ds-item-page-title-field [item]="object" class="mr-auto">
|
<ds-themed-item-page-title-field [item]="object" class="mr-auto">
|
||||||
</ds-item-page-title-field>
|
</ds-themed-item-page-title-field>
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<ds-item-page-title-field [item]="object" class="mr-auto">
|
<ds-themed-item-page-title-field [item]="object" class="mr-auto">
|
||||||
</ds-item-page-title-field>
|
</ds-themed-item-page-title-field>
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<ds-item-page-title-field class="mr-auto" [item]="object">
|
<ds-themed-item-page-title-field class="mr-auto" [item]="object">
|
||||||
</ds-item-page-title-field>
|
</ds-themed-item-page-title-field>
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<ds-item-page-title-field [item]="object" class="mr-auto">
|
<ds-themed-item-page-title-field [item]="object" class="mr-auto">
|
||||||
</ds-item-page-title-field>
|
</ds-themed-item-page-title-field>
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -18,12 +18,12 @@
|
|||||||
<!--[fields]="['project.identifier.status']"-->
|
<!--[fields]="['project.identifier.status']"-->
|
||||||
<!--[label]="'project.page.status'">-->
|
<!--[label]="'project.page.status'">-->
|
||||||
<!--</ds-generic-item-page-field>-->
|
<!--</ds-generic-item-page-field>-->
|
||||||
<ds-metadata-representation-list
|
<ds-themed-metadata-representation-list
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[itemType]="'OrgUnit'"
|
[itemType]="'OrgUnit'"
|
||||||
[metadataFields]="['project.contributor.other']"
|
[metadataFields]="['project.contributor.other']"
|
||||||
[label]="'project.page.contributor' | translate">
|
[label]="'project.page.contributor' | translate">
|
||||||
</ds-metadata-representation-list>
|
</ds-themed-metadata-representation-list>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
[fields]="['project.identifier.funder']"
|
[fields]="['project.identifier.funder']"
|
||||||
[label]="'project.page.funder'">
|
[label]="'project.page.funder'">
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<ng-container *ngIf="(site$ | async) as site">
|
<ng-container *ngIf="(site$ | async) as site">
|
||||||
<ds-view-tracker [object]="site"></ds-view-tracker>
|
<ds-view-tracker [object]="site"></ds-view-tracker>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ds-search-form [inPlaceSearch]="false" [searchPlaceholder]="'home.search-form.placeholder' | translate"></ds-search-form>
|
<ds-themed-search-form [inPlaceSearch]="false" [searchPlaceholder]="'home.search-form.placeholder' | translate"></ds-themed-search-form>
|
||||||
<ds-themed-top-level-community-list></ds-themed-top-level-community-list>
|
<ds-themed-top-level-community-list></ds-themed-top-level-community-list>
|
||||||
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
|
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../../../shared/theme-support/themed.component';
|
||||||
|
import { FeedbackFormComponent } from './feedback-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for {@link FeedbackFormComponent}
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-feedback-form',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
export class ThemedFeedbackFormComponent extends ThemedComponent<FeedbackFormComponent> {
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'FeedbackFormComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../../themes/${themeName}/app/info/feedback/feedback-form/feedback-form.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./feedback-form.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,3 +1,3 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<ds-feedback-form></ds-feedback-form>
|
<ds-themed-feedback-form></ds-themed-feedback-form>
|
||||||
</div>
|
</div>
|
@@ -10,6 +10,7 @@ import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end
|
|||||||
import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
|
import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
|
||||||
import { FeedbackComponent } from './feedback/feedback.component';
|
import { FeedbackComponent } from './feedback/feedback.component';
|
||||||
import { FeedbackFormComponent } from './feedback/feedback-form/feedback-form.component';
|
import { FeedbackFormComponent } from './feedback/feedback-form/feedback-form.component';
|
||||||
|
import { ThemedFeedbackFormComponent } from './feedback/feedback-form/themed-feedback-form.component';
|
||||||
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
|
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
|
||||||
import { FeedbackGuard } from '../core/feedback/feedback.guard';
|
import { FeedbackGuard } from '../core/feedback/feedback.guard';
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ const DECLARATIONS = [
|
|||||||
ThemedPrivacyComponent,
|
ThemedPrivacyComponent,
|
||||||
FeedbackComponent,
|
FeedbackComponent,
|
||||||
FeedbackFormComponent,
|
FeedbackFormComponent,
|
||||||
|
ThemedFeedbackFormComponent,
|
||||||
ThemedFeedbackComponent
|
ThemedFeedbackComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
|
30
src/app/item-page/alerts/themed-item-alerts.component.ts
Normal file
30
src/app/item-page/alerts/themed-item-alerts.component.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ItemAlertsComponent } from './item-alerts.component';
|
||||||
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for {@link ItemAlertsComponent}
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-item-alerts',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
export class ThemedItemAlertsComponent extends ThemedComponent<ItemAlertsComponent> {
|
||||||
|
protected inAndOutputNames: (keyof ItemAlertsComponent & keyof this)[] = ['item'];
|
||||||
|
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'ItemAlertsComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../themes/${themeName}/app/item-page/alerts/item-alerts.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./item-alerts.component');
|
||||||
|
}
|
||||||
|
}
|
@@ -27,13 +27,13 @@
|
|||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
<ds-search-form id="search-form"
|
<ds-themed-search-form id="search-form"
|
||||||
[query]="(searchOptions$ | async)?.query"
|
[query]="(searchOptions$ | async)?.query"
|
||||||
[currentUrl]="'./'"
|
[currentUrl]="'./'"
|
||||||
[inPlaceSearch]="true"
|
[inPlaceSearch]="true"
|
||||||
[searchPlaceholder]="'item.edit.item-mapper.search-form.placeholder' | translate"
|
[searchPlaceholder]="'item.edit.item-mapper.search-form.placeholder' | translate"
|
||||||
(submitSearch)="performedSearch = true">
|
(submitSearch)="performedSearch = true">
|
||||||
</ds-search-form>
|
</ds-themed-search-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -0,0 +1,32 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../../../../shared/theme-support/themed.component';
|
||||||
|
import { FullFileSectionComponent } from './full-file-section.component';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for {@link FullFileSectionComponent}
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-item-page-full-file-section',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: './../../../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
export class ThemedFullFileSectionComponent extends ThemedComponent<FullFileSectionComponent> {
|
||||||
|
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
|
protected inAndOutputNames: (keyof FullFileSectionComponent & keyof this)[] = ['item'];
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'FullFileSectionComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../../../themes/${themeName}/app/item-page/full/field-components/file-section/full-file-section.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./full-file-section.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,14 +1,13 @@
|
|||||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="itemRD?.payload as item">
|
<div *ngIf="itemRD?.payload as item">
|
||||||
<ds-item-alerts [item]="item"></ds-item-alerts>
|
<ds-themed-item-alerts [item]="item"></ds-themed-item-alerts>
|
||||||
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
||||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||||
<div *ngIf="!item.isWithdrawn || (isAdmin$|async)" class="full-item-info">
|
<div *ngIf="!item.isWithdrawn || (isAdmin$|async)" class="full-item-info">
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<ds-item-page-title-field class="mr-auto" [item]="item"></ds-item-page-title-field>
|
<ds-themed-item-page-title-field class="mr-auto" [item]="item"></ds-themed-item-page-title-field>
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="simple-view-link my-3" *ngIf="!fromSubmissionObject">
|
<div class="simple-view-link my-3" *ngIf="!fromSubmissionObject">
|
||||||
<a class="btn btn-outline-primary" [routerLink]="[(itemPageRoute$ | async)]">
|
<a class="btn btn-outline-primary" [routerLink]="[(itemPageRoute$ | async)]">
|
||||||
@@ -28,7 +27,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
|
<ds-themed-item-page-full-file-section [item]="item"></ds-themed-item-page-full-file-section>
|
||||||
<ds-item-page-collections [item]="item"></ds-item-page-collections>
|
<ds-item-page-collections [item]="item"></ds-item-page-collections>
|
||||||
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
|
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
|
||||||
<div class="button-row bottom" *ngIf="fromSubmissionObject">
|
<div class="button-row bottom" *ngIf="fromSubmissionObject">
|
||||||
|
@@ -104,9 +104,13 @@ describe('FullItemPageComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.debugElement.nativeElement.remove();
|
||||||
|
});
|
||||||
|
|
||||||
it('should display the item\'s metadata', () => {
|
it('should display the item\'s metadata', () => {
|
||||||
const table = fixture.debugElement.query(By.css('table'));
|
const table = fixture.debugElement.query(By.css('table'));
|
||||||
for (const metadatum of mockItem.allMetadata([])) {
|
for (const metadatum of mockItem.allMetadata(Object.keys(mockItem.metadata))) {
|
||||||
expect(table.nativeElement.innerHTML).toContain(metadatum.value);
|
expect(table.nativeElement.innerHTML).toContain(metadatum.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -137,7 +141,7 @@ describe('FullItemPageComponent', () => {
|
|||||||
|
|
||||||
it('should display the item', () => {
|
it('should display the item', () => {
|
||||||
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
|
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
|
||||||
expect(objectLoader.nativeElement).toBeDefined();
|
expect(objectLoader.nativeElement).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('when the item is withdrawn and the user is not an admin', () => {
|
describe('when the item is withdrawn and the user is not an admin', () => {
|
||||||
@@ -161,7 +165,7 @@ describe('FullItemPageComponent', () => {
|
|||||||
|
|
||||||
it('should display the item', () => {
|
it('should display the item', () => {
|
||||||
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
|
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
|
||||||
expect(objectLoader.nativeElement).toBeDefined();
|
expect(objectLoader).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -173,7 +177,7 @@ describe('FullItemPageComponent', () => {
|
|||||||
|
|
||||||
it('should display the item', () => {
|
it('should display the item', () => {
|
||||||
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
|
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
|
||||||
expect(objectLoader.nativeElement).toBeDefined();
|
expect(objectLoader).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -34,8 +34,11 @@ import { ResearchEntitiesModule } from '../entity-groups/research-entities/resea
|
|||||||
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
||||||
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
||||||
import { MediaViewerComponent } from './media-viewer/media-viewer.component';
|
import { MediaViewerComponent } from './media-viewer/media-viewer.component';
|
||||||
|
import { ThemedMediaViewerComponent } from './media-viewer/themed-media-viewer.component';
|
||||||
import { MediaViewerVideoComponent } from './media-viewer/media-viewer-video/media-viewer-video.component';
|
import { MediaViewerVideoComponent } from './media-viewer/media-viewer-video/media-viewer-video.component';
|
||||||
|
import { ThemedMediaViewerVideoComponent } from './media-viewer/media-viewer-video/themed-media-viewer-video.component';
|
||||||
import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component';
|
import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component';
|
||||||
|
import { ThemedMediaViewerImageComponent } from './media-viewer/media-viewer-image/themed-media-viewer-image.component';
|
||||||
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
|
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
|
||||||
import { MiradorViewerComponent } from './mirador-viewer/mirador-viewer.component';
|
import { MiradorViewerComponent } from './mirador-viewer/mirador-viewer.component';
|
||||||
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
||||||
@@ -53,7 +56,10 @@ import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/
|
|||||||
import { FileSectionComponent } from './simple/field-components/file-section/file-section.component';
|
import { FileSectionComponent } from './simple/field-components/file-section/file-section.component';
|
||||||
import { ItemSharedModule } from './item-shared.module';
|
import { ItemSharedModule } from './item-shared.module';
|
||||||
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
||||||
|
import { ThemedItemAlertsComponent } from './alerts/themed-item-alerts.component';
|
||||||
|
import {
|
||||||
|
ThemedFullFileSectionComponent
|
||||||
|
} from './full/field-components/file-section/themed-full-file-section.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
@@ -76,14 +82,18 @@ const DECLARATIONS = [
|
|||||||
ItemPageFieldComponent,
|
ItemPageFieldComponent,
|
||||||
CollectionsComponent,
|
CollectionsComponent,
|
||||||
FullFileSectionComponent,
|
FullFileSectionComponent,
|
||||||
|
ThemedFullFileSectionComponent,
|
||||||
PublicationComponent,
|
PublicationComponent,
|
||||||
UntypedItemComponent,
|
UntypedItemComponent,
|
||||||
ItemComponent,
|
ItemComponent,
|
||||||
UploadBitstreamComponent,
|
UploadBitstreamComponent,
|
||||||
AbstractIncrementalListComponent,
|
AbstractIncrementalListComponent,
|
||||||
MediaViewerComponent,
|
MediaViewerComponent,
|
||||||
|
ThemedMediaViewerComponent,
|
||||||
MediaViewerVideoComponent,
|
MediaViewerVideoComponent,
|
||||||
|
ThemedMediaViewerVideoComponent,
|
||||||
MediaViewerImageComponent,
|
MediaViewerImageComponent,
|
||||||
|
ThemedMediaViewerImageComponent,
|
||||||
MiradorViewerComponent,
|
MiradorViewerComponent,
|
||||||
VersionPageComponent,
|
VersionPageComponent,
|
||||||
OrcidPageComponent,
|
OrcidPageComponent,
|
||||||
@@ -91,6 +101,7 @@ const DECLARATIONS = [
|
|||||||
OrcidSyncSettingsComponent,
|
OrcidSyncSettingsComponent,
|
||||||
OrcidQueueComponent,
|
OrcidQueueComponent,
|
||||||
ItemAlertsComponent,
|
ItemAlertsComponent,
|
||||||
|
ThemedItemAlertsComponent,
|
||||||
BitstreamRequestACopyPageComponent,
|
BitstreamRequestACopyPageComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -13,6 +13,9 @@ import { MetadataValuesComponent } from './field-components/metadata-values/meta
|
|||||||
import { GenericItemPageFieldComponent } from './simple/field-components/specific-field/generic/generic-item-page-field.component';
|
import { GenericItemPageFieldComponent } from './simple/field-components/specific-field/generic/generic-item-page-field.component';
|
||||||
import { MetadataRepresentationListComponent } from './simple/metadata-representation-list/metadata-representation-list.component';
|
import { MetadataRepresentationListComponent } from './simple/metadata-representation-list/metadata-representation-list.component';
|
||||||
import { RelatedItemsComponent } from './simple/related-items/related-items-component';
|
import { RelatedItemsComponent } from './simple/related-items/related-items-component';
|
||||||
|
import {
|
||||||
|
ThemedMetadataRepresentationListComponent
|
||||||
|
} from './simple/metadata-representation-list/themed-metadata-representation-list.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
ItemVersionsDeleteModalComponent,
|
ItemVersionsDeleteModalComponent,
|
||||||
@@ -27,6 +30,7 @@ const COMPONENTS = [
|
|||||||
MetadataValuesComponent,
|
MetadataValuesComponent,
|
||||||
GenericItemPageFieldComponent,
|
GenericItemPageFieldComponent,
|
||||||
MetadataRepresentationListComponent,
|
MetadataRepresentationListComponent,
|
||||||
|
ThemedMetadataRepresentationListComponent,
|
||||||
RelatedItemsComponent,
|
RelatedItemsComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -1,6 +1,20 @@
|
|||||||
.ngx-gallery {
|
:host ::ng-deep {
|
||||||
display: inline-block;
|
.ngx-gallery {
|
||||||
margin-bottom: 20px;
|
width: unset !important;
|
||||||
width: 340px !important;
|
height: unset !important;
|
||||||
height: 279px !important;
|
}
|
||||||
|
|
||||||
|
ngx-gallery-image {
|
||||||
|
max-width: 340px !important;
|
||||||
|
|
||||||
|
.ngx-gallery-image {
|
||||||
|
background-position: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngx-gallery-image:after {
|
||||||
|
padding-top: 75%;
|
||||||
|
display: block;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,7 @@ export class MediaViewerImageComponent implements OnInit {
|
|||||||
@Input() preview?: boolean;
|
@Input() preview?: boolean;
|
||||||
@Input() image?: string;
|
@Input() image?: string;
|
||||||
|
|
||||||
loggedin: boolean;
|
thumbnailPlaceholder = './assets/images/replacement_image.svg';
|
||||||
|
|
||||||
galleryOptions: NgxGalleryOptions[];
|
galleryOptions: NgxGalleryOptions[];
|
||||||
galleryImages: NgxGalleryImage[];
|
galleryImages: NgxGalleryImage[];
|
||||||
@@ -28,7 +28,10 @@ export class MediaViewerImageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
isAuthenticated$: Observable<boolean>;
|
isAuthenticated$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(private authService: AuthService) {}
|
constructor(
|
||||||
|
protected authService: AuthService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thi method sets up the gallery settings and data
|
* Thi method sets up the gallery settings and data
|
||||||
@@ -69,20 +72,20 @@ export class MediaViewerImageComponent implements OnInit {
|
|||||||
* @param medias input NgxGalleryImage array
|
* @param medias input NgxGalleryImage array
|
||||||
*/
|
*/
|
||||||
convertToGalleryImage(medias: MediaViewerItem[]): NgxGalleryImage[] {
|
convertToGalleryImage(medias: MediaViewerItem[]): NgxGalleryImage[] {
|
||||||
const mappadImages = [];
|
const mappedImages = [];
|
||||||
for (const image of medias) {
|
for (const image of medias) {
|
||||||
if (image.format === 'image') {
|
if (image.format === 'image') {
|
||||||
mappadImages.push({
|
mappedImages.push({
|
||||||
small: image.thumbnail
|
small: image.thumbnail
|
||||||
? image.thumbnail
|
? image.thumbnail
|
||||||
: './assets/images/replacement_image.svg',
|
: this.thumbnailPlaceholder,
|
||||||
medium: image.thumbnail
|
medium: image.thumbnail
|
||||||
? image.thumbnail
|
? image.thumbnail
|
||||||
: './assets/images/replacement_image.svg',
|
: this.thumbnailPlaceholder,
|
||||||
big: image.bitstream._links.content.href,
|
big: image.bitstream._links.content.href,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mappadImages;
|
return mappedImages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,38 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../../../shared/theme-support/themed.component';
|
||||||
|
import { MediaViewerImageComponent } from './media-viewer-image.component';
|
||||||
|
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for {@link MediaViewerImageComponent}.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-media-viewer-image',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
export class ThemedMediaViewerImageComponent extends ThemedComponent<MediaViewerImageComponent> {
|
||||||
|
|
||||||
|
@Input() images: MediaViewerItem[];
|
||||||
|
@Input() preview?: boolean;
|
||||||
|
@Input() image?: string;
|
||||||
|
|
||||||
|
protected inAndOutputNames: (keyof MediaViewerImageComponent & keyof this)[] = [
|
||||||
|
'images',
|
||||||
|
'preview',
|
||||||
|
'image',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'MediaViewerImageComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../../themes/${themeName}/app/item-page/media-viewer/media-viewer-image/media-viewer-image.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./media-viewer-image.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,4 +1,10 @@
|
|||||||
video {
|
video {
|
||||||
width: 340px;
|
width: 100%;
|
||||||
height: 279px;
|
height: auto;
|
||||||
|
max-width: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: .25rem;
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ import { languageHelper } from './language-helper';
|
|||||||
import { CaptionInfo} from './caption-info';
|
import { CaptionInfo} from './caption-info';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This componenet renders a video viewer and playlist for the media viewer
|
* This component renders a video viewer and playlist for the media viewer
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-media-viewer-video',
|
selector: 'ds-media-viewer-video',
|
||||||
@@ -24,8 +24,6 @@ export class MediaViewerVideoComponent implements OnInit {
|
|||||||
audio: './assets/images/replacement_audio.svg',
|
audio: './assets/images/replacement_audio.svg',
|
||||||
};
|
};
|
||||||
|
|
||||||
replacementThumbnail: string;
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.isCollapsed = false;
|
this.isCollapsed = false;
|
||||||
this.filteredMedias = this.medias.filter((media) => media.format === 'audio' || media.format === 'video');
|
this.filteredMedias = this.medias.filter((media) => media.format === 'audio' || media.format === 'video');
|
||||||
|
@@ -0,0 +1,34 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../../../shared/theme-support/themed.component';
|
||||||
|
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
||||||
|
import { MediaViewerVideoComponent } from './media-viewer-video.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for {@link MediaViewerVideoComponent}.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-media-viewer-video',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
export class ThemedMediaViewerVideoComponent extends ThemedComponent<MediaViewerVideoComponent> {
|
||||||
|
|
||||||
|
@Input() medias: MediaViewerItem[];
|
||||||
|
|
||||||
|
protected inAndOutputNames: (keyof MediaViewerVideoComponent & keyof this)[] = [
|
||||||
|
'medias',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'MediaViewerVideoComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../../themes/${themeName}/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./media-viewer-video.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -12,11 +12,11 @@
|
|||||||
mediaList[0]?.format === 'video' || mediaList[0]?.format === 'audio'
|
mediaList[0]?.format === 'video' || mediaList[0]?.format === 'audio'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<ds-media-viewer-video [medias]="mediaList"></ds-media-viewer-video>
|
<ds-themed-media-viewer-video [medias]="mediaList"></ds-themed-media-viewer-video>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="mediaList[0]?.format === 'image'">
|
<ng-container *ngIf="mediaList[0]?.format === 'image'">
|
||||||
<ds-media-viewer-image [images]="mediaList"></ds-media-viewer-image>
|
<ds-themed-media-viewer-image [images]="mediaList"></ds-themed-media-viewer-image>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container
|
<ng-container
|
||||||
@@ -27,10 +27,10 @@
|
|||||||
mediaList.length === 0
|
mediaList.length === 0
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<ds-media-viewer-image
|
<ds-themed-media-viewer-image
|
||||||
[image]="mediaList[0]?.thumbnail || thumbnailPlaceholder"
|
[image]="mediaList[0]?.thumbnail || thumbnailPlaceholder"
|
||||||
[preview]="false"
|
[preview]="false"
|
||||||
></ds-media-viewer-image>
|
></ds-themed-media-viewer-image>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -135,7 +135,7 @@ describe('MediaViewerComponent', () => {
|
|||||||
|
|
||||||
it('should display a default, thumbnail', () => {
|
it('should display a default, thumbnail', () => {
|
||||||
const defaultThumbnail = fixture.debugElement.query(
|
const defaultThumbnail = fixture.debugElement.query(
|
||||||
By.css('ds-media-viewer-image')
|
By.css('ds-themed-media-viewer-image')
|
||||||
);
|
);
|
||||||
expect(defaultThumbnail.nativeElement).toBeDefined();
|
expect(defaultThumbnail.nativeElement).toBeDefined();
|
||||||
});
|
});
|
||||||
|
@@ -13,9 +13,8 @@ import { hasValue } from '../../shared/empty.util';
|
|||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This componenet renders the media viewers
|
* This component renders the media viewers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-media-viewer',
|
selector: 'ds-media-viewer',
|
||||||
templateUrl: './media-viewer.component.html',
|
templateUrl: './media-viewer.component.html',
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user