Merge branch 'main' into CST-6171

This commit is contained in:
Davide Negretti
2022-11-10 17:41:00 +01:00
committed by GitHub
195 changed files with 1806 additions and 1923 deletions

View File

@@ -6,7 +6,8 @@
"eslint-plugin-import", "eslint-plugin-import",
"eslint-plugin-jsdoc", "eslint-plugin-jsdoc",
"eslint-plugin-deprecation", "eslint-plugin-deprecation",
"eslint-plugin-unused-imports" "unused-imports",
"eslint-plugin-lodash"
], ],
"overrides": [ "overrides": [
{ {
@@ -202,7 +203,13 @@
"deprecation/deprecation": "warn", "deprecation/deprecation": "warn",
"import/order": "off", "import/order": "off",
"import/no-deprecated": "warn" "import/no-deprecated": "warn",
"import/no-namespace": "error",
"unused-imports/no-unused-imports": "error",
"lodash/import-scope": [
"error",
"method"
]
} }
}, },
{ {

View File

@@ -6,6 +6,9 @@ name: Build
# Run this Build for all pushes / PRs to current branch # Run this Build for all pushes / PRs to current branch
on: [push, pull_request] on: [push, pull_request]
permissions:
contents: read # to fetch code (actions/checkout)
jobs: jobs:
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -29,11 +32,11 @@ jobs:
steps: steps:
# https://github.com/actions/checkout # https://github.com/actions/checkout
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v2 uses: actions/checkout@v3
# https://github.com/actions/setup-node # https://github.com/actions/setup-node
- name: Install Node.js ${{ matrix.node-version }} - name: Install Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
@@ -58,7 +61,7 @@ jobs:
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache Yarn dependencies - name: Cache Yarn dependencies
uses: actions/cache@v2 uses: actions/cache@v3
with: with:
# Cache entire Yarn cache directory (see previous step) # Cache entire Yarn cache directory (see previous step)
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -85,7 +88,7 @@ jobs:
# Upload coverage reports to Codecov (for one version of Node only) # Upload coverage reports to Codecov (for one version of Node only)
# https://github.com/codecov/codecov-action # https://github.com/codecov/codecov-action
- name: Upload coverage to Codecov.io - name: Upload coverage to Codecov.io
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v3
if: matrix.node-version == '16.x' if: matrix.node-version == '16.x'
# Using docker-compose start backend using CI configuration # Using docker-compose start backend using CI configuration
@@ -100,7 +103,7 @@ jobs:
# https://github.com/cypress-io/github-action # https://github.com/cypress-io/github-action
# (NOTE: to run these e2e tests locally, just use 'ng e2e') # (NOTE: to run these e2e tests locally, just use 'ng e2e')
- name: Run e2e tests (integration tests) - name: Run e2e tests (integration tests)
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v4
with: with:
# Run tests in Chrome, headless mode # Run tests in Chrome, headless mode
browser: chrome browser: chrome
@@ -116,7 +119,7 @@ jobs:
# Cypress always creates a video of all e2e tests (whether they succeeded or failed) # Cypress always creates a video of all e2e tests (whether they succeeded or failed)
# Save those in an Artifact # Save those in an Artifact
- name: Upload e2e test videos to Artifacts - name: Upload e2e test videos to Artifacts
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: always() if: always()
with: with:
name: e2e-test-videos name: e2e-test-videos
@@ -125,7 +128,7 @@ jobs:
# If e2e tests fail, Cypress creates a screenshot of what happened # If e2e tests fail, Cypress creates a screenshot of what happened
# Save those in an Artifact # Save those in an Artifact
- name: Upload e2e test failure screenshots to Artifacts - name: Upload e2e test failure screenshots to Artifacts
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: failure() if: failure()
with: with:
name: e2e-test-screenshots name: e2e-test-screenshots

View File

@@ -12,6 +12,9 @@ on:
- 'dspace-**' - 'dspace-**'
pull_request: pull_request:
permissions:
contents: read # to fetch code (actions/checkout)
jobs: jobs:
docker: docker:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular' # Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
@@ -39,11 +42,11 @@ jobs:
steps: steps:
# https://github.com/actions/checkout # https://github.com/actions/checkout
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v2 uses: actions/checkout@v3
# https://github.com/docker/setup-buildx-action # https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx - name: Setup Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
# https://github.com/docker/setup-qemu-action # https://github.com/docker/setup-qemu-action
- name: Set up QEMU emulation to build for multiple architectures - name: Set up QEMU emulation to build for multiple architectures
@@ -53,7 +56,7 @@ jobs:
- name: Login to DockerHub - name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push # Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }} password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
@@ -65,7 +68,7 @@ jobs:
# Get Metadata for docker_build step below # Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image - name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image
id: meta_build id: meta_build
uses: docker/metadata-action@v3 uses: docker/metadata-action@v4
with: with:
images: dspace/dspace-angular images: dspace/dspace-angular
tags: ${{ env.IMAGE_TAGS }} tags: ${{ env.IMAGE_TAGS }}
@@ -74,7 +77,7 @@ jobs:
# https://github.com/docker/build-push-action # https://github.com/docker/build-push-action
- name: Build and push 'dspace-angular' image - name: Build and push 'dspace-angular' image
id: docker_build id: docker_build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile

View File

@@ -5,25 +5,22 @@ on:
issues: issues:
types: [opened] types: [opened]
permissions: {}
jobs: jobs:
automation: automation:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# Add the new issue to a project board, if it needs triage # Add the new issue to a project board, if it needs triage
# See https://github.com/marketplace/actions/create-project-card-action # See https://github.com/actions/add-to-project
- name: Add issue to project board - name: Add issue to triage board
# Only add to project board if issue is flagged as "needs triage" or has no labels # Only add to project board if issue is flagged as "needs triage" or has no labels
# NOTE: By default we flag new issues as "needs triage" in our issue template # NOTE: By default we flag new issues as "needs triage" in our issue template
if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '') if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '')
uses: technote-space/create-project-card-action@v1 uses: actions/add-to-project@v0.3.0
# Note, the authentication token below is an ORG level Secret. # Note, the authentication token below is an ORG level Secret.
# It must be created/recreated manually via a personal access token with "public_repo" and "admin:org" permissions # It must be created/recreated manually via a personal access token with admin:org, project, public_repo permissions
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token
# This is necessary because the "DSpace Backlog" project is an org level project (i.e. not repo specific) # This is necessary because the "DSpace Backlog" project is an org level project (i.e. not repo specific)
with: with:
GITHUB_TOKEN: ${{ secrets.ORG_PROJECT_TOKEN }} github-token: ${{ secrets.TRIAGE_PROJECT_TOKEN }}
PROJECT: DSpace Backlog project-url: https://github.com/orgs/DSpace/projects/24
COLUMN: Triage
CHECK_ORG_PROJECT: true
# Ignore errors
continue-on-error: true

View File

@@ -5,21 +5,32 @@ name: Check for merge conflicts
# NOTE: This means merge conflicts are only checked for when a PR is merged to main. # NOTE: This means merge conflicts are only checked for when a PR is merged to main.
on: on:
push: push:
branches: branches: [ main ]
- main # So that the `conflict_label_name` is removed if conflicts are resolved,
# we allow this to run for `pull_request_target` so that github secrets are available.
pull_request_target:
types: [ synchronize ]
permissions: {}
jobs: jobs:
triage: triage:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
if: github.repository == 'dspace/dspace-angular'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pull-requests: write
steps: steps:
# See: https://github.com/mschilde/auto-label-merge-conflicts/ # See: https://github.com/prince-chrismc/label-merge-conflicts-action
- name: Auto-label PRs with merge conflicts - name: Auto-label PRs with merge conflicts
uses: mschilde/auto-label-merge-conflicts@v2.0 uses: prince-chrismc/label-merge-conflicts-action@v2
# Add "merge conflict" label if a merge conflict is detected. Remove it when resolved. # Add "merge conflict" label if a merge conflict is detected. Remove it when resolved.
# Note, the authentication token is created automatically # Note, the authentication token is created automatically
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token
with: with:
CONFLICT_LABEL_NAME: 'merge conflict' conflict_label_name: 'merge conflict'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
# Ignore errors conflict_comment: |
continue-on-error: true Hi @${author},
Conflicts have been detected against the base branch.
Please [resolve these conflicts](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/about-merge-conflicts) as soon as you can. Thanks!

View File

@@ -4,10 +4,11 @@ import { testA11y } from 'cypress/support/utils';
describe('My DSpace page', () => { describe('My DSpace page', () => {
it('should display recent submissions and pass accessibility tests', () => { it('should display recent submissions and pass accessibility tests', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
cy.visit('/mydspace'); cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
cy.get('ds-my-dspace-page').should('exist'); cy.get('ds-my-dspace-page').should('exist');
// At least one recent submission should be displayed // At least one recent submission should be displayed
@@ -36,10 +37,11 @@ describe('My DSpace page', () => {
}); });
it('should have a working detailed view that passes accessibility tests', () => { it('should have a working detailed view that passes accessibility tests', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
cy.visit('/mydspace'); cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
cy.get('ds-my-dspace-page').should('exist'); cy.get('ds-my-dspace-page').should('exist');
// Click button in sidebar to display detailed view // Click button in sidebar to display detailed view
@@ -61,9 +63,11 @@ describe('My DSpace page', () => {
// NOTE: Deleting existing submissions is exercised by submission.spec.ts // NOTE: Deleting existing submissions is exercised by submission.spec.ts
it('should let you start a new submission & edit in-progress submissions', () => { it('should let you start a new submission & edit in-progress submissions', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
cy.visit('/mydspace'); cy.visit('/mydspace');
// 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);
// Open the New Submission dropdown // Open the New Submission dropdown
cy.get('button[data-test="submission-dropdown"]').click(); cy.get('button[data-test="submission-dropdown"]').click();
// Click on the "Item" type in that dropdown // Click on the "Item" type in that dropdown
@@ -131,9 +135,11 @@ describe('My DSpace page', () => {
}); });
it('should let you import from external sources', () => { it('should let you import from external sources', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
cy.visit('/mydspace'); cy.visit('/mydspace');
// 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);
// Open the New Import dropdown // Open the New Import dropdown
cy.get('button[data-test="import-dropdown"]').click(); cy.get('button[data-test="import-dropdown"]').click();
// Click on the "Item" type in that dropdown // Click on the "Item" type in that dropdown

View File

@@ -6,11 +6,12 @@ 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', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
// 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=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
// Should redirect to /workspaceitems, as we've started a new submission // Should redirect to /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems'); cy.url().should('include', '/workspaceitems');
@@ -33,11 +34,12 @@ 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', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
// Create a new submission // Create a new submission
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none'); cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
// Attempt an immediate deposit without filling out any fields // Attempt an immediate deposit without filling out any fields
cy.get('button#deposit').click(); cy.get('button#deposit').click();
@@ -92,11 +94,12 @@ 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', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
// Create a new submission // Create a new submission
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none'); cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
// Fill out all required fields (Title, Date) // Fill out all required fields (Title, Date)
cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests'); cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests');
cy.get('input#dc_date_issued_year').type('2022'); cy.get('input#dc_date_issued_year').type('2022');

View File

@@ -19,6 +19,14 @@ declare global {
* @param password password to login as * @param password password to login as
*/ */
login(email: string, password: string): typeof login; login(email: string, password: string): typeof login;
/**
* Login via form before accessing the next page. Useful to fill out login
* form when a cy.visit() call is to an a page which requires authentication.
* @param email email to login as
* @param password password to login as
*/
loginViaForm(email: string, password: string): typeof loginViaForm;
} }
} }
} }
@@ -26,6 +34,8 @@ declare global {
/** /**
* Login user via REST API directly, and pass authentication token to UI via * Login user via REST API directly, and pass authentication token to UI via
* the UI's dsAuthInfo cookie. * the UI's dsAuthInfo cookie.
* WARNING: WHILE THIS METHOD WORKS, OCCASIONALLY RANDOM AUTHENTICATION ERRORS OCCUR.
* At this time "loginViaForm()" seems more consistent/stable.
* @param email email to login as * @param email email to login as
* @param password password to login as * @param password password to login as
*/ */
@@ -81,3 +91,20 @@ function login(email: string, password: string): void {
} }
// 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
* @param email email to login as
* @param password password to login as
*/
function loginViaForm(email: string, password: string): void {
// Enter email
cy.get('ds-log-in [data-test="email"]').type(email);
// Enter password
cy.get('ds-log-in [data-test="password"]').type(password);
// Click login button
cy.get('ds-log-in [data-test="login-button"]').click();
}
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
Cypress.Commands.add('loginViaForm', loginViaForm);

View File

@@ -89,6 +89,8 @@
"compression": "^1.7.4", "compression": "^1.7.4",
"cookie-parser": "1.4.5", "cookie-parser": "1.4.5",
"core-js": "^3.7.0", "core-js": "^3.7.0",
"date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"express": "^4.17.1", "express": "^4.17.1",
"express-rate-limit": "^5.1.3", "express-rate-limit": "^5.1.3",
@@ -103,20 +105,18 @@
"json5": "^2.1.3", "json5": "^2.1.3",
"jsonschema": "1.4.0", "jsonschema": "1.4.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"klaro": "^0.7.10", "klaro": "^0.7.18",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"markdown-it-mathjax3": "^4.3.1", "markdown-it-mathjax3": "^4.3.1",
"mirador": "^3.3.0", "mirador": "^3.3.0",
"mirador-dl-plugin": "^0.13.0", "mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.11.0", "mirador-share-plugin": "^0.11.0",
"moment": "^2.29.4",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ng-mocks": "^13.1.1", "ng-mocks": "^13.1.1",
"ng2-file-upload": "1.4.0", "ng2-file-upload": "1.4.0",
"ng2-nouislider": "^1.8.3", "ng2-nouislider": "^1.8.3",
"ngx-infinite-scroll": "^10.0.1", "ngx-infinite-scroll": "^10.0.1",
"ngx-moment": "^5.0.0",
"ngx-pagination": "5.0.0", "ngx-pagination": "5.0.0",
"ngx-sortablejs": "^11.1.0", "ngx-sortablejs": "^11.1.0",
"ngx-ui-switch": "^11.0.1", "ngx-ui-switch": "^11.0.1",
@@ -162,14 +162,14 @@
"@types/sanitize-html": "^2.6.2", "@types/sanitize-html": "^2.6.2",
"@typescript-eslint/eslint-plugin": "5.11.0", "@typescript-eslint/eslint-plugin": "5.11.0",
"@typescript-eslint/parser": "5.11.0", "@typescript-eslint/parser": "5.11.0",
"axe-core": "^4.3.3", "axe-core": "^4.4.3",
"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",
"css-loader": "^6.2.0", "css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "^3.4.1", "css-minimizer-webpack-plugin": "^3.4.1",
"cssnano": "^5.0.6", "cssnano": "^5.0.6",
"cypress": "9.5.1", "cypress": "9.7.0",
"cypress-axe": "^0.14.0", "cypress-axe": "^0.14.0",
"debug-loader": "^0.0.1", "debug-loader": "^0.0.1",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
@@ -178,6 +178,7 @@
"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": "^38.0.6", "eslint-plugin-jsdoc": "^38.0.6",
"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",
"fork-ts-checker-webpack-plugin": "^6.0.3", "fork-ts-checker-webpack-plugin": "^6.0.3",

View File

@@ -1,4 +1,4 @@
import * as fs from 'fs'; import { existsSync, writeFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { AppConfig } from '../src/config/app-config.interface'; import { AppConfig } from '../src/config/app-config.interface';
@@ -16,7 +16,7 @@ const appConfig: AppConfig = buildAppConfig();
const angularJsonPath = join(process.cwd(), 'angular.json'); const angularJsonPath = join(process.cwd(), 'angular.json');
if (!fs.existsSync(angularJsonPath)) { if (!existsSync(angularJsonPath)) {
console.error(`Error:\n${angularJsonPath} does not exist\n`); console.error(`Error:\n${angularJsonPath} does not exist\n`);
process.exit(1); process.exit(1);
} }
@@ -30,7 +30,7 @@ try {
angularJson.projects['dspace-angular'].architect.build.options.baseHref = baseHref; angularJson.projects['dspace-angular'].architect.build.options.baseHref = baseHref;
fs.writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n'); writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n');
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }

View File

@@ -1,5 +1,5 @@
import * as fs from 'fs'; import { existsSync, writeFileSync } from 'fs';
import * as yaml from 'js-yaml'; import { dump } from 'js-yaml';
import { join } from 'path'; import { join } from 'path';
/** /**
@@ -18,7 +18,7 @@ if (args[0] === undefined) {
const envFullPath = join(process.cwd(), args[0]); const envFullPath = join(process.cwd(), args[0]);
if (!fs.existsSync(envFullPath)) { if (!existsSync(envFullPath)) {
console.error(`Error:\n${envFullPath} does not exist\n`); console.error(`Error:\n${envFullPath} does not exist\n`);
process.exit(1); process.exit(1);
} }
@@ -26,10 +26,10 @@ if (!fs.existsSync(envFullPath)) {
try { try {
const env = require(envFullPath).environment; const env = require(envFullPath).environment;
const config = yaml.dump(env); const config = dump(env);
if (args[1]) { if (args[1]) {
const ymlFullPath = join(process.cwd(), args[1]); const ymlFullPath = join(process.cwd(), args[1]);
fs.writeFileSync(ymlFullPath, config); writeFileSync(ymlFullPath, config);
} else { } else {
console.log(config); console.log(config);
} }

View File

@@ -1,4 +1,4 @@
import * as child from 'child_process'; import { spawn } from 'child_process';
import { AppConfig } from '../src/config/app-config.interface'; import { AppConfig } from '../src/config/app-config.interface';
import { buildAppConfig } from '../src/config/config.server'; import { buildAppConfig } from '../src/config/config.server';
@@ -9,7 +9,7 @@ const appConfig: AppConfig = buildAppConfig();
* Calls `ng serve` with the following arguments configured for the UI in the app config: host, port, nameSpace, ssl * Calls `ng serve` with the following arguments configured for the UI in the app config: host, port, nameSpace, ssl
* Any CLI arguments given to this script are patched through to `ng serve` as well. * Any CLI arguments given to this script are patched through to `ng serve` as well.
*/ */
child.spawn( spawn(
`ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl} ${process.argv.slice(2).join(' ')} --configuration development`, `ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl} ${process.argv.slice(2).join(' ')} --configuration development`,
{ stdio: 'inherit', shell: true } { stdio: 'inherit', shell: true }
); );

View File

@@ -1,5 +1,5 @@
import * as http from 'http'; import { request } from 'http';
import * as https from 'https'; import { request as https_request } from 'https';
import { AppConfig } from '../src/config/app-config.interface'; import { AppConfig } from '../src/config/app-config.interface';
import { buildAppConfig } from '../src/config/config.server'; import { buildAppConfig } from '../src/config/config.server';
@@ -20,7 +20,7 @@ console.log(`...Testing connection to REST API at ${restUrl}...\n`);
// If SSL enabled, test via HTTPS, else via HTTP // If SSL enabled, test via HTTPS, else via HTTP
if (appConfig.rest.ssl) { if (appConfig.rest.ssl) {
const req = https.request(restUrl, (res) => { const req = https_request(restUrl, (res) => {
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`); console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
// We will keep reading data until the 'end' event fires. // We will keep reading data until the 'end' event fires.
// This ensures we don't just read the first chunk. // This ensures we don't just read the first chunk.
@@ -39,7 +39,7 @@ if (appConfig.rest.ssl) {
req.end(); req.end();
} else { } else {
const req = http.request(restUrl, (res) => { const req = request(restUrl, (res) => {
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`); console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
// We will keep reading data until the 'end' event fires. // We will keep reading data until the 'end' event fires.
// This ensures we don't just read the first chunk. // This ensures we don't just read the first chunk.

View File

@@ -19,14 +19,17 @@ import 'zone.js/node';
import 'reflect-metadata'; import 'reflect-metadata';
import 'rxjs'; import 'rxjs';
import axios from 'axios'; /* eslint-disable import/no-namespace */
import * as pem from 'pem';
import * as https from 'https';
import * as morgan from 'morgan'; import * as morgan from 'morgan';
import * as express from 'express'; import * as express from 'express';
import * as bodyParser from 'body-parser';
import * as compression from 'compression'; import * as compression from 'compression';
import * as expressStaticGzip from 'express-static-gzip'; import * as expressStaticGzip from 'express-static-gzip';
/* eslint-enable import/no-namespace */
import axios from 'axios';
import { createCertificate } from 'pem';
import { createServer } from 'https';
import { json } from 'body-parser';
import { existsSync, readFileSync } from 'fs'; import { existsSync, readFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
@@ -110,7 +113,7 @@ export function app() {
* Add parser for request bodies * Add parser for request bodies
* See [morgan](https://github.com/expressjs/body-parser) * See [morgan](https://github.com/expressjs/body-parser)
*/ */
server.use(bodyParser.json()); server.use(json());
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', (_, options, callback) => server.engine('html', (_, options, callback) =>
@@ -266,7 +269,7 @@ function serverStarted() {
* @param keys SSL credentials * @param keys SSL credentials
*/ */
function createHttpsServer(keys) { function createHttpsServer(keys) {
https.createServer({ createServer({
key: keys.serviceKey, key: keys.serviceKey,
cert: keys.certificate cert: keys.certificate
}, app).listen(environment.ui.port, environment.ui.host, () => { }, app).listen(environment.ui.port, environment.ui.host, () => {
@@ -320,7 +323,7 @@ function start() {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation] process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation]
pem.createCertificate({ createCertificate({
days: 1, days: 1,
selfSigned: true selfSigned: true
}, (error, keys) => { }, (error, keys) => {

View File

@@ -1,12 +1,8 @@
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service';
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service'; import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model'; import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';

View File

@@ -13,13 +13,14 @@
[paginationOptions]="pageConfig" [paginationOptions]="pageConfig"
[pageInfoState]="(bitstreamFormats | async)?.payload" [pageInfoState]="(bitstreamFormats | async)?.payload"
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements" [collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="false"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">
<div class="table-responsive"> <div class="table-responsive">
<table id="formats" class="table table-striped table-hover"> <table id="formats" class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col"></th> <th scope="col"></th>
<th scope="col">{{'admin.registries.bitstream-formats.table.id' | translate}}</th>
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th> <th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th> <th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th> <th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
@@ -35,6 +36,7 @@
> >
</label> </label>
</td> </td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td> <td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td> <td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td> <td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>

View File

@@ -129,16 +129,19 @@ describe('BitstreamFormatsComponent', () => {
}); });
it('should contain the correct formats', () => { it('should contain the correct formats', () => {
const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement; const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(3)')).nativeElement;
expect(unknownName.textContent).toBe('Unknown'); expect(unknownName.textContent).toBe('Unknown');
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(2)')).nativeElement; const UUID: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
expect(UUID.textContent).toBe('test-uuid-1');
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(3)')).nativeElement;
expect(licenseName.textContent).toBe('License'); expect(licenseName.textContent).toBe('License');
const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(2)')).nativeElement; const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(3)')).nativeElement;
expect(ccLicenseName.textContent).toBe('CC License'); expect(ccLicenseName.textContent).toBe('CC License');
const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(2)')).nativeElement; const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(3)')).nativeElement;
expect(adobeName.textContent).toBe('Adobe PDF'); expect(adobeName.textContent).toBe('Adobe PDF');
}); });
}); });

View File

@@ -1,12 +1,11 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { combineLatest as observableCombineLatest, Observable, zip } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable} from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginatedList } from '../../../core/data/paginated-list.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service'; import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
import { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators'; import { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators';
import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@@ -29,21 +28,14 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
*/ */
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>; bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
/**
* The current pagination configuration for the page used by the FindAll method
* Currently simply renders all bitstream formats
*/
config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 20
});
/** /**
* The current pagination configuration for the page * The current pagination configuration for the page
* Currently simply renders all bitstream formats * Currently simply renders all bitstream formats
*/ */
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'rbp', id: 'rbp',
pageSize: 20 pageSize: 20,
pageSizeOptions: [20, 40, 60, 80, 100]
}); });
constructor(private notificationsService: NotificationsService, constructor(private notificationsService: NotificationsService,
@@ -149,7 +141,7 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe( this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
switchMap((findListOptions: FindListOptions) => { switchMap((findListOptions: FindListOptions) => {
return this.bitstreamFormatService.findAll(findListOptions); return this.bitstreamFormatService.findAll(findListOptions);
}) })

View File

@@ -19,10 +19,7 @@ import { RestResponse } from '../../../core/cache/response.models';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../../core/data/find-list-options.model';
describe('MetadataRegistryComponent', () => { describe('MetadataRegistryComponent', () => {
let comp: MetadataRegistryComponent; let comp: MetadataRegistryComponent;

View File

@@ -25,6 +25,7 @@
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
<th scope="col">{{'admin.registries.schema.fields.table.id' | translate}}</th>
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th> <th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th> <th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
</tr> </tr>
@@ -39,6 +40,7 @@
(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)">{{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">.</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>

View File

@@ -23,11 +23,8 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { MetadataField } from '../../../core/metadata/metadata-field.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { VarDirective } from '../../../shared/utils/var.directive'; import { VarDirective } from '../../../shared/utils/var.directive';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../../core/data/find-list-options.model';
describe('MetadataSchemaComponent', () => { describe('MetadataSchemaComponent', () => {
let comp: MetadataSchemaComponent; let comp: MetadataSchemaComponent;
@@ -169,10 +166,10 @@ describe('MetadataSchemaComponent', () => {
}); });
it('should contain the correct fields', () => { it('should contain the correct fields', () => {
const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(2)')).nativeElement; const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(3)')).nativeElement;
expect(editorField.textContent).toBe('mock.contributor.editor'); expect(editorField.textContent).toBe('mock.contributor.editor');
const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(2)')).nativeElement; const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(3)')).nativeElement;
expect(illustratorField.textContent).toBe('mock.contributor.illustrator'); expect(illustratorField.textContent).toBe('mock.contributor.illustrator');
}); });

View File

@@ -16,7 +16,6 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import createSpy = jasmine.createSpy; import createSpy = jasmine.createSpy;
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';

View File

@@ -31,7 +31,6 @@ import { models } from './core/core.module';
import { ThemeService } from './shared/theme-support/theme.service'; import { ThemeService } from './shared/theme-support/theme.service';
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component'; import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
import { distinctNext } from './core/shared/distinct-next'; import { distinctNext } from './core/shared/distinct-next';
import { ModalBeforeDismiss } from './shared/interfaces/modal-before-dismiss.interface';
@Component({ @Component({
selector: 'ds-app', selector: 'ds-app',

View File

@@ -1,4 +1,4 @@
import * as fromRouter from '@ngrx/router-store'; import { routerReducer, RouterReducerState } from '@ngrx/router-store';
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store'; import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
import { import {
ePeopleRegistryReducer, ePeopleRegistryReducer,
@@ -53,7 +53,7 @@ import { MenusState } from './shared/menu/menus-state.model';
import { correlationIdReducer } from './correlation-id/correlation-id.reducer'; import { correlationIdReducer } from './correlation-id/correlation-id.reducer';
export interface AppState { export interface AppState {
router: fromRouter.RouterReducerState; router: RouterReducerState;
hostWindow: HostWindowState; hostWindow: HostWindowState;
forms: FormState; forms: FormState;
metadataRegistry: MetadataRegistryState; metadataRegistry: MetadataRegistryState;
@@ -75,7 +75,7 @@ export interface AppState {
} }
export const appReducers: ActionReducerMap<AppState> = { export const appReducers: ActionReducerMap<AppState> = {
router: fromRouter.routerReducer, router: routerReducer,
hostWindow: hostWindowReducer, hostWindow: hostWindowReducer,
forms: formReducer, forms: formReducer,
metadataRegistry: metadataRegistryReducer, metadataRegistry: metadataRegistryReducer,

View File

@@ -26,7 +26,7 @@ import {
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model'; import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
import { cloneDeep } from 'lodash'; import cloneDeep from 'lodash/cloneDeep';
import { BitstreamDataService } from '../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../core/data/bitstream-data.service';
import { import {
getAllSucceededRemoteDataPayload, getAllSucceededRemoteDataPayload,

View File

@@ -1,5 +1,5 @@
import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
import { of as observableOf, EMPTY } from 'rxjs'; import { EMPTY } from 'rxjs';
import { BitstreamDataService } from '../core/data/bitstream-data.service'; import { BitstreamDataService } from '../core/data/bitstream-data.service';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';

View File

@@ -1,7 +1,6 @@
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { BrowseByGuard } from './browse-by-guard'; import { BrowseByGuard } from './browse-by-guard';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { BrowseDefinition } from '../core/shared/browse-definition.model'; import { BrowseDefinition } from '../core/shared/browse-definition.model';
import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator'; import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator';

View File

@@ -16,7 +16,7 @@ import { Collection } from '../../core/shared/collection.model';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
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 { ChangeDetectionStrategy, EventEmitter } from '@angular/core'; import { EventEmitter } from '@angular/core';
import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
@@ -41,7 +41,7 @@ import {
} from '../../shared/remote-data.utils'; } from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { MyDSpacePageComponent, SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub'; import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
import { GroupDataService } from '../../core/eperson/group-data.service'; import { GroupDataService } from '../../core/eperson/group-data.service';
import { LinkHeadService } from '../../core/services/link-head.service'; import { LinkHeadService } from '../../core/services/link-head.service';

View File

@@ -11,11 +11,11 @@
</div> </div>
<div> <div>
<span class="font-weight-bold">{{'collection.source.controls.harvest.last' | translate}}</span> <span class="font-weight-bold">{{'collection.source.controls.harvest.last' | translate}}</span>
<span>{{contentSource?.message ? contentSource?.message : 'collection.source.controls.harvest.no-information'|translate }}</span> <span>{{contentSource?.lastHarvested ? contentSource?.lastHarvested : 'collection.source.controls.harvest.no-information'|translate }}</span>
</div> </div>
<div> <div>
<span class="font-weight-bold">{{'collection.source.controls.harvest.message' | translate}}</span> <span class="font-weight-bold">{{'collection.source.controls.harvest.message' | translate}}</span>
<span>{{contentSource?.lastHarvested ? contentSource?.lastHarvested : 'collection.source.controls.harvest.no-information'|translate }}</span> <span>{{contentSource?.message ? contentSource?.message: 'collection.source.controls.harvest.no-information'|translate }}</span>
</div> </div>
<button *ngIf="!(testConfigRunning$ |async)" class="btn btn-secondary" <button *ngIf="!(testConfigRunning$ |async)" class="btn btn-secondary"

View File

@@ -8,8 +8,7 @@ import {
DynamicInputModel, DynamicInputModel,
DynamicOptionControlModel, DynamicOptionControlModel,
DynamicRadioGroupModel, DynamicRadioGroupModel,
DynamicSelectModel, DynamicSelectModel
DynamicTextAreaModel
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@@ -23,7 +22,7 @@ import { RemoteData } from '../../../core/data/remote-data';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { first, map, switchMap, take } from 'rxjs/operators'; import { first, map, switchMap, take } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { cloneDeep } from 'lodash'; import cloneDeep from 'lodash/cloneDeep';
import { CollectionDataService } from '../../../core/data/collection-data.service'; import { CollectionDataService } from '../../../core/data/collection-data.service';
import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { MetadataConfig } from '../../../core/shared/metadata-config.model'; import { MetadataConfig } from '../../../core/shared/metadata-config.model';

View File

@@ -17,9 +17,6 @@ import { PageInfo } from '../../core/shared/page-info.model';
import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { of as observableOf } from 'rxjs';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../shared/theme-support/theme.service'; import { ThemeService } from '../../shared/theme-support/theme.service';
@@ -29,7 +26,6 @@ import { GroupDataService } from '../../core/eperson/group-data.service';
import { LinkHeadService } from '../../core/services/link-head.service'; import { LinkHeadService } from '../../core/services/link-head.service';
import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { SearchServiceStub } from '../../shared/testing/search-service.stub';
import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub'; import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';

View File

@@ -17,9 +17,6 @@ import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { of as observableOf } from 'rxjs';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../shared/theme-support/theme.service'; import { ThemeService } from '../../shared/theme-support/theme.service';

View File

@@ -7,7 +7,6 @@ import { createSelector } from '@ngrx/store';
* notation packages up all of the exports into a single object. * notation packages up all of the exports into a single object.
*/ */
import { AuthState } from './auth.reducer'; import { AuthState } from './auth.reducer';
import { AppState } from '../../app.reducer';
import { CoreState } from '../core-state.model'; import { CoreState } from '../core-state.model';
import { coreSelector } from '../core.selectors'; import { coreSelector } from '../core.selectors';

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze'; import * as deepFreeze from 'deep-freeze';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze'; import * as deepFreeze from 'deep-freeze';
import { RemoveFromObjectCacheAction } from './object-cache.actions'; import { RemoveFromObjectCacheAction } from './object-cache.actions';
import { serverSyncBufferReducer } from './server-sync-buffer.reducer'; import { serverSyncBufferReducer } from './server-sync-buffer.reducer';

View File

@@ -3,7 +3,7 @@ import { ChangeAnalyzer } from './change-analyzer';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { MetadataMap } from '../shared/metadata.models'; import { MetadataMap } from '../shared/metadata.models';
import { cloneDeep } from 'lodash'; import cloneDeep from 'lodash/cloneDeep';
/** /**
* A class to determine what differs between two * A class to determine what differs between two

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze'; import * as deepFreeze from 'deep-freeze';
import { import {
AddFieldUpdateAction, AddFieldUpdateAction,

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze'; import * as deepFreeze from 'deep-freeze';
import { import {
RequestConfigureAction, RequestConfigureAction,

View File

@@ -4,7 +4,7 @@ import { HttpHeaders } from '@angular/common/http';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { filter, map, take, tap } from 'rxjs/operators'; import { filter, map, take, tap } from 'rxjs/operators';
import { cloneDeep } from 'lodash'; import cloneDeep from 'lodash/cloneDeep';
import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../../shared/empty.util';
import { ObjectCacheEntry } from '../cache/object-cache.reducer'; import { ObjectCacheEntry } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze'; import * as deepFreeze from 'deep-freeze';
import { indexReducer, MetaIndexState } from './index.reducer'; import { indexReducer, MetaIndexState } from './index.reducer';

View File

@@ -3,7 +3,6 @@ import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { coreSelector } from '../core.selectors'; import { coreSelector } from '../core.selectors';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { IndexState, MetaIndexState } from './index.reducer'; import { IndexState, MetaIndexState } from './index.reducer';
import * as parse from 'url-parse';
import { IndexName } from './index-name.model'; import { IndexName } from './index-name.model';
import { CoreState } from '../core-state.model'; import { CoreState } from '../core-state.model';
@@ -21,9 +20,10 @@ import { CoreState } from '../core-state.model';
*/ */
export const getUrlWithoutEmbedParams = (url: string): string => { export const getUrlWithoutEmbedParams = (url: string): string => {
if (isNotEmpty(url)) { if (isNotEmpty(url)) {
const parsed = parse(url); try {
if (isNotEmpty(parsed.query)) { const parsed = new URL(url);
const parts = parsed.query.split(/[?|&]/) if (isNotEmpty(parsed.search)) {
const parts = parsed.search.split(/[?|&]/)
.filter((part: string) => isNotEmpty(part)) .filter((part: string) => isNotEmpty(part))
.filter((part: string) => !(part.startsWith('embed=') || part.startsWith('embed.size='))); .filter((part: string) => !(part.startsWith('embed=') || part.startsWith('embed.size=')));
let args = ''; let args = '';
@@ -33,6 +33,9 @@ export const getUrlWithoutEmbedParams = (url: string): string => {
url = new URLCombiner(parsed.origin, parsed.pathname, args).toString(); url = new URLCombiner(parsed.origin, parsed.pathname, args).toString();
return url; return url;
} }
} catch (e) {
// Ignore parsing errors. By default, we return the original string below.
}
} }
return url; return url;
@@ -44,9 +47,10 @@ export const getUrlWithoutEmbedParams = (url: string): string => {
*/ */
export const getEmbedSizeParams = (url: string): { name: string, size: number }[] => { export const getEmbedSizeParams = (url: string): { name: string, size: number }[] => {
if (isNotEmpty(url)) { if (isNotEmpty(url)) {
const parsed = parse(url); try {
if (isNotEmpty(parsed.query)) { const parsed = new URL(url);
return parsed.query.split(/[?|&]/) if (isNotEmpty(parsed.search)) {
return parsed.search.split(/[?|&]/)
.filter((part: string) => isNotEmpty(part)) .filter((part: string) => isNotEmpty(part))
.map((part: string) => part.match(/^embed.size=([^=]+)=(\d+)$/)) .map((part: string) => part.match(/^embed.size=([^=]+)=(\d+)$/))
.filter((matches: RegExpMatchArray) => hasValue(matches) && hasValue(matches[1]) && hasValue(matches[2])) .filter((matches: RegExpMatchArray) => hasValue(matches) && hasValue(matches[1]) && hasValue(matches[2]))
@@ -54,6 +58,9 @@ export const getEmbedSizeParams = (url: string): { name: string, size: number }[
return { name: matches[1], size: Number(matches[2]) }; return { name: matches[1], size: Number(matches[2]) };
}); });
} }
} catch (e) {
// Ignore parsing errors. By default, we return an empty result below.
}
} }
return []; return [];

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze'; import * as deepFreeze from 'deep-freeze';
import { import {

View File

@@ -1,8 +1,7 @@
import { filter, map, pairwise } from 'rxjs/operators'; import { filter, map, pairwise } from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as fromRouter from '@ngrx/router-store'; import { RouterNavigationAction, ROUTER_NAVIGATION } from '@ngrx/router-store';
import { RouterNavigationAction } from '@ngrx/router-store';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RouteUpdateAction } from './router.actions'; import { RouteUpdateAction } from './router.actions';
@@ -14,7 +13,7 @@ export class RouterEffects {
*/ */
routeChange$ = createEffect(() => this.actions$ routeChange$ = createEffect(() => this.actions$
.pipe( .pipe(
ofType(fromRouter.ROUTER_NAVIGATION), ofType(ROUTER_NAVIGATION),
pairwise(), pairwise(),
map((actions: RouterNavigationAction[]) => map((actions: RouterNavigationAction[]) =>
actions.map((navigateAction) => { actions.map((navigateAction) => {

View File

@@ -4,7 +4,7 @@ import { ActivatedRoute, NavigationEnd, Params, Router, RouterStateSnapshot, } f
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { isEqual } from 'lodash'; import isEqual from 'lodash/isEqual';
import { AddParameterAction, SetParameterAction, SetParametersAction, SetQueryParameterAction, SetQueryParametersAction } from './route.actions'; import { AddParameterAction, SetParameterAction, SetParametersAction, SetQueryParameterAction, SetQueryParametersAction } from './route.actions';
import { coreSelector } from '../core.selectors'; import { coreSelector } from '../core.selectors';

View File

@@ -3,7 +3,7 @@ import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { HALEndpointService } from './hal-endpoint.service'; import { HALEndpointService } from './hal-endpoint.service';
import { EndpointMapRequest } from '../data/request.models'; import { EndpointMapRequest } from '../data/request.models';
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';

View File

@@ -5,7 +5,8 @@ import {
MetadataValueFilter, MetadataValueFilter,
MetadatumViewModel MetadatumViewModel
} from './metadata.models'; } from './metadata.models';
import { groupBy, sortBy } from 'lodash'; import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy';
/** /**
* Utility class for working with DSpace object metadata. * Utility class for working with DSpace object metadata.

View File

@@ -26,6 +26,8 @@ import { SearchConfigurationService } from './search-configuration.service';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { RequestEntry } from '../../data/request-entry.model'; import { RequestEntry } from '../../data/request-entry.model';
import { Angulartics2 } from 'angulartics2'; import { Angulartics2 } from 'angulartics2';
import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model';
import anything = jasmine.anything;
@Component({ template: '' }) @Component({ template: '' })
class DummyComponent { class DummyComponent {
@@ -36,7 +38,7 @@ describe('SearchService', () => {
let searchService: SearchService; let searchService: SearchService;
const router = new RouterStub(); const router = new RouterStub();
const route = new ActivatedRouteStub(); const route = new ActivatedRouteStub();
const searchConfigService = {paginationID: 'page-id'}; const searchConfigService = { paginationID: 'page-id' };
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
@@ -103,7 +105,8 @@ describe('SearchService', () => {
}; };
const paginationService = new PaginationServiceStub(); const paginationService = new PaginationServiceStub();
const searchConfigService = {paginationID: 'page-id'}; const searchConfigService = { paginationID: 'page-id' };
const requestService = getMockRequestService();
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -119,7 +122,7 @@ describe('SearchService', () => {
providers: [ providers: [
{ provide: Router, useValue: router }, { provide: Router, useValue: router },
{ provide: RouteService, useValue: routeServiceStub }, { provide: RouteService, useValue: routeServiceStub },
{ provide: RequestService, useValue: getMockRequestService() }, { provide: RequestService, useValue: requestService },
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService }, { provide: RemoteDataBuildService, useValue: remoteDataBuildService },
{ provide: HALEndpointService, useValue: halService }, { provide: HALEndpointService, useValue: halService },
{ provide: CommunityDataService, useValue: {} }, { provide: CommunityDataService, useValue: {} },
@@ -138,13 +141,13 @@ describe('SearchService', () => {
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => { it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
searchService.setViewMode(ViewMode.ListElement); searchService.setViewMode(ViewMode.ListElement);
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], {page: 1}, { view: ViewMode.ListElement } expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], { page: 1 }, { view: ViewMode.ListElement }
); );
}); });
it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => { it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => {
searchService.setViewMode(ViewMode.GridElement); searchService.setViewMode(ViewMode.GridElement);
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], {page: 1}, { view: ViewMode.GridElement } expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], { page: 1 }, { view: ViewMode.GridElement }
); );
}); });
@@ -191,5 +194,23 @@ describe('SearchService', () => {
expect((searchService as any).rdb.buildFromHref).toHaveBeenCalledWith(endPoint); expect((searchService as any).rdb.buildFromHref).toHaveBeenCalledWith(endPoint);
}); });
}); });
describe('when getFacetValuesFor is called with a filterQuery', () => {
it('should add the encoded filterQuery to the args list', () => {
jasmine.getEnv().allowRespy(true);
const spyRequest = spyOn((searchService as any), 'request').and.stub();
spyOn(requestService, 'send').and.returnValue(true);
const searchFilterConfig = new SearchFilterConfig();
searchFilterConfig._links = {
self: {
href: 'https://demo.dspace.org/',
},
};
searchService.getFacetValuesFor(searchFilterConfig, 1, undefined, 'filter&Query');
expect(spyRequest).toHaveBeenCalledWith(anything(), 'https://demo.dspace.org?page=0&size=5&prefix=filter%26Query');
});
});
}); });
}); });

View File

@@ -3,7 +3,6 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { Injectable, OnDestroy } from '@angular/core'; import { Injectable, OnDestroy } from '@angular/core';
import { map, switchMap, take } from 'rxjs/operators'; import { map, switchMap, take } from 'rxjs/operators';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { PaginatedList } from '../../data/paginated-list.model';
import { ResponseParsingService } from '../../data/parsing.service'; import { ResponseParsingService } from '../../data/parsing.service';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
import { GetRequest } from '../../data/request.models'; import { GetRequest } from '../../data/request.models';
@@ -271,7 +270,7 @@ export class SearchService implements OnDestroy {
let href; let href;
let args: string[] = []; let args: string[] = [];
if (hasValue(filterQuery)) { if (hasValue(filterQuery)) {
args.push(`prefix=${filterQuery}`); args.push(`prefix=${encodeURIComponent(filterQuery)}`);
} }
if (hasValue(searchOptions)) { if (hasValue(searchOptions)) {
searchOptions = Object.assign(new PaginatedSearchOptions({}), searchOptions, { searchOptions = Object.assign(new PaginatedSearchOptions({}), searchOptions, {

View File

@@ -1,6 +1,6 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import { EquatableObject, excludeFromEquals, fieldsForEquals } from './equals.decorators'; import { EquatableObject, excludeFromEquals, fieldsForEquals } from './equals.decorators';
import { cloneDeep } from 'lodash'; import cloneDeep from 'lodash/cloneDeep';
class Dog extends EquatableObject<Dog> { class Dog extends EquatableObject<Dog> {
public name: string; public name: string;

View File

@@ -1,7 +1,7 @@
import { CorrelationIdService } from './correlation-id.service'; import { CorrelationIdService } from './correlation-id.service';
import { CookieServiceMock } from '../shared/mocks/cookie.service.mock'; import { CookieServiceMock } from '../shared/mocks/cookie.service.mock';
import { UUIDService } from '../core/shared/uuid.service'; import { UUIDService } from '../core/shared/uuid.service';
import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { MockStore } from '@ngrx/store/testing';
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { Store, StoreModule } from '@ngrx/store'; import { Store, StoreModule } from '@ngrx/store';
import { appReducers, AppState, storeModuleConfig } from '../app.reducer'; import { appReducers, AppState, storeModuleConfig } from '../app.reducer';

View File

@@ -5,7 +5,7 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { find, map } from 'rxjs/operators'; import { find, map } from 'rxjs/operators';
import { NotificationsService } from '../shared/notifications/notifications.service'; import { NotificationsService } from '../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty } from '../shared/empty.util';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ProcessDataService } from '../core/data/processes/process-data.service'; import { ProcessDataService } from '../core/data/processes/process-data.service';

View File

@@ -17,9 +17,6 @@ import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { of as observableOf } from 'rxjs';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../shared/theme-support/theme.service'; import { ThemeService } from '../../shared/theme-support/theme.service';

View File

@@ -13,7 +13,7 @@ import { makeStateKey, TransferState } from '@angular/platform-browser';
import { APP_CONFIG, AppConfig } from '../config/app-config.interface'; import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { AppState } from './app.reducer'; import { AppState } from './app.reducer';
import { isEqual } from 'lodash'; import isEqual from 'lodash/isEqual';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { LocaleService } from './core/locale/locale.service'; import { LocaleService } from './core/locale/locale.service';
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';

View File

@@ -1,12 +1,11 @@
import { Observable } from 'rxjs/internal/Observable';
import { waitForAsync, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { waitForAsync, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of as observableOf, of } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { cold } from 'jasmine-marbles'; import { cold } from 'jasmine-marbles';
import { ItemAuthorizationsComponent, BitstreamMapValue } from './item-authorizations.component'; import { ItemAuthorizationsComponent } from './item-authorizations.component';
import { Bitstream } from '../../../core/shared/bitstream.model'; import { Bitstream } from '../../../core/shared/bitstream.model';
import { Bundle } from '../../../core/shared/bundle.model'; import { Bundle } from '../../../core/shared/bundle.model';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
@@ -14,8 +13,6 @@ import { LinkService } from '../../../core/cache/builders/link.service';
import { getMockLinkService } from '../../../shared/mocks/link-service.mock'; import { getMockLinkService } from '../../../shared/mocks/link-service.mock';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { createPaginatedList, createTestComponent } from '../../../shared/testing/utils.test'; import { createPaginatedList, createTestComponent } from '../../../shared/testing/utils.test';
import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model';
import { PageInfo } from '../../../core/shared/page-info.model';
describe('ItemAuthorizationsComponent test suite', () => { describe('ItemAuthorizationsComponent test suite', () => {
let comp: ItemAuthorizationsComponent; let comp: ItemAuthorizationsComponent;

View File

@@ -1,4 +1,4 @@
import { isEqual } from 'lodash'; import isEqual from 'lodash/isEqual';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';

View File

@@ -17,10 +17,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-
import { createPaginatedList } from '../../../../../shared/testing/utils.test'; import { createPaginatedList } from '../../../../../shared/testing/utils.test';
import { RequestService } from '../../../../../core/data/request.service'; import { RequestService } from '../../../../../core/data/request.service';
import { PaginationService } from '../../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../../core/pagination/pagination.service';
import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../../../../core/cache/models/sort-options.model';
import { PaginationServiceStub } from '../../../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../../../../core/data/find-list-options.model';
describe('PaginatedDragAndDropBitstreamListComponent', () => { describe('PaginatedDragAndDropBitstreamListComponent', () => {
let comp: PaginatedDragAndDropBitstreamListComponent; let comp: PaginatedDragAndDropBitstreamListComponent;

View File

@@ -1,6 +1,6 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core'; import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core';
import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Bitstream } from '../../../../core/shared/bitstream.model';
import { cloneDeep } from 'lodash'; import cloneDeep from 'lodash/cloneDeep';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';

View File

@@ -5,7 +5,7 @@ import {
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { hasValue, isNotEmpty } from '../../../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
import { RegistryService } from '../../../../core/registry/registry.service'; import { RegistryService } from '../../../../core/registry/registry.service';
import { cloneDeep } from 'lodash'; import cloneDeep from 'lodash/cloneDeep';
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';

View File

@@ -3,7 +3,7 @@ import { Item } from '../../../core/shared/item.model';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { cloneDeep } from 'lodash'; import cloneDeep from 'lodash/cloneDeep';
import { first, switchMap } from 'rxjs/operators'; import { first, switchMap } from 'rxjs/operators';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';

View File

@@ -1,18 +1,16 @@
<ds-metadata-field-wrapper [label]="label | translate"> <ds-metadata-field-wrapper [label]="label | translate">
<ng-container *ngFor="let mdValue of mdValues; let last=last;"> <ng-container *ngFor="let mdValue of mdValues; let last=last;">
<ng-container *ngTemplateOutlet="(renderMarkdown ? markdown : simple); context: {value: mdValue.value, classes: 'dont-break-out preserve-line-breaks'}"> <ng-container *ngTemplateOutlet="(renderMarkdown ? markdown : simple); context: {value: mdValue.value}">
</ng-container> </ng-container>
<span class="separator" *ngIf="!last" [innerHTML]="separator"></span> <span class="separator" *ngIf="!last" [innerHTML]="separator"></span>
</ng-container> </ng-container>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ng-template #markdown let-value="value" let-classes="classes"> <ng-template #markdown let-value="value">
<span class="{{classes}}" [innerHTML]="value | dsMarkdown | async"> <span class="dont-break-out" [innerHTML]="value | dsMarkdown | async">
</span> </span>
</ng-template> </ng-template>
<ng-template #simple let-value="value" let-classes="classes"> <ng-template #simple let-value="value">
<span class="{{classes}}"> <span class="dont-break-out preserve-line-breaks">{{value}}</span>
{{value}}
</span>
</ng-template> </ng-template>

View File

@@ -16,11 +16,8 @@ import { MockBitstreamFormat1 } from '../../../../shared/mocks/item.mock';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model';
import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../../../core/data/find-list-options.model';
describe('FullFileSectionComponent', () => { describe('FullFileSectionComponent', () => {
let comp: FullFileSectionComponent; let comp: FullFileSectionComponent;

View File

@@ -35,4 +35,10 @@ export class GenericItemPageFieldComponent extends ItemPageFieldComponent {
*/ */
@Input() label: string; @Input() label: string;
/**
* Whether the {@link MarkdownPipe} should be used to render this metadata.
*/
@Input() enableMarkdown = false;
} }

View File

@@ -76,7 +76,7 @@
<ds-generic-item-page-field [item]="object" <ds-generic-item-page-field [item]="object"
[fields]="['dc.subject']" [fields]="['dc.subject']"
[separator]="','" [separator]="', '"
[label]="'item.page.subject'"> [label]="'item.page.subject'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<ds-generic-item-page-field [item]="object" <ds-generic-item-page-field [item]="object"

View File

@@ -28,7 +28,6 @@ import { TruncatableService } from '../../../../shared/truncatable/truncatable.s
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component'; import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
import { compareArraysUsing, compareArraysUsingIds } from './item-relationships-utils'; import { compareArraysUsing, compareArraysUsingIds } from './item-relationships-utils';
import { ItemComponent } from './item.component';
import { createPaginatedList } from '../../../../shared/testing/utils.test'; import { createPaginatedList } from '../../../../shared/testing/utils.test';
import { RouteService } from '../../../../core/services/route.service'; import { RouteService } from '../../../../core/services/route.service';
import { MetadataValue } from '../../../../core/shared/metadata.models'; import { MetadataValue } from '../../../../core/shared/metadata.models';

View File

@@ -61,7 +61,7 @@
<ds-generic-item-page-field [item]="object" <ds-generic-item-page-field [item]="object"
[fields]="['dc.subject']" [fields]="['dc.subject']"
[separator]="','" [separator]="', '"
[label]="'item.page.subject'"> [label]="'item.page.subject'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<ds-generic-item-page-field [item]="object" <ds-generic-item-page-field [item]="object"

View File

@@ -11,10 +11,6 @@ import { cold, hot } from 'jasmine-marbles';
import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-type'; import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-type';
import { PaginationServiceStub } from '../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../shared/testing/pagination-service.stub';
import { Context } from '../core/shared/context.model'; import { Context } from '../core/shared/context.model';
import { LinkService } from '../core/cache/builders/link.service';
import { HALEndpointService } from '../core/shared/hal-endpoint.service';
import { RequestService } from '../core/data/request.service';
import { RemoteDataBuildService } from '../core/cache/builders/remote-data-build.service';
import { HALEndpointServiceStub } from '../shared/testing/hal-endpoint-service.stub'; import { HALEndpointServiceStub } from '../shared/testing/hal-endpoint-service.stub';
import { getMockRemoteDataBuildService } from '../shared/mocks/remote-data-build.service.mock'; import { getMockRemoteDataBuildService } from '../shared/mocks/remote-data-build.service.mock';

View File

@@ -1,4 +1,4 @@
<div class="nav-item dropdown expandable-navbar-section" <div class="nav-item dropdown expandable-navbar-section text-md-center"
*ngVar="(active | async) as isActive" *ngVar="(active | async) as isActive"
(keyup.enter)="isActive ? deactivateSection($event) : activateSection($event)" (keyup.enter)="isActive ? deactivateSection($event) : activateSection($event)"
(keyup.space)="isActive ? deactivateSection($event) : activateSection($event)" (keyup.space)="isActive ? deactivateSection($event) : activateSection($event)"

View File

@@ -1,3 +1,10 @@
.expandable-navbar-section {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
}
.dropdown-menu { .dropdown-menu {
overflow: hidden; overflow: hidden;
min-width: 100%; min-width: 100%;

View File

@@ -1,3 +1,3 @@
<div class="nav-item navbar-section"> <div class="nav-item navbar-section text-md-center">
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container> <ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
</div> </div>

View File

@@ -0,0 +1,5 @@
.navbar-section {
display: flex;
align-items: center;
height: 100%;
}

View File

@@ -1,10 +1,13 @@
<nav [ngClass]="{'open': !(menuCollapsed | async)}" [@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')" <nav [ngClass]="{'open': !(menuCollapsed | async)}" [@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
class="navbar navbar-light navbar-expand-md p-md-0 navbar-container" role="navigation" [attr.aria-label]="'nav.main.description' | translate"> class="navbar navbar-light navbar-expand-md p-md-0 navbar-container" role="navigation" [attr.aria-label]="'nav.main.description' | translate">
<!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed --> <!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed -->
<div class="container"> <div class="navbar-inner-container w-100" [class.container]="!(isXsOrSm$ | async)">
<div class="reset-padding-md w-100"> <div class="reset-padding-md w-100">
<div id="collapsingNav"> <div id="collapsingNav">
<ul class="navbar-nav navbar-navigation mr-auto shadow-none"> <ul class="navbar-nav navbar-navigation mr-auto shadow-none">
<li *ngIf="(isXsOrSm$ | async) && (isAuthenticated$ | async)">
<ds-user-menu [inExpandableNavbar]="true"></ds-user-menu>
</li>
<ng-container *ngFor="let section of (sections | async)"> <ng-container *ngFor="let section of (sections | async)">
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container> <ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
</ng-container> </ng-container>

View File

@@ -6,13 +6,14 @@ nav.navbar {
/** Mobile menu styling **/ /** Mobile menu styling **/
@media screen and (max-width: map-get($grid-breakpoints, md)-0.02) { @media screen and (max-width: map-get($grid-breakpoints, md)-0.02) {
.navbar { .navbar {
width: 100vw; width: 100%;
background-color: var(--bs-white); background-color: var(--bs-white);
position: absolute; position: absolute;
overflow: hidden; overflow: hidden;
height: 0; height: 0;
&.open { &.open {
height: 100vh; //doesn't matter because wrapper is sticky height: auto;
min-height: 100vh; //doesn't matter because wrapper is sticky
} }
} }
} }
@@ -27,7 +28,7 @@ nav.navbar {
/* TODO remove when https://github.com/twbs/bootstrap/issues/24726 is fixed */ /* TODO remove when https://github.com/twbs/bootstrap/issues/24726 is fixed */
.navbar-expand-md.navbar-container { .navbar-expand-md.navbar-container {
@media screen and (max-width: map-get($grid-breakpoints, md)-0.02) { @media screen and (max-width: map-get($grid-breakpoints, md)-0.02) {
> .container { > .navbar-inner-container {
padding: 0 var(--bs-spacer); padding: 0 var(--bs-spacer);
} }
padding: 0; padding: 0;

View File

@@ -22,9 +22,17 @@ import { Item } from '../core/shared/item.model';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { ThemeService } from '../shared/theme-support/theme.service'; import { ThemeService } from '../shared/theme-support/theme.service';
import { getMockThemeService } from '../shared/mocks/theme-service.mock'; import { getMockThemeService } from '../shared/mocks/theme-service.mock';
import { Store, StoreModule } from '@ngrx/store';
import { AppState, storeModuleConfig } from '../app.reducer';
import { authReducer } from '../core/auth/auth.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model';
import { EPersonMock } from '../shared/testing/eperson.mock';
let comp: NavbarComponent; let comp: NavbarComponent;
let fixture: ComponentFixture<NavbarComponent>; let fixture: ComponentFixture<NavbarComponent>;
let store: Store<AppState>;
let initialState: any;
const authorizationService = jasmine.createSpyObj('authorizationService', { const authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true) isAuthorized: observableOf(true)
@@ -83,10 +91,24 @@ describe('NavbarComponent', () => {
} }
), ),
]; ];
initialState = {
core: {
auth: {
authenticated: true,
loaded: true,
blocking: false,
loading: false,
authToken: new AuthTokenInfo('test_token'),
userId: EPersonMock.id,
authMethods: []
}
}
};
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
TranslateModule.forRoot(), TranslateModule.forRoot(),
StoreModule.forRoot({ auth: authReducer }, storeModuleConfig),
NoopAnimationsModule, NoopAnimationsModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterTestingModule], RouterTestingModule],
@@ -99,6 +121,7 @@ describe('NavbarComponent', () => {
{ provide: ActivatedRoute, useValue: routeStub }, { provide: ActivatedRoute, useValue: routeStub },
{ provide: BrowseService, useValue: { getBrowseDefinitions: createSuccessfulRemoteDataObject$(buildPaginatedList(undefined, browseDefinitions)) } }, { provide: BrowseService, useValue: { getBrowseDefinitions: createSuccessfulRemoteDataObject$(buildPaginatedList(undefined, browseDefinitions)) } },
{ provide: AuthorizationDataService, useValue: authorizationService }, { provide: AuthorizationDataService, useValue: authorizationService },
provideMockStore({ initialState }),
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })
@@ -107,7 +130,7 @@ describe('NavbarComponent', () => {
// synchronous beforeEach // synchronous beforeEach
beforeEach(() => { beforeEach(() => {
store = TestBed.inject(Store);
fixture = TestBed.createComponent(NavbarComponent); fixture = TestBed.createComponent(NavbarComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;

View File

@@ -8,6 +8,10 @@ import { ActivatedRoute } from '@angular/router';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { MenuID } from '../shared/menu/menu-id.model'; import { MenuID } from '../shared/menu/menu-id.model';
import { ThemeService } from '../shared/theme-support/theme.service'; import { ThemeService } from '../shared/theme-support/theme.service';
import { Observable } from 'rxjs';
import { select, Store } from '@ngrx/store';
import { AppState } from '../app.reducer';
import { isAuthenticated } from '../core/auth/selectors';
/** /**
* Component representing the public navbar * Component representing the public navbar
@@ -25,18 +29,29 @@ export class NavbarComponent extends MenuComponent {
*/ */
menuID = MenuID.PUBLIC; menuID = MenuID.PUBLIC;
/**
* Whether user is authenticated.
* @type {Observable<string>}
*/
public isAuthenticated$: Observable<boolean>;
public isXsOrSm$: Observable<boolean>;
constructor(protected menuService: MenuService, constructor(protected menuService: MenuService,
protected injector: Injector, protected injector: Injector,
public windowService: HostWindowService, public windowService: HostWindowService,
public browseService: BrowseService, public browseService: BrowseService,
public authorizationService: AuthorizationDataService, public authorizationService: AuthorizationDataService,
public route: ActivatedRoute, public route: ActivatedRoute,
protected themeService: ThemeService protected themeService: ThemeService,
private store: Store<AppState>,
) { ) {
super(menuService, injector, authorizationService, route, themeService); super(menuService, injector, authorizationService, route, themeService);
} }
ngOnInit(): void { ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
this.isXsOrSm$ = this.windowService.isXsOrSm();
this.isAuthenticated$ = this.store.pipe(select(isAuthenticated));
} }
} }

View File

@@ -4,7 +4,7 @@ import { HostWindowResizeAction } from '../shared/host-window.actions';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { provideMockActions } from '@ngrx/effects/testing'; import { provideMockActions } from '@ngrx/effects/testing';
import { cold, hot } from 'jasmine-marbles'; import { cold, hot } from 'jasmine-marbles';
import * as fromRouter from '@ngrx/router-store'; import { ROUTER_NAVIGATION } from '@ngrx/router-store';
import { CollapseMenuAction } from '../shared/menu/menu.actions'; import { CollapseMenuAction } from '../shared/menu/menu.actions';
import { MenuService } from '../shared/menu/menu.service'; import { MenuService } from '../shared/menu/menu.service';
import { MenuServiceStub } from '../shared/testing/menu-service.stub'; import { MenuServiceStub } from '../shared/testing/menu-service.stub';
@@ -43,7 +43,7 @@ describe('NavbarEffects', () => {
describe('routeChange$', () => { describe('routeChange$', () => {
it('should return a COLLAPSE action in response to an UPDATE_LOCATION action', () => { it('should return a COLLAPSE action in response to an UPDATE_LOCATION action', () => {
actions = hot('--a-', { a: { type: fromRouter.ROUTER_NAVIGATION } }); actions = hot('--a-', { a: { type: ROUTER_NAVIGATION } });
const expected = cold('--b-', { b: new CollapseMenuAction(MenuID.PUBLIC) }); const expected = cold('--b-', { b: new CollapseMenuAction(MenuID.PUBLIC) });

View File

@@ -1,7 +1,7 @@
import { first, map, switchMap } from 'rxjs/operators'; import { first, map, switchMap } from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as fromRouter from '@ngrx/router-store'; import { ROUTER_NAVIGATION } from '@ngrx/router-store';
import { HostWindowActionTypes } from '../shared/host-window.actions'; import { HostWindowActionTypes } from '../shared/host-window.actions';
import { import {
@@ -33,7 +33,7 @@ export class NavbarEffects {
*/ */
routeChange$ = createEffect(() => this.actions$ routeChange$ = createEffect(() => this.actions$
.pipe( .pipe(
ofType(fromRouter.ROUTER_NAVIGATION), ofType(ROUTER_NAVIGATION),
map(() => new CollapseMenuAction(this.menuID)) map(() => new CollapseMenuAction(this.menuID))
)); ));
/** /**

View File

@@ -15,7 +15,6 @@ import {
} from '@angular/core/testing'; } from '@angular/core/testing';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component'; import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component';
import { Process } from '../processes/process.model'; import { Process } from '../processes/process.model';

View File

@@ -8,7 +8,7 @@ import { EPerson } from '../../core/eperson/models/eperson.model';
import { FormBuilderService } from '../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { cloneDeep } from 'lodash'; import cloneDeep from 'lodash/cloneDeep';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
describe('ProfilePageMetadataFormComponent', () => { describe('ProfilePageMetadataFormComponent', () => {

View File

@@ -11,7 +11,7 @@ import { TranslateService } from '@ngx-translate/core';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { LangConfig } from '../../../config/lang-config.interface'; import { LangConfig } from '../../../config/lang-config.interface';
import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { cloneDeep } from 'lodash'; import cloneDeep from 'lodash/cloneDeep';
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../core/shared/operators'; import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../core/shared/operators';
import { FormBuilderService } from '../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';

View File

@@ -2,7 +2,6 @@ import { RegistrationGuard } from './registration.guard';
import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; import { EpersonRegistrationService } from '../core/data/eperson-registration.service';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../core/auth/auth.service'; import { AuthService } from '../core/auth/auth.service';
import { Location } from '@angular/common';
import { import {
createFailedRemoteDataObject$, createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject,

View File

@@ -1,9 +1,9 @@
<div class="outer-wrapper" [class.d-none]="shouldShowFullscreenLoader"> <div class="outer-wrapper" [class.d-none]="shouldShowFullscreenLoader" [@slideSidebarPadding]="{
<ds-themed-admin-sidebar></ds-themed-admin-sidebar>
<div class="inner-wrapper" [@slideSidebarPadding]="{
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'), value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)} params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)}
}"> }">
<ds-themed-admin-sidebar></ds-themed-admin-sidebar>
<div class="inner-wrapper">
<ds-themed-header-navbar-wrapper></ds-themed-header-navbar-wrapper> <ds-themed-header-navbar-wrapper></ds-themed-header-navbar-wrapper>
<main class="main-content"> <main class="main-content">
<ds-themed-breadcrumbs></ds-themed-breadcrumbs> <ds-themed-breadcrumbs></ds-themed-breadcrumbs>

View File

@@ -12,7 +12,7 @@ export const slide = trigger('slide', [
export const slideMobileNav = trigger('slideMobileNav', [ export const slideMobileNav = trigger('slideMobileNav', [
state('expanded', style({ height: '100vh' })), state('expanded', style({ height: 'auto', 'min-height': '100vh' })),
state('collapsed', style({ height: 0 })), state('collapsed', style({ height: 0 })),

View File

@@ -2,11 +2,11 @@
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item" <li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"
(click)="$event.stopPropagation();"> (click)="$event.stopPropagation();">
<div ngbDropdown #loginDrop display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut> <div ngbDropdown #loginDrop display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="javascript:void(0);" class="dropdownLogin px-1 " [attr.aria-label]="'nav.login' |translate" (click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly" ngbDropdownToggle> <a href="javascript:void(0);" class="dropdownLogin px-1" [attr.aria-label]="'nav.login' |translate"
{{ 'nav.login' | translate }} (click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly"
</a> ngbDropdownToggle>{{ 'nav.login' | translate }}</a>
<div class="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu <div class="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu
[attr.aria-label]="'nav.login' |translate"> [attr.aria-label]="'nav.login' | translate">
<ds-log-in <ds-log-in
[isStandalonePage]="false"></ds-log-in> [isStandalonePage]="false"></ds-log-in>
</div> </div>
@@ -19,16 +19,16 @@
</li> </li>
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"> <li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item">
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut> <div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="javascript:void(0);" role="button" [attr.aria-label]="'nav.logout' |translate" (click)="$event.preventDefault()" [title]="'nav.logout' | translate" class="px-1" [attr.data-test]="'user-menu' | dsBrowserOnly" ngbDropdownToggle> <a href="javascript:void(0);" role="button" [attr.aria-label]="'nav.user-profile-menu-and-logout' |translate" (click)="$event.preventDefault()" [title]="'nav.user-profile-menu-and-logout' | translate" class="px-1" [attr.data-test]="'user-menu' | dsBrowserOnly" ngbDropdownToggle>
<i class="fas fa-user-circle fa-lg fa-fw"></i></a> <i class="fas fa-user-circle fa-lg fa-fw"></i></a>
<div class="logoutDropdownMenu" ngbDropdownMenu [attr.aria-label]="'nav.logout' |translate"> <div class="logoutDropdownMenu" ngbDropdownMenu [attr.aria-label]="'nav.user-profile-menu-and-logout' |translate">
<ds-user-menu></ds-user-menu> <ds-user-menu></ds-user-menu>
</div> </div>
</div> </div>
</li> </li>
<li *ngIf="(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item"> <li *ngIf="(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
<a id="logoutLink" role="button" [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="px-1"> <a id="logoutLink" role="button" [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="px-1">
<i class="fas fa-user-circle fa-lg fa-fw"></i> <i class="fas fa-sign-out-alt fa-lg fa-fw"></i>
<span class="sr-only">(current)</span> <span class="sr-only">(current)</span>
</a> </a>
</li> </li>

View File

@@ -1,10 +1,13 @@
<ds-themed-loading *ngIf="(loading$ | async)"></ds-themed-loading> <ds-themed-loading *ngIf="(loading$ | async)"></ds-themed-loading>
<div *ngIf="!(loading$ | async)"> <div *ngIf="!(loading$ | async)">
<span class="dropdown-item-text">{{(user$ | async)?.name}} ({{(user$ | async)?.email}})</span> <span class="dropdown-item-text" [class.pl-0]="inExpandableNavbar">
<a class="dropdown-item" [routerLink]="[profileRoute]" routerLinkActive="active">{{'nav.profile' | translate}}</a> {{(user$ | async)?.name}}<br>
<a class="dropdown-item" [routerLink]="[mydspaceRoute]" routerLinkActive="active">{{'nav.mydspace' | translate}}</a> <span class="text-muted">{{(user$ | async)?.email}}</span>
</span>
<a [ngClass]="inExpandableNavbar ? 'nav-item nav-link' : 'dropdown-item'" [routerLink]="[profileRoute]" routerLinkActive="active">{{'nav.profile' | translate}}</a>
<a [ngClass]="inExpandableNavbar ? 'nav-item nav-link' : 'dropdown-item'" [routerLink]="[mydspaceRoute]" routerLinkActive="active">{{'nav.mydspace' | translate}}</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<ds-log-out></ds-log-out> <ds-log-out *ngIf="!inExpandableNavbar" data-test="log-out-component"></ds-log-out>
</div> </div>

View File

@@ -162,10 +162,24 @@ describe('UserMenuComponent', () => {
}); });
it('should display user name and email', () => { it('should display user name and email', () => {
const user = 'User Test (test@test.com)'; const username = 'User Test';
const email = 'test@test.com';
const span = deUserMenu.query(By.css('.dropdown-item-text')); const span = deUserMenu.query(By.css('.dropdown-item-text'));
expect(span).toBeDefined(); expect(span).toBeDefined();
expect(span.nativeElement.innerHTML).toBe(user); expect(span.nativeElement.innerHTML).toContain(username);
expect(span.nativeElement.innerHTML).toContain(email);
});
it('should create logout component', () => {
const components = fixture.debugElement.query(By.css('[data-test="log-out-component"]'));
expect(components).toBeTruthy();
});
it('should not create logout component', () => {
component.inExpandableNavbar = true;
fixture.detectChanges();
const components = fixture.debugElement.query(By.css('[data-test="log-out-component"]'));
expect(components).toBeFalsy();
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
@@ -20,6 +20,11 @@ import { getProfileModuleRoute } from '../../../app-routing-paths';
}) })
export class UserMenuComponent implements OnInit { export class UserMenuComponent implements OnInit {
/**
* The input flag to show user details in navbar expandable menu
*/
@Input() inExpandableNavbar = false;
/** /**
* True if the authentication is loading. * True if the authentication is loading.
* @type {Observable<boolean>} * @type {Observable<boolean>}

View File

@@ -11,7 +11,7 @@ import {
SimpleChanges SimpleChanges
} from '@angular/core'; } from '@angular/core';
import { findIndex } from 'lodash'; import findIndex from 'lodash/findIndex';
import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model';
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';

View File

@@ -1,7 +1,7 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, } from '@angular/core';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { isObject } from 'lodash'; import isObject from 'lodash/isObject';
import { Chips } from './models/chips.model'; import { Chips } from './models/chips.model';
import { ChipsItem } from './models/chips-item.model'; import { ChipsItem } from './models/chips-item.model';

View File

@@ -1,4 +1,5 @@
import { isObject, uniqueId } from 'lodash'; import isObject from 'lodash/isObject';
import uniqueId from 'lodash/uniqueId';
import { hasValue, isNotEmpty } from '../../empty.util'; import { hasValue, isNotEmpty } from '../../empty.util';
import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
import { ConfidenceType } from '../../../core/shared/confidence-type'; import { ConfidenceType } from '../../../core/shared/confidence-type';

View File

@@ -1,4 +1,6 @@
import { findIndex, isEqual, isObject } from 'lodash'; import findIndex from 'lodash/findIndex';
import isEqual from 'lodash/isEqual';
import isObject from 'lodash/isObject';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { ChipsItem, ChipsItemIcon } from './chips-item.model'; import { ChipsItem, ChipsItemIcon } from './chips-item.model';
import { hasValue, isNotEmpty } from '../../empty.util'; import { hasValue, isNotEmpty } from '../../empty.util';

View File

@@ -11,7 +11,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { DeleteComColPageComponent } from './delete-comcol-page.component'; import { DeleteComColPageComponent } from './delete-comcol-page.component';
import { NotificationsService } from '../../../notifications/notifications.service'; import { NotificationsService } from '../../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
import { RequestService } from '../../../../core/data/request.service';
import { getTestScheduler } from 'jasmine-marbles'; import { getTestScheduler } from 'jasmine-marbles';
import { ComColDataService } from '../../../../core/data/comcol-data.service'; import { ComColDataService } from '../../../../core/data/comcol-data.service';
import { createFailedRemoteDataObject$, createNoContentRemoteDataObject$ } from '../../../remote-data.utils'; import { createFailedRemoteDataObject$, createNoContentRemoteDataObject$ } from '../../../remote-data.utils';

View File

@@ -10,7 +10,8 @@ import { AuthService } from '../../core/auth/auth.service';
import { CookieService } from '../../core/services/cookie.service'; import { CookieService } from '../../core/services/cookie.service';
import { getTestScheduler } from 'jasmine-marbles'; import { getTestScheduler } from 'jasmine-marbles';
import { MetadataValue } from '../../core/shared/metadata.models'; import { MetadataValue } from '../../core/shared/metadata.models';
import { clone, cloneDeep } from 'lodash'; import clone from 'lodash/clone';
import cloneDeep from 'lodash/cloneDeep';
import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
@@ -100,7 +101,7 @@ describe('BrowserKlaroService', () => {
mockConfig = { mockConfig = {
translations: { translations: {
en: { zz: {
purposes: {}, purposes: {},
test: { test: {
testeritis: testKey testeritis: testKey
@@ -158,8 +159,8 @@ describe('BrowserKlaroService', () => {
it('addAppMessages', () => { it('addAppMessages', () => {
service.addAppMessages(); service.addAppMessages();
expect(mockConfig.translations.en[appName]).toBeDefined(); expect(mockConfig.translations.zz[appName]).toBeDefined();
expect(mockConfig.translations.en.purposes[purpose]).toBeDefined(); expect(mockConfig.translations.zz.purposes[purpose]).toBeDefined();
}); });
it('translateConfiguration', () => { it('translateConfiguration', () => {

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import * as Klaro from 'klaro'; import { setup, show } from 'klaro/dist/klaro-no-translations';
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@@ -10,7 +10,8 @@ import { KlaroService } from './klaro.service';
import { hasValue, isEmpty, isNotEmpty } from '../empty.util'; import { hasValue, isEmpty, isNotEmpty } from '../empty.util';
import { CookieService } from '../../core/services/cookie.service'; import { CookieService } from '../../core/services/cookie.service';
import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { cloneDeep, debounce } from 'lodash'; import cloneDeep from 'lodash/cloneDeep';
import debounce from 'lodash/debounce';
import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-configuration'; import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-configuration';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../core/shared/operators';
@@ -78,7 +79,7 @@ export class BrowserKlaroService extends KlaroService {
initialize() { initialize() {
if (!environment.info.enablePrivacyStatement) { if (!environment.info.enablePrivacyStatement) {
delete this.klaroConfig.privacyPolicy; delete this.klaroConfig.privacyPolicy;
this.klaroConfig.translations.en.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy'; this.klaroConfig.translations.zz.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy';
} }
const hideGoogleAnalytics$ = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe( const hideGoogleAnalytics$ = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe(
@@ -135,7 +136,7 @@ export class BrowserKlaroService extends KlaroService {
this.klaroConfig.services = this.filterConfigServices(servicesToHide); this.klaroConfig.services = this.filterConfigServices(servicesToHide);
Klaro.setup(this.klaroConfig); setup(this.klaroConfig);
}); });
} }
@@ -219,7 +220,7 @@ export class BrowserKlaroService extends KlaroService {
* Show the cookie consent form * Show the cookie consent form
*/ */
showSettings() { showSettings() {
Klaro.show(this.klaroConfig); show(this.klaroConfig);
} }
/** /**
@@ -227,12 +228,12 @@ export class BrowserKlaroService extends KlaroService {
*/ */
addAppMessages() { addAppMessages() {
this.klaroConfig.services.forEach((app) => { this.klaroConfig.services.forEach((app) => {
this.klaroConfig.translations.en[app.name] = { this.klaroConfig.translations.zz[app.name] = {
title: this.getTitleTranslation(app.name), title: this.getTitleTranslation(app.name),
description: this.getDescriptionTranslation(app.name) description: this.getDescriptionTranslation(app.name)
}; };
app.purposes.forEach((purpose) => { app.purposes.forEach((purpose) => {
this.klaroConfig.translations.en.purposes[purpose] = this.getPurposeTranslation(purpose); this.klaroConfig.translations.zz.purposes[purpose] = this.getPurposeTranslation(purpose);
}); });
}); });
} }
@@ -246,7 +247,7 @@ export class BrowserKlaroService extends KlaroService {
*/ */
this.translateService.setDefaultLang(environment.defaultLanguage); this.translateService.setDefaultLang(environment.defaultLanguage);
this.translate(this.klaroConfig.translations.en); this.translate(this.klaroConfig.translations.zz);
} }
/** /**

View File

@@ -54,10 +54,46 @@ export const klaroConfiguration: any = {
https://github.com/KIProtect/klaro/tree/master/src/translations https://github.com/KIProtect/klaro/tree/master/src/translations
*/ */
translations: { translations: {
en: { /*
The `zz` key contains default translations that will be used as fallback values.
This can e.g. be useful for defining a fallback privacy policy URL.
FOR DSPACE: We use 'zz' to map to our own i18n translations for klaro, see
translateConfiguration() in browser-klaro.service.ts. All the below i18n keys are specified
in your /src/assets/i18n/*.json5 translation pack.
*/
zz: {
acceptAll: 'cookies.consent.accept-all', acceptAll: 'cookies.consent.accept-all',
acceptSelected: 'cookies.consent.accept-selected', acceptSelected: 'cookies.consent.accept-selected',
app: { close: 'cookies.consent.close',
consentModal: {
title: 'cookies.consent.content-modal.title',
description: 'cookies.consent.content-modal.description'
},
consentNotice: {
changeDescription: 'cookies.consent.update',
title: 'cookies.consent.content-notice.title',
description: 'cookies.consent.content-notice.description',
learnMore: 'cookies.consent.content-notice.learnMore',
},
decline: 'cookies.consent.decline',
ok: 'cookies.consent.ok',
poweredBy: 'Powered by Klaro!',
privacyPolicy: {
name: 'cookies.consent.content-modal.privacy-policy.name',
text: 'cookies.consent.content-modal.privacy-policy.text'
},
purposeItem: {
service: 'cookies.consent.content-modal.service',
services: 'cookies.consent.content-modal.services'
},
purposes: {
},
save: 'cookies.consent.save',
service: {
disableAll: {
description: 'cookies.consent.app.disable-all.description',
title: 'cookies.consent.app.disable-all.title'
},
optOut: { optOut: {
description: 'cookies.consent.app.opt-out.description', description: 'cookies.consent.app.opt-out.description',
title: 'cookies.consent.app.opt-out.title' title: 'cookies.consent.app.opt-out.title'
@@ -65,26 +101,10 @@ export const klaroConfiguration: any = {
purpose: 'cookies.consent.app.purpose', purpose: 'cookies.consent.app.purpose',
purposes: 'cookies.consent.app.purposes', purposes: 'cookies.consent.app.purposes',
required: { required: {
description: 'cookies.consent.app.required.description', title: 'cookies.consent.app.required.title',
title: 'cookies.consent.app.required.title' description: 'cookies.consent.app.required.description'
}
} }
},
close: 'cookies.consent.close',
decline: 'cookies.consent.decline',
changeDescription: 'cookies.consent.update',
consentNotice: {
description: 'cookies.consent.content-notice.description',
learnMore: 'cookies.consent.content-notice.learnMore'
},
consentModal: {
description: 'cookies.consent.content-modal.description',
privacyPolicy: {
name: 'cookies.consent.content-modal.privacy-policy.name',
text: 'cookies.consent.content-modal.privacy-policy.text'
},
title: 'cookies.consent.content-modal.title'
},
purposes: {}
} }
}, },
services: [ services: [

View File

@@ -0,0 +1,107 @@
import { dateToString, dateToNgbDateStruct, dateToISOFormat, isValidDate, yearFromString } from './date.util';
describe('Date Utils', () => {
describe('dateToISOFormat', () => {
it('should convert Date to YYYY-MM-DDThh:mm:ssZ string', () => {
// NOTE: month is zero indexed which is why it increases by one
expect(dateToISOFormat(new Date(Date.UTC(2022, 5, 3)))).toEqual('2022-06-03T00:00:00Z');
});
it('should convert Date string to YYYY-MM-DDThh:mm:ssZ string', () => {
expect(dateToISOFormat('2022-06-03')).toEqual('2022-06-03T00:00:00Z');
});
it('should convert Month string to YYYY-MM-DDThh:mm:ssZ string', () => {
expect(dateToISOFormat('2022-06')).toEqual('2022-06-01T00:00:00Z');
});
it('should convert Year string to YYYY-MM-DDThh:mm:ssZ string', () => {
expect(dateToISOFormat('2022')).toEqual('2022-01-01T00:00:00Z');
});
it('should convert ISO Date string to YYYY-MM-DDThh:mm:ssZ string', () => {
// NOTE: Time is always zeroed out as proven by this test.
expect(dateToISOFormat('2022-06-03T03:24:04Z')).toEqual('2022-06-03T00:00:00Z');
});
it('should convert NgbDateStruct to YYYY-MM-DDThh:mm:ssZ string', () => {
// NOTE: month is zero indexed which is why it increases by one
const date = new Date(Date.UTC(2022, 5, 3));
expect(dateToISOFormat(dateToNgbDateStruct(date))).toEqual('2022-06-03T00:00:00Z');
});
});
describe('dateToString', () => {
it('should convert Date to YYYY-MM-DD string', () => {
// NOTE: month is zero indexed which is why it increases by one
expect(dateToString(new Date(Date.UTC(2022, 5, 3)))).toEqual('2022-06-03');
});
it('should convert Date with time to YYYY-MM-DD string', () => {
// NOTE: month is zero indexed which is why it increases by one
expect(dateToString(new Date(Date.UTC(2022, 5, 3, 3, 24, 0)))).toEqual('2022-06-03');
});
it('should convert Month only to YYYY-MM-DD string', () => {
// NOTE: month is zero indexed which is why it increases by one
expect(dateToString(new Date(Date.UTC(2022, 5)))).toEqual('2022-06-01');
});
it('should convert ISO Date to YYYY-MM-DD string', () => {
expect(dateToString(new Date('2022-06-03T03:24:00Z'))).toEqual('2022-06-03');
});
it('should convert NgbDateStruct to YYYY-MM-DD string', () => {
// NOTE: month is zero indexed which is why it increases by one
const date = new Date(Date.UTC(2022, 5, 3));
expect(dateToString(dateToNgbDateStruct(date))).toEqual('2022-06-03');
});
});
describe('isValidDate', () => {
it('should return false for null', () => {
expect(isValidDate(null)).toBe(false);
});
it('should return false for empty string', () => {
expect(isValidDate('')).toBe(false);
});
it('should return false for text', () => {
expect(isValidDate('test')).toBe(false);
});
it('should return true for YYYY', () => {
expect(isValidDate('2022')).toBe(true);
});
it('should return true for YYYY-MM', () => {
expect(isValidDate('2022-12')).toBe(true);
});
it('should return true for YYYY-MM-DD', () => {
expect(isValidDate('2022-06-03')).toBe(true);
});
it('should return true for YYYY-MM-DDTHH:MM:SS', () => {
expect(isValidDate('2022-06-03T10:20:30')).toBe(true);
});
it('should return true for YYYY-MM-DDTHH:MM:SSZ', () => {
expect(isValidDate('2022-06-03T10:20:30Z')).toBe(true);
});
it('should return false for a month that does not exist', () => {
expect(isValidDate('2022-13')).toBe(false);
});
it('should return false for a day that does not exist', () => {
expect(isValidDate('2022-02-60')).toBe(false);
});
it('should return false for a time that does not exist', () => {
expect(isValidDate('2022-02-60T10:60:20')).toBe(false);
});
});
describe('yearFromString', () => {
it('should return year from YYYY string', () => {
expect(yearFromString('2022')).toEqual(2022);
});
it('should return year from YYYY-MM string', () => {
expect(yearFromString('1970-06')).toEqual(1970);
});
it('should return year from YYYY-MM-DD string', () => {
expect(yearFromString('1914-10-23')).toEqual(1914);
});
it('should return year from YYYY-MM-DDTHH:MM:SSZ string', () => {
expect(yearFromString('1914-10-23T10:20:30Z')).toEqual(1914);
});
it('should return null if invalid date', () => {
expect(yearFromString('test')).toBeNull();
});
});
});

View File

@@ -1,9 +1,8 @@
import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
import { formatInTimeZone } from 'date-fns-tz';
import { isObject } from 'lodash'; import { isValid } from 'date-fns';
import * as moment from 'moment'; import isObject from 'lodash/isObject';
import { hasNoValue } from './empty.util';
import { isNull, isUndefined } from './empty.util';
/** /**
* Returns true if the passed value is a NgbDateStruct. * Returns true if the passed value is a NgbDateStruct.
@@ -31,21 +30,7 @@ export function dateToISOFormat(date: Date | NgbDateStruct | string): string {
const dateObj: Date = (date instanceof Date) ? date : const dateObj: Date = (date instanceof Date) ? date :
((typeof date === 'string') ? ngbDateStructToDate(stringToNgbDateStruct(date)) : ngbDateStructToDate(date)); ((typeof date === 'string') ? ngbDateStructToDate(stringToNgbDateStruct(date)) : ngbDateStructToDate(date));
let year = dateObj.getUTCFullYear().toString(); return formatInTimeZone(dateObj, 'UTC', "yyyy-MM-dd'T'HH:mm:ss'Z'");
let month = (dateObj.getUTCMonth() + 1).toString();
let day = dateObj.getUTCDate().toString();
let hour = dateObj.getHours().toString();
let min = dateObj.getMinutes().toString();
let sec = dateObj.getSeconds().toString();
year = (year.length === 1) ? '0' + year : year;
month = (month.length === 1) ? '0' + month : month;
day = (day.length === 1) ? '0' + day : day;
hour = (hour.length === 1) ? '0' + hour : hour;
min = (min.length === 1) ? '0' + min : min;
sec = (sec.length === 1) ? '0' + sec : sec;
const dateStr = `${year}${month}${day}${hour}${min}${sec}`;
return moment.utc(dateStr, 'YYYYMMDDhhmmss').format();
} }
/** /**
@@ -81,7 +66,7 @@ export function stringToNgbDateStruct(date: string): NgbDateStruct {
* the NgbDateStruct object * the NgbDateStruct object
*/ */
export function dateToNgbDateStruct(date?: Date): NgbDateStruct { export function dateToNgbDateStruct(date?: Date): NgbDateStruct {
if (isNull(date) || isUndefined(date)) { if (hasNoValue(date)) {
date = new Date(); date = new Date();
} }
@@ -102,16 +87,7 @@ export function dateToNgbDateStruct(date?: Date): NgbDateStruct {
*/ */
export function dateToString(date: Date | NgbDateStruct): string { export function dateToString(date: Date | NgbDateStruct): string {
const dateObj: Date = (date instanceof Date) ? date : ngbDateStructToDate(date); const dateObj: Date = (date instanceof Date) ? date : ngbDateStructToDate(date);
return formatInTimeZone(dateObj, 'UTC', 'yyyy-MM-dd');
let year = dateObj.getUTCFullYear().toString();
let month = (dateObj.getUTCMonth() + 1).toString();
let day = dateObj.getUTCDate().toString();
year = (year.length === 1) ? '0' + year : year;
month = (month.length === 1) ? '0' + month : month;
day = (day.length === 1) ? '0' + day : day;
const dateStr = `${year}-${month}-${day}`;
return moment.utc(dateStr, 'YYYYMMDD').format('YYYY-MM-DD');
} }
/** /**
@@ -119,5 +95,15 @@ export function dateToString(date: Date | NgbDateStruct): string {
* @param date the string to be checked * @param date the string to be checked
*/ */
export function isValidDate(date: string) { export function isValidDate(date: string) {
return moment(date).isValid(); return (hasNoValue(date)) ? false : isValid(new Date(date));
} }
/**
* Parse given date string to a year number based on expected formats
* @param date the string to be parsed
* @param formats possible formats the string may align with. MUST be valid date-fns formats
*/
export function yearFromString(date: string) {
return isValidDate(date) ? new Date(date).getUTCFullYear() : null;
}

View File

@@ -1,5 +1,5 @@
import { Component, EventEmitter, HostListener, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, HostListener, Input, OnInit, Output } from '@angular/core';
import { uniqueId } from 'lodash'; import uniqueId from 'lodash/uniqueId';
import { FileUploader } from 'ng2-file-upload'; import { FileUploader } from 'ng2-file-upload';
import { Observable, of as observableOf } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import { UploaderOptions } from '../uploader/uploader-options.model'; import { UploaderOptions } from '../uploader/uploader-options.model';

View File

@@ -10,7 +10,7 @@ import {
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { import {
mockInputWithTypeBindModel, MockRelationModel, mockDcTypeInputModel mockInputWithTypeBindModel, MockRelationModel
} from '../../../mocks/form-models.mock'; } from '../../../mocks/form-models.mock';
import {DsDynamicTypeBindRelationService} from './ds-dynamic-type-bind-relation.service'; import {DsDynamicTypeBindRelationService} from './ds-dynamic-type-bind-relation.service';
import {FormFieldMetadataValueObject} from '../models/form-field-metadata-value.model'; import {FormFieldMetadataValueObject} from '../models/form-field-metadata-value.model';

View File

@@ -1,4 +1,10 @@
import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core'; import {
DynamicFormControlLayout,
DynamicFormControlRelation,
DynamicFormGroupModel,
DynamicFormGroupModelConfig,
serializable
} from '@ng-dynamic-forms/core';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@@ -16,6 +22,7 @@ export interface DynamicConcatModelConfig extends DynamicFormGroupModelConfig {
separator: string; separator: string;
value?: any; value?: any;
hint?: string; hint?: string;
typeBindRelations?: DynamicFormControlRelation[];
relationship?: RelationshipOptions; relationship?: RelationshipOptions;
repeatable: boolean; repeatable: boolean;
required: boolean; required: boolean;
@@ -29,6 +36,8 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
@serializable() separator: string; @serializable() separator: string;
@serializable() hasLanguages = false; @serializable() hasLanguages = false;
@serializable() typeBindRelations: DynamicFormControlRelation[];
@serializable() typeBindHidden = false;
@serializable() relationship?: RelationshipOptions; @serializable() relationship?: RelationshipOptions;
@serializable() repeatable?: boolean; @serializable() repeatable?: boolean;
@serializable() required?: boolean; @serializable() required?: boolean;
@@ -55,6 +64,7 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
this.metadataValue = config.metadataValue; this.metadataValue = config.metadataValue;
this.valueUpdates = new Subject<string>(); this.valueUpdates = new Subject<string>();
this.valueUpdates.subscribe((value: string) => this.value = value); this.valueUpdates.subscribe((value: string) => this.value = value);
this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : [];
} }
get value() { get value() {

View File

@@ -2,6 +2,7 @@ import { Subject } from 'rxjs';
import { import {
DynamicCheckboxGroupModel, DynamicCheckboxGroupModel,
DynamicFormControlLayout, DynamicFormControlLayout,
DynamicFormControlRelation,
DynamicFormGroupModelConfig, DynamicFormGroupModelConfig,
serializable serializable
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
@@ -15,6 +16,7 @@ export interface DynamicListCheckboxGroupModelConfig extends DynamicFormGroupMod
groupLength?: number; groupLength?: number;
repeatable: boolean; repeatable: boolean;
value?: any; value?: any;
typeBindRelations?: DynamicFormControlRelation[];
} }
export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel { export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel {
@@ -23,6 +25,7 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel {
@serializable() repeatable: boolean; @serializable() repeatable: boolean;
@serializable() groupLength: number; @serializable() groupLength: number;
@serializable() _value: VocabularyEntry[]; @serializable() _value: VocabularyEntry[];
@serializable() typeBindRelations: DynamicFormControlRelation[];
isListGroup = true; isListGroup = true;
valueUpdates: Subject<any>; valueUpdates: Subject<any>;
@@ -37,6 +40,7 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel {
this.valueUpdates = new Subject<any>(); this.valueUpdates = new Subject<any>();
this.valueUpdates.subscribe((value: VocabularyEntry | VocabularyEntry[]) => this.value = value); this.valueUpdates.subscribe((value: VocabularyEntry | VocabularyEntry[]) => this.value = value);
this.valueUpdates.next(config.value); this.valueUpdates.next(config.value);
this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : [];
} }
get hasAuthority(): boolean { get hasAuthority(): boolean {

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