Merge branch 'DSpace:main' into main

This commit is contained in:
reetagithub
2022-06-13 07:58:58 +03:00
committed by GitHub
954 changed files with 34976 additions and 20893 deletions

View File

@@ -2,10 +2,16 @@
# For additional information regarding the format and rule options, please see: # For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries # https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running: # You can see what browsers were selected by your queries by running:
# npx browserslist # npx browserslist
> 0.5% last 1 Chrome version
last 2 versions last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR Firefox ESR
not IE 9-11 # For IE 9-11 support, remove 'not'. not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

222
.eslintrc.json Normal file
View File

@@ -0,0 +1,222 @@
{
"root": true,
"plugins": [
"@typescript-eslint",
"@angular-eslint/eslint-plugin",
"eslint-plugin-import",
"eslint-plugin-jsdoc",
"eslint-plugin-deprecation",
"eslint-plugin-unused-imports"
],
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"./tsconfig.json",
"./cypress/tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"max-classes-per-file": [
"error",
1
],
"comma-dangle": [
"off",
"always-multiline"
],
"eol-last": [
"error",
"always"
],
"no-console": [
"error",
{
"allow": [
"log",
"warn",
"dir",
"timeLog",
"assert",
"clear",
"count",
"countReset",
"group",
"groupEnd",
"table",
"debug",
"info",
"dirxml",
"error",
"groupCollapsed",
"Console",
"profile",
"profileEnd",
"timeStamp",
"context"
]
}
],
"curly": "error",
"brace-style": [
"error",
"1tbs",
{
"allowSingleLine": true
}
],
"eqeqeq": [
"error",
"always",
{
"null": "ignore"
}
],
"radix": "error",
"guard-for-in": "error",
"no-bitwise": "error",
"no-restricted-imports": "error",
"no-caller": "error",
"no-debugger": "error",
"no-redeclare": "error",
"no-eval": "error",
"no-fallthrough": "error",
"no-trailing-spaces": "error",
"space-infix-ops": "error",
"keyword-spacing": "error",
"no-var": "error",
"no-unused-expressions": [
"error",
{
"allowTernary": true
}
],
"prefer-const": "off", // todo: re-enable & fix errors (more strict than it used to be in TSLint)
"prefer-spread": "off",
"no-underscore-dangle": "off",
// todo: disabled rules from eslint:recommended, consider re-enabling & fixing
"no-prototype-builtins": "off",
"no-useless-escape": "off",
"no-case-declarations": "off",
"no-extra-boolean-cast": "off",
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "ds",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "ds",
"style": "kebab-case"
}
],
"@angular-eslint/pipe-prefix": [
"error",
{
"prefixes": [
"ds"
]
}
],
"@angular-eslint/no-attribute-decorator": "error",
"@angular-eslint/no-forward-ref": "error",
"@angular-eslint/no-output-native": "warn",
"@angular-eslint/no-output-on-prefix": "warn",
"@angular-eslint/no-conflicting-lifecycle": "warn",
"@typescript-eslint/no-inferrable-types":[
"error",
{
"ignoreParameters": true
}
],
"@typescript-eslint/quotes": [
"error",
"single",
{
"avoidEscape": true,
"allowTemplateLiterals": true
}
],
"@typescript-eslint/semi": "error",
"@typescript-eslint/no-shadow": "error",
"@typescript-eslint/dot-notation": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "property",
"format": null
}
],
"@typescript-eslint/member-ordering": [
"error",
{
"default": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unified-signatures": "error",
"@typescript-eslint/ban-types": "warn", // todo: deal with {} type issues & re-enable
"@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/no-misused-promises": "warn",
"@typescript-eslint/restrict-plus-operands": "warn",
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/require-await": "off",
"deprecation/deprecation": "warn",
"import/order": "off",
"import/no-deprecated": "warn"
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {
// todo: re-enable & fix errors
"@angular-eslint/template/no-negated-async": "off",
"@angular-eslint/template/eqeqeq": "off"
}
}
]
}

View File

@@ -70,7 +70,10 @@ jobs:
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile
- name: Run lint - name: Run lint
run: yarn run lint run: yarn run lint --quiet
- name: Check for circular dependencies
run: yarn run check-circ-deps
- name: Run build - name: Run build
run: yarn run build:prod run: yarn run build:prod
@@ -128,6 +131,14 @@ jobs:
name: e2e-test-screenshots name: e2e-test-screenshots
path: cypress/screenshots path: cypress/screenshots
- name: Stop app (in case it stays up after e2e tests)
run: |
app_pid=$(lsof -t -i:4000)
if [[ ! -z $app_pid ]]; then
echo "App was still up! (PID: $app_pid)"
kill -9 $app_pid
fi
# Start up the app with SSR enabled (run in background) # Start up the app with SSR enabled (run in background)
- name: Start app in SSR (server-side rendering) mode - name: Start app in SSR (server-side rendering) mode
run: | run: |

View File

@@ -31,6 +31,10 @@ jobs:
# We turn off 'latest' tag by default. # We turn off 'latest' tag by default.
TAGS_FLAVOR: | TAGS_FLAVOR: |
latest=false latest=false
# Architectures / Platforms for which we will build Docker images
# If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work.
# If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64.
PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }}
steps: steps:
# https://github.com/actions/checkout # https://github.com/actions/checkout
@@ -41,6 +45,10 @@ jobs:
- name: Setup Docker Buildx - name: Setup Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU emulation to build for multiple architectures
uses: docker/setup-qemu-action@v2
# https://github.com/docker/login-action # https://github.com/docker/login-action
- 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
@@ -70,6 +78,7 @@ jobs:
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: ${{ env.PLATFORMS }}
# For pull requests, we run the Docker build (to ensure no PR changes break the build), # For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR # but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/.angular/cache
/__build__ /__build__
/__server_build__ /__server_build__
/node_modules /node_modules
@@ -36,3 +37,5 @@ package-lock.json
.env .env
/nbproject/ /nbproject/
junit.xml

View File

@@ -9,4 +9,9 @@ EXPOSE 4000
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com # We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
# See, for example https://github.com/yarnpkg/yarn/issues/5540 # See, for example https://github.com/yarnpkg/yarn/issues/5540
RUN yarn install --network-timeout 300000 RUN yarn install --network-timeout 300000
CMD yarn run start:dev
# On startup, run in DEVELOPMENT mode (this defaults to live reloading enabled, etc).
# Listen / accept connections from all IP addresses.
# NOTE: At this time it is only possible to run Docker container in Production mode
# if you have a public IP. See https://github.com/DSpace/dspace-angular/issues/1485
CMD yarn serve --host 0.0.0.0

View File

@@ -330,7 +330,10 @@ All E2E tests must be created under the `./cypress/integration/` folder, and mus
* In the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner), you'll Cypress automatically visit the page. This first test will succeed, as all you are doing is making sure the _page exists_. * In the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner), you'll Cypress automatically visit the page. This first test will succeed, as all you are doing is making sure the _page exists_.
* From here, you can use the [Selector Playground](https://docs.cypress.io/guides/core-concepts/test-runner#Selector-Playground) in the Cypress Test Runner window to determine how to tell Cypress to interact with a specific HTML element on that page. * From here, you can use the [Selector Playground](https://docs.cypress.io/guides/core-concepts/test-runner#Selector-Playground) in the Cypress Test Runner window to determine how to tell Cypress to interact with a specific HTML element on that page.
* Most commands start by telling Cypress to [get()](https://docs.cypress.io/api/commands/get) a specific element, using a CSS or jQuery style selector * Most commands start by telling Cypress to [get()](https://docs.cypress.io/api/commands/get) a specific element, using a CSS or jQuery style selector
* It's generally best not to rely on attributes like `class` and `id` in tests, as those are likely to change later on. Instead, you can add a `data-test` attribute to makes it clear that it's required for a test.
* Cypress can then do actions like [click()](https://docs.cypress.io/api/commands/click) an element, or [type()](https://docs.cypress.io/api/commands/type) text in an input field, etc. * Cypress can then do actions like [click()](https://docs.cypress.io/api/commands/click) an element, or [type()](https://docs.cypress.io/api/commands/type) text in an input field, etc.
* When running with server-side rendering enabled, the client first receives HTML without the JS; only once the page is rendered client-side do some elements (e.g. a button that toggles a Bootstrap dropdown) become fully interactive. This can trip up Cypress in some cases as it may try to `click` or `type` in an element that's not fully loaded yet, causing tests to fail.
* To work around this issue, define the attributes you use for Cypress selectors as `[attr.data-test]="'button' | ngBrowserOnly"`. This will only show the attribute in CSR HTML, forcing Cypress to wait until CSR is complete before interacting with the element.
* Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions. * Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions.
* Any time you save your test file, the Cypress Test Runner will reload & rerun it. This allows you can see your results quickly as you write the tests & correct any broken tests rapidly. * Any time you save your test file, the Cypress Test Runner will reload & rerun it. This allows you can see your results quickly as you write the tests & correct any broken tests rapidly.
* Cypress also has a great guide on [writing your first test](https://on.cypress.io/writing-first-test) with much more info. Keep in mind, while the examples in the Cypress docs often involve Javascript files (.js), the same examples will work in our Typescript (.ts) e2e tests. * Cypress also has a great guide on [writing your first test](https://on.cypress.io/writing-first-test) with much more info. Keep in mind, while the examples in the Cypress docs often involve Javascript files (.js), the same examples will work in our Typescript (.ts) e2e tests.

View File

@@ -17,7 +17,6 @@
"build": { "build": {
"builder": "@angular-builders/custom-webpack:browser", "builder": "@angular-builders/custom-webpack:browser",
"options": { "options": {
"extractCss": true,
"preserveSymlinks": true, "preserveSymlinks": true,
"customWebpackConfig": { "customWebpackConfig": {
"path": "./webpack/webpack.browser.ts", "path": "./webpack/webpack.browser.ts",
@@ -67,16 +66,27 @@
"scripts": [] "scripts": []
}, },
"configurations": { "configurations": {
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"production": { "production": {
"fileReplacements": [ "fileReplacements": [
{ {
"replace": "src/environments/environment.ts", "replace": "src/environments/environment.ts",
"with": "src/environments/environment.production.ts" "with": "src/environments/environment.production.ts"
},
{
"replace": "src/config/store/devtools.ts",
"with": "src/config/store/devtools.prod.ts"
} }
], ],
"optimization": true, "optimization": true,
"outputHashing": "all", "outputHashing": "all",
"extractCss": true,
"namedChunks": false, "namedChunks": false,
"aot": true, "aot": true,
"extractLicenses": true, "extractLicenses": true,
@@ -104,6 +114,9 @@
"port": 4000 "port": 4000
}, },
"configurations": { "configurations": {
"development": {
"browserTarget": "dspace-angular:build:development"
},
"production": { "production": {
"browserTarget": "dspace-angular:build:production" "browserTarget": "dspace-angular:build:production"
} }
@@ -157,19 +170,6 @@
} }
} }
}, },
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"cypress/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": { "e2e": {
"builder": "@cypress/schematic:cypress", "builder": "@cypress/schematic:cypress",
"options": { "options": {
@@ -197,6 +197,10 @@
"tsConfig": "tsconfig.server.json" "tsConfig": "tsconfig.server.json"
}, },
"configurations": { "configurations": {
"development": {
"sourceMap": true,
"optimization": false
},
"production": { "production": {
"sourceMap": false, "sourceMap": false,
"optimization": true, "optimization": true,
@@ -204,6 +208,10 @@
{ {
"replace": "src/environments/environment.ts", "replace": "src/environments/environment.ts",
"with": "src/environments/environment.production.ts" "with": "src/environments/environment.production.ts"
},
{
"replace": "src/config/store/devtools.ts",
"with": "src/config/store/devtools.prod.ts"
} }
] ]
} }
@@ -253,12 +261,22 @@
"watch": true, "watch": true,
"headless": false "headless": false
} }
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
} }
} }
} }
}, },
"defaultProject": "dspace-angular", "defaultProject": "dspace-angular",
"cli": { "cli": {
"analytics": false "analytics": false,
"defaultCollection": "@angular-eslint/schematics"
} }
} }

View File

@@ -148,6 +148,12 @@ languages:
- code: fi - code: fi
label: Suomi label: Suomi
active: true active: true
- code: tr
label: Türkçe
active: true
- code: bn
label: বাংলা
active: true
# Browse-By Pages # Browse-By Pages
browseBy: browseBy:
@@ -158,10 +164,12 @@ browseBy:
# The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items) # The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items)
defaultLowerLimit: 1900 defaultLowerLimit: 1900
# Item Page Config # Item Config
item: item:
edit: edit:
undoTimeout: 10000 # 10 seconds undoTimeout: 10000 # 10 seconds
# Show the item access status label in items lists
showAccessStatuses: false
# Collection Page Config # Collection Page Config
collection: collection:
@@ -228,6 +236,10 @@ themes:
rel: manifest rel: manifest
href: assets/dspace/images/favicons/manifest.webmanifest href: assets/dspace/images/favicons/manifest.webmanifest
# The default bundles that should always be displayed as suggestions when you upload a new bundle
bundle:
- standardBundles: [ ORIGINAL, THUMBNAIL, LICENSE ]
# Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with 'image' or 'video'). # Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with 'image' or 'video').
# For images, this enables a gallery viewer where you can zoom or page through images. # For images, this enables a gallery viewer where you can zoom or page through images.
# For videos, this enables embedded video streaming # For videos, this enables embedded video streaming

View File

@@ -65,7 +65,7 @@ describe('My DSpace page', () => {
cy.visit('/mydspace'); cy.visit('/mydspace');
// Open the New Submission dropdown // Open the New Submission dropdown
cy.get('#dropdownSubmission').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
cy.get('#entityControlsDropdownMenu button[title="none"]').click(); cy.get('#entityControlsDropdownMenu button[title="none"]').click();
@@ -98,7 +98,7 @@ describe('My DSpace page', () => {
const id = subpaths[2]; const id = subpaths[2];
// Click the "Save for Later" button to save this submission // Click the "Save for Later" button to save this submission
cy.get('button#saveForLater').click(); cy.get('ds-submission-form-footer [data-test="save-for-later"]').click();
// "Save for Later" should send us to MyDSpace // "Save for Later" should send us to MyDSpace
cy.url().should('include', '/mydspace'); cy.url().should('include', '/mydspace');
@@ -122,7 +122,7 @@ describe('My DSpace page', () => {
cy.url().should('include', '/workspaceitems/' + id + '/edit'); cy.url().should('include', '/workspaceitems/' + id + '/edit');
// Discard our new submission by clicking Discard in Submission form & confirming // Discard our new submission by clicking Discard in Submission form & confirming
cy.get('button#discard').click(); cy.get('ds-submission-form-footer [data-test="discard"]').click();
cy.get('button#discard_submit').click(); cy.get('button#discard_submit').click();
// Discarding should send us back to MyDSpace // Discarding should send us back to MyDSpace
@@ -135,7 +135,7 @@ describe('My DSpace page', () => {
cy.visit('/mydspace'); cy.visit('/mydspace');
// Open the New Import dropdown // Open the New Import dropdown
cy.get('#dropdownImport').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
cy.get('#importControlsDropdownMenu button[title="none"]').click(); cy.get('#importControlsDropdownMenu button[title="none"]').click();

View File

@@ -24,7 +24,7 @@ describe('Search Page', () => {
// Click each filter toggle to open *every* filter // Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well) // (As we want to scan filter section for accessibility issues as well)
cy.get('.filter-toggle').click({ multiple: true }); cy.get('[data-test="filter-toggle"]').click({ multiple: true });
// Analyze <ds-search-page> for accessibility issues // Analyze <ds-search-page> for accessibility issues
testA11y( testA11y(

View File

@@ -42,6 +42,7 @@ describe('New Submission page', () => {
cy.get('button#deposit').click(); cy.get('button#deposit').click();
// A warning alert should display. // A warning alert should display.
cy.get('ds-notification div.alert-success').should('not.exist');
cy.get('ds-notification div.alert-warning').should('be.visible'); cy.get('ds-notification div.alert-warning').should('be.visible');
// First section should have an exclamation error in the header // First section should have an exclamation error in the header

View File

@@ -21,7 +21,7 @@ import './commands';
import 'cypress-axe'; import 'cypress-axe';
// Runs once before the first test in each "block" // Runs once before the first test in each "block"
before(() => { beforeEach(() => {
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie // Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
// This just ensures it doesn't get in the way of matching other objects in the page. // This just ensures it doesn't get in the way of matching other objects in the page.
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true}'); cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true}');

View File

@@ -22,7 +22,7 @@ module.exports = function (config) {
reports: ['html', 'lcovonly', 'text-summary'], reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true fixWebpackSourcePaths: true
}, },
reporters: ['mocha', 'kjhtml'], reporters: ['mocha', 'kjhtml', 'coverage-istanbul'],
mochaReporter: { mochaReporter: {
ignoreSkipped: true, ignoreSkipped: true,
output: 'autowatch' output: 'autowatch'

View File

@@ -9,10 +9,11 @@
"start:dev": "nodemon --exec \"cross-env NODE_ENV=development yarn run serve\"", "start:dev": "nodemon --exec \"cross-env NODE_ENV=development yarn run serve\"",
"start:prod": "yarn run build:prod && cross-env NODE_ENV=production yarn run serve:ssr", "start:prod": "yarn run build:prod && cross-env NODE_ENV=production yarn run serve:ssr",
"start:mirador:prod": "yarn run build:mirador && yarn run start:prod", "start:mirador:prod": "yarn run build:mirador && yarn run start:prod",
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts", "preserve": "yarn base-href",
"serve": "ng serve --configuration development",
"serve:ssr": "node dist/server/main", "serve:ssr": "node dist/server/main",
"analyze": "webpack-bundle-analyzer dist/browser/stats.json", "analyze": "webpack-bundle-analyzer dist/browser/stats.json",
"build": "ng build", "build": "ng build --configuration development",
"build:stats": "ng build --stats-json", "build:stats": "ng build --stats-json",
"build:prod": "yarn run build:ssr", "build:prod": "yarn run build:ssr",
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production", "build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
@@ -36,7 +37,9 @@
"merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts", "merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:run": "cypress run", "cypress:run": "cypress run",
"env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts" "env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts",
"base-href": "ts-node --project ./tsconfig.ts-node.json scripts/base-href.ts",
"check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./"
}, },
"browser": { "browser": {
"fs": false, "fs": false,
@@ -47,33 +50,37 @@
"private": true, "private": true,
"resolutions": { "resolutions": {
"minimist": "^1.2.5", "minimist": "^1.2.5",
"webdriver-manager": "^12.1.8" "webdriver-manager": "^12.1.8",
"ts-node": "10.2.1"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "~11.2.14", "@angular/animations": "~13.2.6",
"@angular/cdk": "^11.2.13", "@angular/cdk": "^13.2.6",
"@angular/common": "~11.2.14", "@angular/common": "~13.2.6",
"@angular/compiler": "~11.2.14", "@angular/compiler": "~13.2.6",
"@angular/core": "~11.2.14", "@angular/core": "~13.2.6",
"@angular/forms": "~11.2.14", "@angular/forms": "~13.2.6",
"@angular/localize": "11.2.14", "@angular/localize": "13.2.6",
"@angular/platform-browser": "~11.2.14", "@angular/platform-browser": "~13.2.6",
"@angular/platform-browser-dynamic": "~11.2.14", "@angular/platform-browser-dynamic": "~13.2.6",
"@angular/platform-server": "~11.2.14", "@angular/platform-server": "~13.2.6",
"@angular/router": "~11.2.14", "@angular/router": "~13.2.6",
"@kolkov/ngx-gallery": "^1.2.3", "@babel/runtime": "^7.17.2",
"@ng-bootstrap/ng-bootstrap": "9.1.3", "@kolkov/ngx-gallery": "^2.0.1",
"@ng-dynamic-forms/core": "^13.0.0", "@material-ui/core": "^4.11.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "^13.0.0", "@material-ui/icons": "^4.9.1",
"@ngrx/effects": "^11.1.1", "@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@ngrx/router-store": "^11.1.1", "@ng-dynamic-forms/core": "^15.0.0",
"@ngrx/store": "^11.1.1", "@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0",
"@nguniversal/express-engine": "11.2.1", "@ngrx/effects": "^13.0.2",
"@ngrx/router-store": "^13.0.2",
"@ngrx/store": "^13.0.2",
"@nguniversal/express-engine": "^13.0.2",
"@ngx-translate/core": "^13.0.0", "@ngx-translate/core": "^13.0.0",
"@nicky-lenaers/ngx-scroll-to": "^9.0.0", "@nicky-lenaers/ngx-scroll-to": "^9.0.0",
"angular-idle-preload": "3.0.0", "angular-idle-preload": "3.0.0",
"angular2-text-mask": "9.0.0", "angulartics2": "^12.0.0",
"angulartics2": "^10.0.0", "axios": "^0.27.2",
"bootstrap": "4.3.1", "bootstrap": "4.3.1",
"caniuse-lite": "^1.0.30001165", "caniuse-lite": "^1.0.30001165",
"cerialize": "0.1.18", "cerialize": "0.1.18",
@@ -100,9 +107,9 @@
"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.1", "moment": "^2.29.2",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ng-mocks": "11.11.2", "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",
@@ -111,27 +118,35 @@
"ngx-sortablejs": "^11.1.0", "ngx-sortablejs": "^11.1.0",
"nouislider": "^14.6.3", "nouislider": "^14.6.3",
"pem": "1.14.4", "pem": "1.14.4",
"postcss-cli": "^8.3.0", "postcss-cli": "^9.1.0",
"prop-types": "^15.7.2",
"react-copy-to-clipboard": "^5.0.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^6.6.3", "rxjs": "^7.5.5",
"sortablejs": "1.13.0", "sortablejs": "1.13.0",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"url-parse": "^1.5.6", "url-parse": "^1.5.6",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"webfontloader": "1.6.28", "webfontloader": "1.6.28",
"zone.js": "^0.10.3" "zone.js": "~0.11.5",
"ngx-ui-switch": "^11.0.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "10.0.1", "@angular-builders/custom-webpack": "~13.1.0",
"@angular-devkit/build-angular": "~0.1102.15", "@angular-devkit/build-angular": "~13.2.6",
"@angular/cli": "~11.2.15", "@angular-eslint/builder": "13.1.0",
"@angular/compiler-cli": "~11.2.14", "@angular-eslint/eslint-plugin": "13.1.0",
"@angular/language-service": "~11.2.14", "@angular-eslint/eslint-plugin-template": "13.1.0",
"@angular-eslint/schematics": "13.1.0",
"@angular-eslint/template-parser": "13.1.0",
"@angular/cli": "~13.2.6",
"@angular/compiler-cli": "~13.2.6",
"@angular/language-service": "~13.2.6",
"@cypress/schematic": "^1.5.0", "@cypress/schematic": "^1.5.0",
"@fortawesome/fontawesome-free": "^5.5.0", "@fortawesome/fontawesome-free": "^5.5.0",
"@ngrx/store-devtools": "^11.1.1", "@ngrx/store-devtools": "^13.0.2",
"@ngtools/webpack": "10.2.3", "@ngtools/webpack": "^13.2.6",
"@nguniversal/builders": "~11.2.1", "@nguniversal/builders": "^13.0.2",
"@types/deep-freeze": "0.1.2", "@types/deep-freeze": "0.1.2",
"@types/express": "^4.17.9", "@types/express": "^4.17.9",
"@types/file-saver": "^2.0.1", "@types/file-saver": "^2.0.1",
@@ -140,23 +155,30 @@
"@types/js-cookie": "2.2.6", "@types/js-cookie": "2.2.6",
"@types/lodash": "^4.14.165", "@types/lodash": "^4.14.165",
"@types/node": "^14.14.9", "@types/node": "^14.14.9",
"@typescript-eslint/eslint-plugin": "5.11.0",
"@typescript-eslint/parser": "5.11.0",
"axe-core": "^4.3.3", "axe-core": "^4.3.3",
"codelyzer": "^6.0.0", "compression-webpack-plugin": "^9.2.0",
"compression-webpack-plugin": "^3.0.1",
"copy-webpack-plugin": "^6.4.1", "copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "3.4.0", "css-loader": "^6.2.0",
"cssnano": "^4.1.10", "css-minimizer-webpack-plugin": "^3.4.1",
"cssnano": "^5.0.6",
"cypress": "9.5.1", "cypress": "9.5.1",
"cypress-axe": "^0.13.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",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"eslint": "^8.2.0",
"eslint-plugin-deprecation": "^1.3.2",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsdoc": "^38.0.6",
"eslint-plugin-unused-imports": "^2.0.0",
"express-static-gzip": "^2.1.5",
"fork-ts-checker-webpack-plugin": "^6.0.3", "fork-ts-checker-webpack-plugin": "^6.0.3",
"html-loader": "^1.3.2", "html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.0", "jasmine-core": "^3.8.0",
"jasmine-core": "~3.6.0", "jasmine-marbles": "0.9.2",
"jasmine-marbles": "0.6.0",
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "~5.0.0",
"karma": "^6.3.14", "karma": "^6.3.14",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.1.0",
@@ -164,12 +186,13 @@
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "^1.5.0",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"ngx-mask": "^13.1.7",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",
"optimize-css-assets-webpack-plugin": "^5.0.4", "postcss": "^8.1",
"postcss-apply": "0.11.0", "postcss-apply": "0.12.0",
"postcss-import": "^12.0.1", "postcss-import": "^14.0.0",
"postcss-loader": "^3.0.0", "postcss-loader": "^4.0.3",
"postcss-preset-env": "6.7.0", "postcss-preset-env": "^7.4.2",
"postcss-responsive-type": "1.0.0", "postcss-responsive-type": "1.0.0",
"protractor": "^7.0.0", "protractor": "^7.0.0",
"protractor-istanbul-plugin": "2.0.0", "protractor-istanbul-plugin": "2.0.0",
@@ -177,16 +200,16 @@
"react": "^16.14.0", "react": "^16.14.0",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs-spy": "^7.5.3", "rxjs-spy": "^8.0.2",
"sass": "~1.32.6",
"sass-loader": "^12.6.0",
"sass-resources-loader": "^2.1.1", "sass-resources-loader": "^2.1.1",
"script-ext-html-webpack-plugin": "2.1.5", "string-replace-loader": "^3.1.0",
"string-replace-loader": "^2.3.0",
"terser-webpack-plugin": "^2.3.1", "terser-webpack-plugin": "^2.3.1",
"ts-loader": "^5.2.0", "ts-loader": "^5.2.0",
"ts-node": "^8.10.2", "ts-node": "^8.10.2",
"tslint": "^6.1.3", "typescript": "~4.5.5",
"typescript": "~4.0.5", "webpack": "^5.69.1",
"webpack": "^4.44.2",
"webpack-bundle-analyzer": "^4.4.0", "webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.2.0", "webpack-cli": "^4.2.0",
"webpack-dev-server": "^4.5.0" "webpack-dev-server": "^4.5.0"

36
scripts/base-href.ts Normal file
View File

@@ -0,0 +1,36 @@
import * as fs from 'fs';
import { join } from 'path';
import { AppConfig } from '../src/config/app-config.interface';
import { buildAppConfig } from '../src/config/config.server';
/**
* Script to set baseHref as `ui.nameSpace` for development mode. Adds `baseHref` to angular.json build options.
*
* Usage (see package.json):
*
* yarn base-href
*/
const appConfig: AppConfig = buildAppConfig();
const angularJsonPath = join(process.cwd(), 'angular.json');
if (!fs.existsSync(angularJsonPath)) {
console.error(`Error:\n${angularJsonPath} does not exist\n`);
process.exit(1);
}
try {
const angularJson = require(angularJsonPath);
const baseHref = `${appConfig.ui.nameSpace}${appConfig.ui.nameSpace.endsWith('/') ? '' : '/'}`;
console.log(`Setting baseHref to ${baseHref} in angular.json`);
angularJson.projects['dspace-angular'].architect.build.options.baseHref = baseHref;
fs.writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n');
} catch (e) {
console.error(e);
}

View File

@@ -1,4 +1,5 @@
import { projectRoot} from '../webpack/helpers'; import { projectRoot } from '../webpack/helpers';
const commander = require('commander'); const commander = require('commander');
const fs = require('fs'); const fs = require('fs');
const JSON5 = require('json5'); const JSON5 = require('json5');
@@ -119,7 +120,7 @@ function syncFileWithSource(pathToTargetFile, pathToOutputFile) {
outputChunks.forEach(function (chunk) { outputChunks.forEach(function (chunk) {
progressBar.increment(); progressBar.increment();
chunk.split("\n").forEach(function (line) { chunk.split("\n").forEach(function (line) {
file.write(" " + line + "\n"); file.write((line === '' ? '' : ` ${line}`) + "\n");
}); });
}); });
file.write("\n}"); file.write("\n}");
@@ -192,7 +193,10 @@ function createNewChunkComparingSourceAndTarget(correspondingTargetChunk, source
const targetList = correspondingTargetChunk.split("\n"); const targetList = correspondingTargetChunk.split("\n");
const oldKeyValueInTargetComments = getSubStringWithRegex(correspondingTargetChunk, "\\s*\\/\\/\\s*\".*"); const oldKeyValueInTargetComments = getSubStringWithRegex(correspondingTargetChunk, "\\s*\\/\\/\\s*\".*");
const keyValueTarget = targetList[targetList.length - 1]; let keyValueTarget = targetList[targetList.length - 1];
if (!keyValueTarget.endsWith(",")) {
keyValueTarget = keyValueTarget + ",";
}
if (oldKeyValueInTargetComments != null) { if (oldKeyValueInTargetComments != null) {
const oldKeyValueUncommented = getSubStringWithRegex(oldKeyValueInTargetComments[0], "\".*")[0]; const oldKeyValueUncommented = getSubStringWithRegex(oldKeyValueInTargetComments[0], "\".*")[0];

View File

@@ -15,16 +15,18 @@
* import for `ngExpressEngine`. * import for `ngExpressEngine`.
*/ */
import 'zone.js/dist/zone-node'; import 'zone.js/node';
import 'reflect-metadata'; import 'reflect-metadata';
import 'rxjs'; import 'rxjs';
import axios from 'axios';
import * as pem from 'pem'; import * as pem from 'pem';
import * as https from 'https'; 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 bodyParser from 'body-parser';
import * as compression from 'compression'; import * as compression from 'compression';
import * as expressStaticGzip from 'express-static-gzip';
import { existsSync, readFileSync } from 'fs'; import { existsSync, readFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
@@ -37,14 +39,14 @@ import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
import { environment } from './src/environments/environment'; import { environment } from './src/environments/environment';
import { createProxyMiddleware } from 'http-proxy-middleware'; import { createProxyMiddleware } from 'http-proxy-middleware';
import { hasValue, hasNoValue } from './src/app/shared/empty.util'; import { hasNoValue, hasValue } from './src/app/shared/empty.util';
import { UIServerConfig } from './src/config/ui-server-config.interface'; import { UIServerConfig } from './src/config/ui-server-config.interface';
import { ServerAppModule } from './src/main.server'; import { ServerAppModule } from './src/main.server';
import { buildAppConfig } from './src/config/config.server'; import { buildAppConfig } from './src/config/config.server';
import { AppConfig, APP_CONFIG } from './src/config/app-config.interface'; import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
import { extendEnvironmentWithAppConfig } from './src/config/config.util'; import { extendEnvironmentWithAppConfig } from './src/config/config.util';
/* /*
@@ -66,6 +68,8 @@ extendEnvironmentWithAppConfig(environment, appConfig);
// The Express app is exported so that it can be used by serverless Functions. // The Express app is exported so that it can be used by serverless Functions.
export function app() { export function app() {
const router = express.Router();
/* /*
* Create a new express application * Create a new express application
*/ */
@@ -74,11 +78,15 @@ export function app() {
/* /*
* If production mode is enabled in the environment file: * If production mode is enabled in the environment file:
* - Enable Angular's production mode * - Enable Angular's production mode
* - Enable compression for response bodies. See [compression](https://github.com/expressjs/compression) * - Enable compression for SSR reponses. See [compression](https://github.com/expressjs/compression)
*/ */
if (environment.production) { if (environment.production) {
enableProdMode(); enableProdMode();
server.use(compression()); server.use(compression({
// only compress responses we've marked as SSR
// otherwise, this middleware may compress files we've chosen not to compress via compression-webpack-plugin
filter: (_, res) => res.locals.ssr,
}));
} }
/* /*
@@ -133,7 +141,11 @@ export function app() {
/** /**
* Proxy the sitemaps * Proxy the sitemaps
*/ */
server.use('/sitemap**', createProxyMiddleware({ target: `${environment.rest.baseUrl}/sitemaps`, changeOrigin: true })); router.use('/sitemap**', createProxyMiddleware({
target: `${environment.rest.baseUrl}/sitemaps`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true
}));
/** /**
* Checks if the rateLimiter property is present * Checks if the rateLimiter property is present
@@ -150,15 +162,28 @@ export function app() {
/* /*
* Serve static resources (images, i18n messages, …) * Serve static resources (images, i18n messages, …)
* Handle pre-compressed files with [express-static-gzip](https://github.com/tkoenig89/express-static-gzip)
*/ */
server.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false })); router.get('*.*', cacheControl, expressStaticGzip(DIST_FOLDER, {
index: false,
enableBrotli: true,
orderPreference: ['br', 'gzip'],
}));
/* /*
* Fallthrough to the IIIF viewer (must be included in the build). * Fallthrough to the IIIF viewer (must be included in the build).
*/ */
server.use('/iiif', express.static(IIIF_VIEWER, {index:false})); router.use('/iiif', express.static(IIIF_VIEWER, { index: false }));
/**
* Checking server status
*/
server.get('/app/health', healthCheck);
// Register the ngApp callback function to handle incoming requests // Register the ngApp callback function to handle incoming requests
server.get('*', ngApp); router.get('*', ngApp);
server.use(environment.ui.nameSpace, router);
return server; return server;
} }
@@ -180,6 +205,7 @@ function ngApp(req, res) {
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
}, (err, data) => { }, (err, data) => {
if (hasNoValue(err) && hasValue(data)) { if (hasNoValue(err) && hasValue(data)) {
res.locals.ssr = true; // mark response as SSR
res.send(data); res.send(data);
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') { } else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
// When this error occurs we can't fall back to CSR because the response has already been // When this error occurs we can't fall back to CSR because the response has already been
@@ -191,13 +217,25 @@ function ngApp(req, res) {
if (hasValue(err)) { if (hasValue(err)) {
console.warn('Error details : ', err); console.warn('Error details : ', err);
} }
res.sendFile(DIST_FOLDER + '/index.html'); res.render(indexHtml, {
req,
providers: [{
provide: APP_BASE_HREF,
useValue: req.baseUrl
}]
});
} }
}); });
} else { } else {
// If preboot is disabled, just serve the client // If preboot is disabled, just serve the client
console.log('Universal off, serving for direct CSR'); console.log('Universal off, serving for direct CSR');
res.sendFile(DIST_FOLDER + '/index.html'); res.render(indexHtml, {
req,
providers: [{
provide: APP_BASE_HREF,
useValue: req.baseUrl
}]
});
} }
} }
@@ -287,6 +325,21 @@ function start() {
} }
} }
/*
* The callback function to serve health check requests
*/
function healthCheck(req, res) {
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
axios.get(baseUrl)
.then((response) => {
res.status(response.status).send(response.data);
})
.catch((error) => {
res.status(error.response.status).send({
error: error.message
});
});
}
// Webpack will replace 'require' with '__webpack_require__' // Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require' // '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle. // The below code is to ensure that the server is run only when not requiring the bundle.

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-classes-per-file */
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { type } from '../../shared/ngrx/type'; import { type } from '../../shared/ngrx/type';
@@ -16,7 +17,6 @@ export const EPeopleRegistryActionTypes = {
CANCEL_EDIT_EPERSON: type('dspace/epeople-registry/CANCEL_EDIT_EPERSON'), CANCEL_EDIT_EPERSON: type('dspace/epeople-registry/CANCEL_EDIT_EPERSON'),
}; };
/* tslint:disable:max-classes-per-file */
/** /**
* Used to edit an EPerson in the EPeople registry * Used to edit an EPerson in the EPeople registry
*/ */
@@ -37,7 +37,6 @@ export class EPeopleRegistryCancelEPersonAction implements Action {
type = EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON; type = EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON;
} }
/* tslint:enable:max-classes-per-file */
/** /**
* Export a type alias of all actions in this action group * Export a type alias of all actions in this action group

View File

@@ -9,7 +9,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { FindListOptions } from '../../core/data/request.models';
import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
@@ -27,6 +26,7 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
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 { 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('EPeopleRegistryComponent', () => { describe('EPeopleRegistryComponent', () => {
let component: EPeopleRegistryComponent; let component: EPeopleRegistryComponent;

View File

@@ -8,7 +8,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { FindListOptions } from '../../../core/data/request.models';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../../core/eperson/models/eperson.model';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
@@ -29,6 +28,7 @@ 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 { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../../core/data/find-list-options.model';
import { ValidateEmailNotTaken } from './validators/email-taken.validator'; import { ValidateEmailNotTaken } from './validators/email-taken.validator';
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service'; import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';

View File

@@ -2,8 +2,8 @@ import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule, FormArray, FormControl, FormGroup,Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule, By } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@@ -35,6 +35,7 @@ import { RouterMock } from '../../../shared/mocks/router.mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { ValidateGroupExists } from './validators/group-exists.validator'; import { ValidateGroupExists } from './validators/group-exists.validator';
import { NoContent } from '../../../core/shared/NoContent.model';
describe('GroupFormComponent', () => { describe('GroupFormComponent', () => {
let component: GroupFormComponent; let component: GroupFormComponent;
@@ -87,6 +88,9 @@ describe('GroupFormComponent', () => {
patch(group: Group, operations: Operation[]) { patch(group: Group, operations: Operation[]) {
return null; return null;
}, },
delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
return createSuccessfulRemoteDataObject$({});
},
cancelEditGroup(): void { cancelEditGroup(): void {
this.activeGroup = null; this.activeGroup = null;
}, },
@@ -348,4 +352,46 @@ describe('GroupFormComponent', () => {
}); });
}); });
describe('delete', () => {
let deleteButton;
beforeEach(() => {
component.initialisePage();
component.canEdit$ = observableOf(true);
component.groupBeingEdited = {
permanent: false
} as Group;
fixture.detectChanges();
deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement;
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf({ id: 'active-group' }));
});
describe('if confirmed via modal', () => {
beforeEach(waitForAsync(() => {
deleteButton.click();
fixture.detectChanges();
(document as any).querySelector('.modal-footer .confirm').click();
}));
it('should call GroupDataService.delete', () => {
expect(groupsDataServiceStub.delete).toHaveBeenCalledWith('active-group');
});
});
describe('if canceled via modal', () => {
beforeEach(waitForAsync(() => {
deleteButton.click();
fixture.detectChanges();
(document as any).querySelector('.modal-footer .cancel').click();
}));
it('should not call GroupDataService.delete', () => {
expect(groupsDataServiceStub.delete).not.toHaveBeenCalled();
});
});
});
}); });

View File

@@ -426,7 +426,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
.subscribe((rd: RemoteData<NoContent>) => { .subscribe((rd: RemoteData<NoContent>) => {
if (rd.hasSucceeded) { if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: group.name })); this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: group.name }));
this.reset(); this.onCancel();
} else { } else {
this.notificationsService.error( this.notificationsService.error(
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: group.name }), this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: group.name }),
@@ -439,16 +439,6 @@ export class GroupFormComponent implements OnInit, OnDestroy {
}); });
} }
/**
* This method will ensure that the page gets reset and that the cache is cleared
*/
reset() {
this.groupDataService.getBrowseEndpoint().pipe(take(1)).subscribe((href: string) => {
this.requestService.removeByHrefSubstring(href);
});
this.onCancel();
}
/** /**
* Cancel the current edit when component is destroyed & unsub all subscriptions * Cancel the current edit when component is destroyed & unsub all subscriptions
*/ */

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-classes-per-file */
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { Group } from '../../core/eperson/models/group.model'; import { Group } from '../../core/eperson/models/group.model';
import { type } from '../../shared/ngrx/type'; import { type } from '../../shared/ngrx/type';
@@ -16,7 +17,6 @@ export const GroupRegistryActionTypes = {
CANCEL_EDIT_GROUP: type('dspace/epeople-registry/CANCEL_EDIT_GROUP'), CANCEL_EDIT_GROUP: type('dspace/epeople-registry/CANCEL_EDIT_GROUP'),
}; };
/* tslint:disable:max-classes-per-file */
/** /**
* Used to edit a Group in the Group registry * Used to edit a Group in the Group registry
*/ */
@@ -37,7 +37,6 @@ export class GroupRegistryCancelGroupAction implements Action {
type = GroupRegistryActionTypes.CANCEL_EDIT_GROUP; type = GroupRegistryActionTypes.CANCEL_EDIT_GROUP;
} }
/* tslint:enable:max-classes-per-file */
/** /**
* Export a type alias of all actions in this action group * Export a type alias of all actions in this action group

View File

@@ -79,7 +79,7 @@
</button> </button>
</ng-container> </ng-container>
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete" <button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm" (click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm btn-delete"
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}"> title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>

View File

@@ -31,6 +31,7 @@ import { RouterMock } from '../../shared/mocks/router.mock';
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 { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { NoContent } from '../../core/shared/NoContent.model';
describe('GroupRegistryComponent', () => { describe('GroupRegistryComponent', () => {
let component: GroupsRegistryComponent; let component: GroupsRegistryComponent;
@@ -145,7 +146,10 @@ describe('GroupRegistryComponent', () => {
totalPages: 1, totalPages: 1,
currentPage: 1 currentPage: 1
}), [result])); }), [result]));
} },
delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
return createSuccessfulRemoteDataObject$({});
},
}; };
dsoDataServiceStub = { dsoDataServiceStub = {
findByHref(href: string): Observable<RemoteData<DSpaceObject>> { findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
@@ -301,4 +305,29 @@ describe('GroupRegistryComponent', () => {
}); });
}); });
}); });
describe('delete', () => {
let deleteButton;
beforeEach(fakeAsync(() => {
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
setIsAuthorized(true, true);
// force rerender after setup changes
component.search({ query: '' });
tick();
fixture.detectChanges();
// only mockGroup[0] is deletable, so we should only get one button
deleteButton = fixture.debugElement.query(By.css('.btn-delete')).nativeElement;
}));
it('should call GroupDataService.delete', () => {
deleteButton.click();
fixture.detectChanges();
expect(groupsDataServiceStub.delete).toHaveBeenCalledWith(mockGroups[0].id);
});
});
}); });

View File

@@ -9,7 +9,7 @@ import {
of as observableOf, of as observableOf,
Subscription Subscription
} from 'rxjs'; } from 'rxjs';
import { catchError, map, switchMap, take, tap } from 'rxjs/operators'; import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
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 { FeatureID } from '../../core/data/feature-authorization/feature-id';
@@ -199,7 +199,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
if (rd.hasSucceeded) { if (rd.hasSucceeded) {
this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id]; this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id];
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.group.name })); this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.group.name }));
this.reset();
} else { } else {
this.notificationsService.error( this.notificationsService.error(
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.group.name }), this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.group.name }),
@@ -209,17 +208,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
} }
} }
/**
* This method will set everything to stale, which will cause the lists on this page to update.
*/
reset() {
this.groupService.getBrowseEndpoint().pipe(
take(1)
).subscribe((href: string) => {
this.requestService.setStaleByHrefSubstring(href);
});
}
/** /**
* Get the members (epersons embedded value of a group) * Get the members (epersons embedded value of a group)
* @param group * @param group

View File

@@ -1,6 +1,17 @@
<div class="container"> <div class="container">
<h2 id="header">{{'admin.metadata-import.page.header' | translate}}</h2> <h2 id="header">{{'admin.metadata-import.page.header' | translate}}</h2>
<p>{{'admin.metadata-import.page.help' | translate}}</p> <p>{{'admin.metadata-import.page.help' | translate}}</p>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
<label class="form-check-label" for="validateOnly">
{{'admin.metadata-import.page.validateOnly' | translate}}
</label>
</div>
<small id="validateOnlyHelpBlock" class="form-text text-muted">
{{'admin.metadata-import.page.validateOnly.hint' | translate}}
</small>
</div>
<ds-file-dropzone-no-uploader <ds-file-dropzone-no-uploader
(onFileAdded)="setFile($event)" (onFileAdded)="setFile($event)"
@@ -8,8 +19,10 @@
[dropMessageLabelReplacement]="'admin.metadata-import.page.dropMsgReplace'"> [dropMessageLabelReplacement]="'admin.metadata-import.page.dropMsgReplace'">
</ds-file-dropzone-no-uploader> </ds-file-dropzone-no-uploader>
<div class="space-children-mr">
<button class="btn btn-secondary" id="backButton" <button class="btn btn-secondary" id="backButton"
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button> (click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
<button class="btn btn-primary" id="proceedButton" <button class="btn btn-primary" id="proceedButton"
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button> (click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
</div>
</div> </div>

View File

@@ -87,8 +87,9 @@ describe('MetadataImportPageComponent', () => {
comp.setFile(fileMock); comp.setFile(fileMock);
}); });
describe('if proceed button is pressed', () => { describe('if proceed button is pressed without validate only', () => {
beforeEach(fakeAsync(() => { beforeEach(fakeAsync(() => {
comp.validateOnly = false;
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
proceed.click(); proceed.click();
fixture.detectChanges(); fixture.detectChanges();
@@ -107,6 +108,28 @@ describe('MetadataImportPageComponent', () => {
}); });
}); });
describe('if proceed button is pressed with validate only', () => {
beforeEach(fakeAsync(() => {
comp.validateOnly = true;
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
proceed.click();
fixture.detectChanges();
}));
it('metadata-import script is invoked with -f fileName and the mockFile and -v validate-only', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }),
Object.assign(new ProcessParameter(), { name: '-v', value: true }),
];
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
});
it('success notification is shown', () => {
expect(notificationService.success).toHaveBeenCalled();
});
it('redirected to process page', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
});
});
describe('if proceed is pressed; but script invoke fails', () => { describe('if proceed is pressed; but script invoke fails', () => {
beforeEach(fakeAsync(() => { beforeEach(fakeAsync(() => {
jasmine.getEnv().allowRespy(true); jasmine.getEnv().allowRespy(true);

View File

@@ -30,6 +30,11 @@ export class MetadataImportPageComponent {
*/ */
fileObject: File; fileObject: File;
/**
* The validate only flag
*/
validateOnly = true;
public constructor(private location: Location, public constructor(private location: Location,
protected translate: TranslateService, protected translate: TranslateService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
@@ -62,6 +67,9 @@ export class MetadataImportPageComponent {
const parameterValues: ProcessParameter[] = [ const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }), Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
]; ];
if (this.validateOnly) {
parameterValues.push(Object.assign(new ProcessParameter(), { name: '-v', value: true }));
}
this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe( this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-classes-per-file */
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { type } from '../../../shared/ngrx/type'; import { type } from '../../../shared/ngrx/type';
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
@@ -17,7 +18,6 @@ export const BitstreamFormatsRegistryActionTypes = {
DESELECT_ALL_FORMAT: type('dspace/bitstream-formats-registry/DESELECT_ALL_FORMAT') DESELECT_ALL_FORMAT: type('dspace/bitstream-formats-registry/DESELECT_ALL_FORMAT')
}; };
/* tslint:disable:max-classes-per-file */
/** /**
* Used to select a single bitstream format in the bitstream format registry * Used to select a single bitstream format in the bitstream format registry
*/ */
@@ -51,7 +51,6 @@ export class BitstreamFormatsRegistryDeselectAllAction implements Action {
type = BitstreamFormatsRegistryActionTypes.DESELECT_ALL_FORMAT; type = BitstreamFormatsRegistryActionTypes.DESELECT_ALL_FORMAT;
} }
/* tslint:enable:max-classes-per-file */
/** /**
* Export a type alias of all actions in this action group * Export a type alias of all actions in this action group

View File

@@ -25,9 +25,9 @@ import {
import { createPaginatedList } from '../../../shared/testing/utils.test'; import { createPaginatedList } from '../../../shared/testing/utils.test';
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 { FindListOptions } from '../../../core/data/request.models';
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('BitstreamFormatsComponent', () => { describe('BitstreamFormatsComponent', () => {
let comp: BitstreamFormatsComponent; let comp: BitstreamFormatsComponent;

View File

@@ -5,7 +5,6 @@ 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 { FindListOptions } from '../../../core/data/request.models';
import { map, switchMap, take } from 'rxjs/operators'; import { map, switchMap, take } from 'rxjs/operators';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
@@ -13,6 +12,7 @@ import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { NoContent } from '../../../core/shared/NoContent.model'; import { NoContent } from '../../../core/shared/NoContent.model';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { FindListOptions } from '../../../core/data/find-list-options.model';
/** /**
* This component renders a list of bitstream formats * This component renders a list of bitstream formats

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-classes-per-file */
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { type } from '../../../shared/ngrx/type'; import { type } from '../../../shared/ngrx/type';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
@@ -26,7 +27,6 @@ export const MetadataRegistryActionTypes = {
DESELECT_ALL_FIELD: type('dspace/metadata-registry/DESELECT_ALL_FIELD') DESELECT_ALL_FIELD: type('dspace/metadata-registry/DESELECT_ALL_FIELD')
}; };
/* tslint:disable:max-classes-per-file */
/** /**
* Used to edit a metadata schema in the metadata registry * Used to edit a metadata schema in the metadata registry
*/ */
@@ -133,7 +133,6 @@ export class MetadataRegistryDeselectAllFieldAction implements Action {
type = MetadataRegistryActionTypes.DESELECT_ALL_FIELD; type = MetadataRegistryActionTypes.DESELECT_ALL_FIELD;
} }
/* tslint:enable:max-classes-per-file */
/** /**
* Export a type alias of all actions in this action group * Export a type alias of all actions in this action group

View File

@@ -21,8 +21,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.u
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
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 { FindListOptions } from '../../../core/data/request.models';
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;
@@ -52,7 +52,7 @@ describe('MetadataRegistryComponent', () => {
} }
]; ];
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList)); const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
/* tslint:disable:no-empty */ /* eslint-disable no-empty,@typescript-eslint/no-empty-function */
const registryServiceStub = { const registryServiceStub = {
getMetadataSchemas: () => mockSchemas, getMetadataSchemas: () => mockSchemas,
getActiveMetadataSchema: () => observableOf(undefined), getActiveMetadataSchema: () => observableOf(undefined),
@@ -66,7 +66,7 @@ describe('MetadataRegistryComponent', () => {
}, },
clearMetadataSchemaRequests: () => observableOf(undefined) clearMetadataSchemaRequests: () => observableOf(undefined)
}; };
/* tslint:enable:no-empty */ /* eslint-enable no-empty, @typescript-eslint/no-empty-function */
paginationService = new PaginationServiceStub(); paginationService = new PaginationServiceStub();

View File

@@ -128,7 +128,6 @@ export class MetadataRegistryComponent {
* Delete all the selected metadata schemas * Delete all the selected metadata schemas
*/ */
deleteSchemas() { deleteSchemas() {
this.registryService.clearMetadataSchemaRequests().subscribe();
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe( this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
(schemas) => { (schemas) => {
const tasks$ = []; const tasks$ = [];
@@ -148,7 +147,6 @@ export class MetadataRegistryComponent {
} }
this.registryService.deselectAllMetadataSchema(); this.registryService.deselectAllMetadataSchema();
this.registryService.cancelEditMetadataSchema(); this.registryService.cancelEditMetadataSchema();
this.forceUpdateSchemas();
}); });
} }
); );

View File

@@ -17,7 +17,7 @@ describe('MetadataSchemaFormComponent', () => {
let fixture: ComponentFixture<MetadataSchemaFormComponent>; let fixture: ComponentFixture<MetadataSchemaFormComponent>;
let registryService: RegistryService; let registryService: RegistryService;
/* tslint:disable:no-empty */ /* eslint-disable no-empty,@typescript-eslint/no-empty-function */
const registryServiceStub = { const registryServiceStub = {
getActiveMetadataSchema: () => observableOf(undefined), getActiveMetadataSchema: () => observableOf(undefined),
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema), createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
@@ -33,7 +33,7 @@ describe('MetadataSchemaFormComponent', () => {
}; };
} }
}; };
/* tslint:enable:no-empty */ /* eslint-enable no-empty, @typescript-eslint/no-empty-function */
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({

View File

@@ -24,7 +24,7 @@ describe('MetadataFieldFormComponent', () => {
prefix: 'fake' prefix: 'fake'
}); });
/* tslint:disable:no-empty */ /* eslint-disable no-empty,@typescript-eslint/no-empty-function */
const registryServiceStub = { const registryServiceStub = {
getActiveMetadataField: () => observableOf(undefined), getActiveMetadataField: () => observableOf(undefined),
createMetadataField: (field: MetadataField) => observableOf(field), createMetadataField: (field: MetadataField) => observableOf(field),
@@ -43,7 +43,7 @@ describe('MetadataFieldFormComponent', () => {
}; };
} }
}; };
/* tslint:enable:no-empty */ /* eslint-enable no-empty, @typescript-eslint/no-empty-function */
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({

View File

@@ -25,9 +25,9 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.u
import { VarDirective } from '../../../shared/utils/var.directive'; import { VarDirective } from '../../../shared/utils/var.directive';
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 { FindListOptions } from '../../../core/data/request.models';
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;
@@ -106,7 +106,7 @@ describe('MetadataSchemaComponent', () => {
} }
]; ];
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList)); const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
/* tslint:disable:no-empty */ /* eslint-disable no-empty,@typescript-eslint/no-empty-function */
const registryServiceStub = { const registryServiceStub = {
getMetadataSchemas: () => mockSchemas, getMetadataSchemas: () => mockSchemas,
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))), getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))),
@@ -122,7 +122,7 @@ describe('MetadataSchemaComponent', () => {
}, },
clearMetadataFieldRequests: () => observableOf(undefined) clearMetadataFieldRequests: () => observableOf(undefined)
}; };
/* tslint:enable:no-empty */ /* eslint-enable no-empty, @typescript-eslint/no-empty-function */
const schemaNameParam = 'mock'; const schemaNameParam = 'mock';
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), { const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
params: observableOf({ params: observableOf({

View File

@@ -174,15 +174,12 @@ export class MetadataSchemaComponent implements OnInit {
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed); const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
if (successResponses.length > 0) { if (successResponses.length > 0) {
this.showNotification(true, successResponses.length); this.showNotification(true, successResponses.length);
this.registryService.clearMetadataFieldRequests();
} }
if (failedResponses.length > 0) { if (failedResponses.length > 0) {
this.showNotification(false, failedResponses.length); this.showNotification(false, failedResponses.length);
} }
this.registryService.deselectAllMetadataField(); this.registryService.deselectAllMetadataField();
this.registryService.cancelEditMetadataField(); this.registryService.cancelEditMetadataField();
this.forceUpdateFields();
}); });
} }
); );

View File

@@ -18,6 +18,8 @@ import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-r
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
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';
import { AccessStatusDataService } from 'src/app/core/data/access-status-data.service';
import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model';
describe('ItemAdminSearchResultGridElementComponent', () => { describe('ItemAdminSearchResultGridElementComponent', () => {
let component: ItemAdminSearchResultGridElementComponent; let component: ItemAdminSearchResultGridElementComponent;
@@ -31,6 +33,12 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
} }
}; };
const mockAccessStatusDataService = {
findAccessStatusFor(item: Item): Observable<RemoteData<AccessStatusObject>> {
return createSuccessfulRemoteDataObject$(new AccessStatusObject());
}
};
const mockThemeService = getMockThemeService(); const mockThemeService = getMockThemeService();
function init() { function init() {
@@ -55,6 +63,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
{ provide: TruncatableService, useValue: mockTruncatableService }, { provide: TruncatableService, useValue: mockTruncatableService },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: ThemeService, useValue: mockThemeService }, { provide: ThemeService, useValue: mockThemeService },
{ provide: AccessStatusDataService, useValue: mockAccessStatusDataService },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })

View File

@@ -1,28 +1,30 @@
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary my-1 move-link" [routerLink]="[getMoveRoute()]" [title]="'admin.search.item.move' | translate"> <div class="space-children-mr my-1">
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary move-link" [routerLink]="[getMoveRoute()]" [title]="'admin.search.item.move' | translate">
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span> <i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
</a> </a>
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-secondary my-1 private-link" [routerLink]="[getPrivateRoute()]" [title]="'admin.search.item.make-private' | translate"> <a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-secondary private-link" [routerLink]="[getPrivateRoute()]" [title]="'admin.search.item.make-private' | translate">
<i class="fa fa-eye-slash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span> <i class="fa fa-eye-slash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span>
</a> </a>
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-secondary my-1 public-link" [routerLink]="[getPublicRoute()]" [title]="'admin.search.item.make-public' | translate"> <a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-secondary public-link" [routerLink]="[getPublicRoute()]" [title]="'admin.search.item.make-public' | translate">
<i class="fa fa-eye"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span> <i class="fa fa-eye"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span>
</a> </a>
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary my-1 edit-link" [routerLink]="[getEditRoute()]" [title]="'admin.search.item.edit' | translate"> <a [ngClass]="{'btn-sm': small}" class="btn btn-secondary edit-link" [routerLink]="[getEditRoute()]" [title]="'admin.search.item.edit' | translate">
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span> <i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
</a> </a>
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-warning t my-1 withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate"> <a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-warning t withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate">
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span> <i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
</a> </a>
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-warning my-1 reinstate-link" [routerLink]="[getReinstateRoute()]" [title]="'admin.search.item.reinstate' | translate"> <a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-warning reinstate-link" [routerLink]="[getReinstateRoute()]" [title]="'admin.search.item.reinstate' | translate">
<i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span> <i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
</a> </a>
<a [ngClass]="{'btn-sm': small}" class="btn btn-danger my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.search.item.delete' | translate"> <a [ngClass]="{'btn-sm': small}" class="btn btn-danger delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.search.item.delete' | translate">
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span> <i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span>
</a> </a>
</div>

View File

@@ -1,10 +1,10 @@
import { Component, Inject, Injector, OnInit } from '@angular/core'; import { Component, Inject, Injector, OnInit } from '@angular/core';
import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component'; import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component';
import { MenuID } from '../../../shared/menu/initial-menus-state';
import { MenuService } from '../../../shared/menu/menu.service'; import { MenuService } from '../../../shared/menu/menu.service';
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator'; import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model'; import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
import { MenuSection } from '../../../shared/menu/menu.reducer'; import { MenuSection } from '../../../shared/menu/menu-section.model';
import { MenuID } from '../../../shared/menu/menu-id.model';
import { isNotEmpty } from '../../../shared/empty.util'; import { isNotEmpty } from '../../../shared/empty.util';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@@ -12,7 +12,7 @@ import { Router } from '@angular/router';
* Represents a non-expandable section in the admin sidebar * Represents a non-expandable section in the admin sidebar
*/ */
@Component({ @Component({
/* tslint:disable:component-selector */ /* eslint-disable @angular-eslint/component-selector */
selector: 'li[ds-admin-sidebar-section]', selector: 'li[ds-admin-sidebar-section]',
templateUrl: './admin-sidebar-section.component.html', templateUrl: './admin-sidebar-section.component.html',
styleUrls: ['./admin-sidebar-section.component.scss'], styleUrls: ['./admin-sidebar-section.component.scss'],

View File

@@ -182,150 +182,4 @@ describe('AdminSidebarComponent', () => {
expect(menuService.collapseMenuPreview).toHaveBeenCalled(); expect(menuService.collapseMenuPreview).toHaveBeenCalled();
})); }));
}); });
describe('menu', () => {
beforeEach(() => {
spyOn(menuService, 'addSection');
});
describe('for regular user', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake(() => {
return observableOf(false);
});
});
beforeEach(() => {
comp.createMenu();
});
it('should not show site admin section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'admin_search', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'registries', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
parentID: 'registries', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'curation_tasks', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'workflow', visible: false,
}));
});
it('should not show edit_community', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'edit_community', visible: false,
}));
});
it('should not show edit_collection', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'edit_collection', visible: false,
}));
});
it('should not show access control section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'access_control', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
parentID: 'access_control', visible: false,
}));
});
});
describe('for site admin', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
return observableOf(featureID === FeatureID.AdministratorOf);
});
});
beforeEach(() => {
comp.createMenu();
});
it('should contain site admin section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'admin_search', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'registries', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
parentID: 'registries', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'curation_tasks', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'workflow', visible: true,
}));
});
});
describe('for community admin', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
return observableOf(featureID === FeatureID.IsCommunityAdmin);
});
});
beforeEach(() => {
comp.createMenu();
});
it('should show edit_community', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'edit_community', visible: true,
}));
});
});
describe('for collection admin', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
return observableOf(featureID === FeatureID.IsCollectionAdmin);
});
});
beforeEach(() => {
comp.createMenu();
});
it('should show edit_collection', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'edit_collection', visible: true,
}));
});
});
describe('for group admin', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
return observableOf(featureID === FeatureID.CanManageGroups);
});
});
beforeEach(() => {
comp.createMenu();
});
it('should show access control section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'access_control', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
parentID: 'access_control', visible: true,
}));
});
});
});
}); });

View File

@@ -1,27 +1,14 @@
import { Component, HostListener, Injector, OnInit } from '@angular/core'; import { Component, HostListener, Injector, OnInit } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { combineLatest, combineLatest as observableCombineLatest, Observable, BehaviorSubject } from 'rxjs'; import { debounceTime, distinctUntilChanged, first, map, withLatestFrom } from 'rxjs/operators';
import { debounceTime, first, map, take, distinctUntilChanged, withLatestFrom } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { ScriptDataService } from '../../core/data/processes/script-data.service';
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide'; import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
import { ExportMetadataSelectorComponent } from '../../shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
import { MenuComponent } from '../../shared/menu/menu.component'; import { MenuComponent } from '../../shared/menu/menu.component';
import { MenuService } from '../../shared/menu/menu.service'; import { MenuService } from '../../shared/menu/menu.service';
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service'; import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
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 { MenuID } from '../../shared/menu/menu-id.model';
import { Router, ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
/** /**
* Component representing the admin sidebar * Component representing the admin sidebar
@@ -66,11 +53,9 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
constructor( constructor(
protected menuService: MenuService, protected menuService: MenuService,
protected injector: Injector, protected injector: Injector,
protected variableService: CSSVariableService, private variableService: CSSVariableService,
protected authService: AuthService, private authService: AuthService,
protected modalService: NgbModal,
public authorizationService: AuthorizationDataService, public authorizationService: AuthorizationDataService,
protected scriptDataService: ScriptDataService,
public route: ActivatedRoute public route: ActivatedRoute
) { ) {
super(menuService, injector, authorizationService, route); super(menuService, injector, authorizationService, route);
@@ -81,7 +66,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
* Set and calculate all initial values of the instance variables * Set and calculate all initial values of the instance variables
*/ */
ngOnInit(): void { ngOnInit(): void {
this.createMenu();
super.ngOnInit(); super.ngOnInit();
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth'); this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
this.authService.isAuthenticated() this.authService.isAuthenticated()
@@ -116,501 +100,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
}); });
} }
/**
* Initialize all menu sections and items for this menu
*/
createMenu() {
this.createMainMenuSections();
this.createSiteAdministratorMenuSections();
this.createExportMenuSections();
this.createImportMenuSections();
this.createAccessControlMenuSections();
}
/**
* Initialize the main menu sections.
* edit_community / edit_collection is only included if the current user is a Community or Collection admin
*/
createMainMenuSections() {
combineLatest([
this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin),
this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin),
this.authorizationService.isAuthorized(FeatureID.AdministratorOf)
]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => {
const menuList = [
/* News */
{
id: 'new',
active: false,
visible: true,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.new'
} as TextMenuItemModel,
icon: 'plus',
index: 0
},
{
id: 'new_community',
parentID: 'new',
active: false,
visible: isCommunityAdmin,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.new_community',
function: () => {
this.modalService.open(CreateCommunityParentSelectorComponent);
}
} as OnClickMenuItemModel,
},
{
id: 'new_collection',
parentID: 'new',
active: false,
visible: isCommunityAdmin,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.new_collection',
function: () => {
this.modalService.open(CreateCollectionParentSelectorComponent);
}
} as OnClickMenuItemModel,
},
{
id: 'new_item',
parentID: 'new',
active: false,
visible: true,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.new_item',
function: () => {
this.modalService.open(CreateItemParentSelectorComponent);
}
} as OnClickMenuItemModel,
},
{
id: 'new_process',
parentID: 'new',
active: false,
visible: isCollectionAdmin,
model: {
type: MenuItemType.LINK,
text: 'menu.section.new_process',
link: '/processes/new'
} as LinkMenuItemModel,
},
// TODO: enable this menu item once the feature has been implemented
// {
// id: 'new_item_version',
// parentID: 'new',
// active: false,
// visible: true,
// model: {
// type: MenuItemType.LINK,
// text: 'menu.section.new_item_version',
// link: ''
// } as LinkMenuItemModel,
// },
/* Edit */
{
id: 'edit',
active: false,
visible: true,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.edit'
} as TextMenuItemModel,
icon: 'pencil-alt',
index: 1
},
{
id: 'edit_community',
parentID: 'edit',
active: false,
visible: isCommunityAdmin,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.edit_community',
function: () => {
this.modalService.open(EditCommunitySelectorComponent);
}
} as OnClickMenuItemModel,
},
{
id: 'edit_collection',
parentID: 'edit',
active: false,
visible: isCollectionAdmin,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.edit_collection',
function: () => {
this.modalService.open(EditCollectionSelectorComponent);
}
} as OnClickMenuItemModel,
},
{
id: 'edit_item',
parentID: 'edit',
active: false,
visible: true,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.edit_item',
function: () => {
this.modalService.open(EditItemSelectorComponent);
}
} as OnClickMenuItemModel,
},
/* Statistics */
// TODO: enable this menu item once the feature has been implemented
// {
// id: 'statistics_task',
// active: false,
// visible: true,
// model: {
// type: MenuItemType.LINK,
// text: 'menu.section.statistics_task',
// link: ''
// } as LinkMenuItemModel,
// icon: 'chart-bar',
// index: 8
// },
/* Control Panel */
// TODO: enable this menu item once the feature has been implemented
// {
// id: 'control_panel',
// active: false,
// visible: isSiteAdmin,
// model: {
// type: MenuItemType.LINK,
// text: 'menu.section.control_panel',
// link: ''
// } as LinkMenuItemModel,
// icon: 'cogs',
// index: 9
// },
/* Processes */
{
id: 'processes',
active: false,
visible: isSiteAdmin,
model: {
type: MenuItemType.LINK,
text: 'menu.section.processes',
link: '/processes'
} as LinkMenuItemModel,
icon: 'terminal',
index: 10
},
];
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
shouldPersistOnRouteChange: true
})));
});
}
/**
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
* the export scripts exist and the current user is allowed to execute them
*/
createExportMenuSections() {
const menuList = [
/* Export */
{
id: 'export',
active: false,
visible: true,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.export'
} as TextMenuItemModel,
icon: 'file-export',
index: 3,
shouldPersistOnRouteChange: true
},
// TODO: enable this menu item once the feature has been implemented
// {
// id: 'export_community',
// parentID: 'export',
// active: false,
// visible: true,
// model: {
// type: MenuItemType.LINK,
// text: 'menu.section.export_community',
// link: ''
// } as LinkMenuItemModel,
// shouldPersistOnRouteChange: true
// },
// TODO: enable this menu item once the feature has been implemented
// {
// id: 'export_collection',
// parentID: 'export',
// active: false,
// visible: true,
// model: {
// type: MenuItemType.LINK,
// text: 'menu.section.export_collection',
// link: ''
// } as LinkMenuItemModel,
// shouldPersistOnRouteChange: true
// },
// TODO: enable this menu item once the feature has been implemented
// {
// id: 'export_item',
// parentID: 'export',
// active: false,
// visible: true,
// model: {
// type: MenuItemType.LINK,
// text: 'menu.section.export_item',
// link: ''
// } as LinkMenuItemModel,
// shouldPersistOnRouteChange: true
// },
];
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
observableCombineLatest(
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
// this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME)
).pipe(
// TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed; otherwise even in production mode, the metadata export button is only available after a refresh (and not in dev mode)
// filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists),
take(1)
).subscribe(() => {
this.menuService.addSection(this.menuID, {
id: 'export_metadata',
parentID: 'export',
active: true,
visible: true,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.export_metadata',
function: () => {
this.modalService.open(ExportMetadataSelectorComponent);
}
} as OnClickMenuItemModel,
shouldPersistOnRouteChange: true
});
});
}
/**
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
* the import scripts exist and the current user is allowed to execute them
*/
createImportMenuSections() {
const menuList = [
/* Import */
{
id: 'import',
active: false,
visible: true,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.import'
} as TextMenuItemModel,
icon: 'file-import',
index: 2
},
// TODO: enable this menu item once the feature has been implemented
// {
// id: 'import_batch',
// parentID: 'import',
// active: false,
// visible: true,
// model: {
// type: MenuItemType.LINK,
// text: 'menu.section.import_batch',
// link: ''
// } as LinkMenuItemModel,
// }
];
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
shouldPersistOnRouteChange: true
})));
observableCombineLatest(
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
// this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME)
).pipe(
// TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed
// filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists),
take(1)
).subscribe(() => {
this.menuService.addSection(this.menuID, {
id: 'import_metadata',
parentID: 'import',
active: true,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.import_metadata',
link: '/admin/metadata-import'
} as LinkMenuItemModel,
shouldPersistOnRouteChange: true
});
});
}
/**
* Create menu sections dependent on whether or not the current user is a site administrator
*/
createSiteAdministratorMenuSections() {
this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => {
const menuList = [
/* Admin Search */
{
id: 'admin_search',
active: false,
visible: authorized,
model: {
type: MenuItemType.LINK,
text: 'menu.section.admin_search',
link: '/admin/search'
} as LinkMenuItemModel,
icon: 'search',
index: 5
},
/* Registries */
{
id: 'registries',
active: false,
visible: authorized,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.registries'
} as TextMenuItemModel,
icon: 'list',
index: 6
},
{
id: 'registries_metadata',
parentID: 'registries',
active: false,
visible: authorized,
model: {
type: MenuItemType.LINK,
text: 'menu.section.registries_metadata',
link: 'admin/registries/metadata'
} as LinkMenuItemModel,
},
{
id: 'registries_format',
parentID: 'registries',
active: false,
visible: authorized,
model: {
type: MenuItemType.LINK,
text: 'menu.section.registries_format',
link: 'admin/registries/bitstream-formats'
} as LinkMenuItemModel,
},
/* Curation tasks */
{
id: 'curation_tasks',
active: false,
visible: authorized,
model: {
type: MenuItemType.LINK,
text: 'menu.section.curation_task',
link: 'admin/curation-tasks'
} as LinkMenuItemModel,
icon: 'filter',
index: 7
},
/* Workflow */
{
id: 'workflow',
active: false,
visible: authorized,
model: {
type: MenuItemType.LINK,
text: 'menu.section.workflow',
link: '/admin/workflow'
} as LinkMenuItemModel,
icon: 'user-check',
index: 11
},
];
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
shouldPersistOnRouteChange: true
})));
});
}
/**
* Create menu sections dependent on whether or not the current user can manage access control groups
*/
createAccessControlMenuSections() {
observableCombineLatest(
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
this.authorizationService.isAuthorized(FeatureID.CanManageGroups)
).subscribe(([isSiteAdmin, canManageGroups]) => {
const menuList = [
/* Access Control */
{
id: 'access_control_people',
parentID: 'access_control',
active: false,
visible: isSiteAdmin,
model: {
type: MenuItemType.LINK,
text: 'menu.section.access_control_people',
link: '/access-control/epeople'
} as LinkMenuItemModel,
},
{
id: 'access_control_groups',
parentID: 'access_control',
active: false,
visible: canManageGroups,
model: {
type: MenuItemType.LINK,
text: 'menu.section.access_control_groups',
link: '/access-control/groups'
} as LinkMenuItemModel,
},
// TODO: enable this menu item once the feature has been implemented
// {
// id: 'access_control_authorizations',
// parentID: 'access_control',
// active: false,
// visible: authorized,
// model: {
// type: MenuItemType.LINK,
// text: 'menu.section.access_control_authorizations',
// link: ''
// } as LinkMenuItemModel,
// },
{
id: 'access_control',
active: false,
visible: canManageGroups || isSiteAdmin,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.access_control'
} as TextMenuItemModel,
icon: 'key',
index: 4
},
];
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
shouldPersistOnRouteChange: true,
})));
});
}
@HostListener('focusin') @HostListener('focusin')
public handleFocusIn() { public handleFocusIn() {
this.inFocus$.next(true); this.inFocus$.next(true);

View File

@@ -4,18 +4,18 @@ import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sid
import { slide } from '../../../shared/animations/slide'; import { slide } from '../../../shared/animations/slide';
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service'; import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
import { bgColor } from '../../../shared/animations/bgColor'; import { bgColor } from '../../../shared/animations/bgColor';
import { MenuID } from '../../../shared/menu/initial-menus-state';
import { MenuService } from '../../../shared/menu/menu.service'; import { MenuService } from '../../../shared/menu/menu.service';
import { combineLatest as combineLatestObservable, Observable } from 'rxjs'; import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator'; import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
import { MenuID } from '../../../shared/menu/menu-id.model';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
/** /**
* Represents a expandable section in the sidebar * Represents a expandable section in the sidebar
*/ */
@Component({ @Component({
/* tslint:disable:component-selector */ /* eslint-disable @angular-eslint/component-selector */
selector: 'li[ds-expandable-admin-sidebar-section]', selector: 'li[ds-expandable-admin-sidebar-section]',
templateUrl: './expandable-admin-sidebar-section.component.html', templateUrl: './expandable-admin-sidebar-section.component.html',
styleUrls: ['./expandable-admin-sidebar-section.component.scss'], styleUrls: ['./expandable-admin-sidebar-section.component.scss'],

View File

@@ -1,7 +1,8 @@
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.workflow.item.delete' | translate"> <div class="space-children-mr">
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.workflow.item.delete' | translate">
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.workflow.item.delete" | translate}}</span> <i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.workflow.item.delete" | translate}}</span>
</a> </a>
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 send-back-link" [routerLink]="[getSendBackRoute()]" [title]="'admin.workflow.item.send-back' | translate">
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 send-back-link" [routerLink]="[getSendBackRoute()]" [title]="'admin.workflow.item.send-back' | translate">
<i class="fa fa-hand-point-left"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.workflow.item.send-back" | translate}}</span> <i class="fa fa-hand-point-left"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.workflow.item.send-back" | translate}}</span>
</a> </a>
</div>

View File

@@ -70,6 +70,12 @@ export function getWorkflowItemModuleRoute() {
return `/${WORKFLOW_ITEM_MODULE_PATH}`; return `/${WORKFLOW_ITEM_MODULE_PATH}`;
} }
export const WORKSPACE_ITEM_MODULE_PATH = 'workspaceitems';
export function getWorkspaceItemModuleRoute() {
return `/${WORKSPACE_ITEM_MODULE_PATH}`;
}
export function getDSORoute(dso: DSpaceObject): string { export function getDSORoute(dso: DSpaceObject): string {
if (hasValue(dso)) { if (hasValue(dso)) {
switch ((dso as any).type) { switch ((dso as any).type) {
@@ -116,3 +122,5 @@ export const REQUEST_COPY_MODULE_PATH = 'request-a-copy';
export function getRequestCopyModulePath() { export function getRequestCopyModulePath() {
return `/${REQUEST_COPY_MODULE_PATH}`; return `/${REQUEST_COPY_MODULE_PATH}`;
} }
export const HEALTH_PAGE_PATH = 'health';

View File

@@ -1,15 +1,18 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule, NoPreloading } from '@angular/router';
import { AuthBlockingGuard } from './core/auth/auth-blocking.guard'; import { AuthBlockingGuard } from './core/auth/auth-blocking.guard';
import { AuthenticatedGuard } from './core/auth/authenticated.guard'; import { AuthenticatedGuard } from './core/auth/authenticated.guard';
import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; import {
SiteAdministratorGuard
} from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { import {
ACCESS_CONTROL_MODULE_PATH, ACCESS_CONTROL_MODULE_PATH,
ADMIN_MODULE_PATH, ADMIN_MODULE_PATH,
BITSTREAM_MODULE_PATH, BITSTREAM_MODULE_PATH,
FORBIDDEN_PATH, FORBIDDEN_PATH,
FORGOT_PASSWORD_PATH, FORGOT_PASSWORD_PATH,
HEALTH_PAGE_PATH,
INFO_MODULE_PATH, INFO_MODULE_PATH,
INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR,
LEGACY_BITSTREAM_MODULE_PATH, LEGACY_BITSTREAM_MODULE_PATH,
@@ -27,9 +30,14 @@ import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-
import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard'; import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard';
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component'; import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component'; import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; import {
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component'; GroupAdministratorGuard
} from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
import {
ThemedPageInternalServerErrorComponent
} from './page-internal-server-error/themed-page-internal-server-error.component';
import { ServerCheckGuard } from './core/server-check/server-check.guard'; import { ServerCheckGuard } from './core/server-check/server-check.guard';
import { MenuResolver } from './menu.resolver';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -39,6 +47,7 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard';
path: '', path: '',
canActivate: [AuthBlockingGuard], canActivate: [AuthBlockingGuard],
canActivateChild: [ServerCheckGuard], canActivateChild: [ServerCheckGuard],
resolve: [MenuResolver],
children: [ children: [
{ path: '', redirectTo: '/home', pathMatch: 'full' }, { path: '', redirectTo: '/home', pathMatch: 'full' },
{ {
@@ -208,6 +217,11 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard';
loadChildren: () => import('./statistics-page/statistics-page-routing.module') loadChildren: () => import('./statistics-page/statistics-page-routing.module')
.then((m) => m.StatisticsPageRoutingModule) .then((m) => m.StatisticsPageRoutingModule)
}, },
{
path: HEALTH_PAGE_PATH,
loadChildren: () => import('./health-page/health-page.module')
.then((m) => m.HealthPageModule)
},
{ {
path: ACCESS_CONTROL_MODULE_PATH, path: ACCESS_CONTROL_MODULE_PATH,
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule), loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
@@ -217,6 +231,12 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard';
] ]
} }
], { ], {
// enableTracing: true,
useHash: false,
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
initialNavigation: 'enabledBlocking',
preloadingStrategy: NoPreloading,
onSameUrlNavigation: 'reload', onSameUrlNavigation: 'reload',
}) })
], ],

View File

@@ -4,7 +4,7 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule, DOCUMENT } from '@angular/common'; import { CommonModule, DOCUMENT } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { Angulartics2GoogleAnalytics } from 'angulartics2';
// Load the implementations that should be tested // Load the implementations that should be tested
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
@@ -187,7 +187,7 @@ describe('App component', () => {
link.setAttribute('rel', 'stylesheet'); link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css'); link.setAttribute('type', 'text/css');
link.setAttribute('class', 'theme-css'); link.setAttribute('class', 'theme-css');
link.setAttribute('href', '/custom-theme.css'); link.setAttribute('href', 'custom-theme.css');
expect(headSpy.appendChild).toHaveBeenCalledWith(link); expect(headSpy.appendChild).toHaveBeenCalledWith(link);
}); });

View File

@@ -12,6 +12,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ActivationEnd,
NavigationCancel, NavigationCancel,
NavigationEnd, NavigationEnd,
NavigationStart, ResolveEnd, NavigationStart, ResolveEnd,
@@ -23,7 +24,7 @@ import { BehaviorSubject, Observable, of } from 'rxjs';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { Angulartics2GoogleAnalytics } from 'angulartics2';
import { MetadataService } from './core/metadata/metadata.service'; import { MetadataService } from './core/metadata/metadata.service';
import { HostWindowResizeAction } from './shared/host-window.actions'; import { HostWindowResizeAction } from './shared/host-window.actions';
@@ -72,7 +73,7 @@ export class AppComponent implements OnInit, AfterViewInit {
/** /**
* Whether or not the app is in the process of rerouting * Whether or not the app is in the process of rerouting
*/ */
isRouteLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true); isRouteLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
/** /**
* Whether or not the theme is in the process of being swapped * Whether or not the theme is in the process of being swapped
@@ -121,7 +122,7 @@ export class AppComponent implements OnInit, AfterViewInit {
this.themeService.getThemeName$().subscribe((themeName: string) => { this.themeService.getThemeName$().subscribe((themeName: string) => {
if (isPlatformBrowser(this.platformId)) { if (isPlatformBrowser(this.platformId)) {
// the theme css will never download server side, so this should only happen on the browser // the theme css will never download server side, so this should only happen on the browser
this.isThemeCSSLoading$.next(true); this.distinctNext(this.isThemeCSSLoading$, true);
} }
if (hasValue(themeName)) { if (hasValue(themeName)) {
this.loadGlobalThemeConfig(themeName); this.loadGlobalThemeConfig(themeName);
@@ -196,16 +197,45 @@ export class AppComponent implements OnInit, AfterViewInit {
} }
ngAfterViewInit() { ngAfterViewInit() {
let resolveEndFound = false; let updatingTheme = false;
let snapshot: ActivatedRouteSnapshot;
this.router.events.subscribe((event) => { this.router.events.subscribe((event) => {
if (event instanceof NavigationStart) { if (event instanceof NavigationStart) {
resolveEndFound = false; updatingTheme = false;
this.isRouteLoading$.next(true); this.distinctNext(this.isRouteLoading$, true);
this.isThemeLoading$.next(true);
} else if (event instanceof ResolveEnd) { } else if (event instanceof ResolveEnd) {
resolveEndFound = true; // this is the earliest point where we have all the information we need
const activatedRouteSnapShot: ActivatedRouteSnapshot = event.state.root; // to update the theme, but this event is not emitted on first load
this.themeService.updateThemeOnRouteChange$(event.urlAfterRedirects, activatedRouteSnapShot).pipe( this.updateTheme(event.urlAfterRedirects, event.state.root);
updatingTheme = true;
} else if (!updatingTheme && event instanceof ActivationEnd) {
// if there was no ResolveEnd, keep track of the snapshot...
snapshot = event.snapshot;
} else if (event instanceof NavigationEnd) {
if (!updatingTheme) {
// ...and use it to update the theme on NavigationEnd instead
this.updateTheme(event.urlAfterRedirects, snapshot);
updatingTheme = true;
}
this.distinctNext(this.isRouteLoading$, false);
} else if (event instanceof NavigationCancel) {
if (!updatingTheme) {
this.distinctNext(this.isThemeLoading$, false);
}
this.distinctNext(this.isRouteLoading$, false);
}
});
}
/**
* Update the theme according to the current route, if applicable.
* @param urlAfterRedirects the current URL after redirects
* @param snapshot the current route snapshot
* @private
*/
private updateTheme(urlAfterRedirects: string, snapshot: ActivatedRouteSnapshot): void {
this.themeService.updateThemeOnRouteChange$(urlAfterRedirects, snapshot).pipe(
switchMap((changed) => { switchMap((changed) => {
if (changed) { if (changed) {
return this.isThemeCSSLoading$; return this.isThemeCSSLoading$;
@@ -214,17 +244,7 @@ export class AppComponent implements OnInit, AfterViewInit {
} }
}) })
).subscribe((changed) => { ).subscribe((changed) => {
this.isThemeLoading$.next(changed); this.distinctNext(this.isThemeLoading$, changed);
});
} else if (
event instanceof NavigationEnd ||
event instanceof NavigationCancel
) {
if (!resolveEndFound) {
this.isThemeLoading$.next(false);
}
this.isRouteLoading$.next(false);
}
}); });
} }
@@ -269,7 +289,7 @@ export class AppComponent implements OnInit, AfterViewInit {
link.setAttribute('rel', 'stylesheet'); link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css'); link.setAttribute('type', 'text/css');
link.setAttribute('class', 'theme-css'); link.setAttribute('class', 'theme-css');
link.setAttribute('href', `/${encodeURIComponent(themeName)}-theme.css`); link.setAttribute('href', `${encodeURIComponent(themeName)}-theme.css`);
// wait for the new css to download before removing the old one to prevent a // wait for the new css to download before removing the old one to prevent a
// flash of unstyled content // flash of unstyled content
link.onload = () => { link.onload = () => {
@@ -281,7 +301,7 @@ export class AppComponent implements OnInit, AfterViewInit {
}); });
} }
// the fact that this callback is used, proves we're on the browser. // the fact that this callback is used, proves we're on the browser.
this.isThemeCSSLoading$.next(false); this.distinctNext(this.isThemeCSSLoading$, false);
}; };
head.appendChild(link); head.appendChild(link);
} }
@@ -376,4 +396,17 @@ export class AppComponent implements OnInit, AfterViewInit {
} }
}); });
} }
/**
* Use nextValue to update a given BehaviorSubject, only if it differs from its current value
*
* @param bs a BehaviorSubject
* @param nextValue the next value for that BehaviorSubject
* @protected
*/
protected distinctNext<T>(bs: BehaviorSubject<T>, nextValue: T): void {
if (bs.getValue() !== nextValue) {
bs.next(nextValue);
}
}
} }

View File

@@ -1,4 +1,4 @@
import { APP_BASE_HREF, CommonModule } from '@angular/common'; import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, NgModule } from '@angular/core'; import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AbstractControl } from '@angular/forms'; import { AbstractControl } from '@angular/forms';
@@ -8,7 +8,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store'; import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
import { MetaReducer, Store, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store'; import { MetaReducer, Store, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { import {
DYNAMIC_ERROR_MESSAGES_MATCHER, DYNAMIC_ERROR_MESSAGES_MATCHER,
DYNAMIC_MATCHER_PROVIDERS, DYNAMIC_MATCHER_PROVIDERS,
@@ -16,10 +15,6 @@ import {
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
import { AdminSidebarSectionComponent } from './admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
import { AdminSidebarComponent } from './admin/admin-sidebar/admin-sidebar.component';
import { ExpandableAdminSidebarSectionComponent } from './admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { appEffects } from './app.effects'; import { appEffects } from './app.effects';
@@ -28,45 +23,30 @@ import { appReducers, AppState, storeModuleConfig } from './app.reducer';
import { CheckAuthenticationTokenAction } from './core/auth/auth.actions'; import { CheckAuthenticationTokenAction } from './core/auth/auth.actions';
import { CoreModule } from './core/core.module'; import { CoreModule } from './core/core.module';
import { ClientCookieService } from './core/services/client-cookie.service'; import { ClientCookieService } from './core/services/client-cookie.service';
import { FooterComponent } from './footer/footer.component';
import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component';
import { HeaderComponent } from './header/header.component';
import { NavbarModule } from './navbar/navbar.module'; import { NavbarModule } from './navbar/navbar.module';
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer'; import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer';
import { NotificationComponent } from './shared/notifications/notification/notification.component';
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
import { SharedModule } from './shared/shared.module'; import { SharedModule } from './shared/shared.module';
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { ForbiddenComponent } from './forbidden/forbidden.component';
import { AuthInterceptor } from './core/auth/auth.interceptor'; import { AuthInterceptor } from './core/auth/auth.interceptor';
import { LocaleInterceptor } from './core/locale/locale.interceptor'; import { LocaleInterceptor } from './core/locale/locale.interceptor';
import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor'; import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
import { LogInterceptor } from './core/log/log.interceptor'; import { LogInterceptor } from './core/log/log.interceptor';
import { RootComponent } from './root/root.component'; import { EagerThemesModule } from '../themes/eager-themes.module';
import { ThemedRootComponent } from './root/themed-root.component';
import { ThemedEntryComponentModule } from '../themes/themed-entry-component.module';
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
import { ThemedHeaderComponent } from './header/themed-header.component';
import { ThemedFooterComponent } from './footer/themed-footer.component';
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
import { PageInternalServerErrorComponent } from './page-internal-server-error/page-internal-server-error.component';
import { ThemedAdminSidebarComponent } from './admin/admin-sidebar/themed-admin-sidebar.component';
import { APP_CONFIG, AppConfig } from '../config/app-config.interface'; import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
import { NgxMaskModule } from 'ngx-mask';
import { StoreDevModules } from '../config/store/devtools';
import { RootModule } from './root.module';
export function getConfig() { export function getConfig() {
return environment; return environment;
} }
export function getBase(appConfig: AppConfig) { const getBaseHref = (document: Document, appConfig: AppConfig): string => {
return appConfig.ui.nameSpace; const baseTag = document.querySelector('head > base');
} baseTag.setAttribute('href', `${appConfig.ui.nameSpace}${appConfig.ui.nameSpace.endsWith('/') ? '' : '/'}`);
return baseTag.getAttribute('href');
};
export function getMetaReducers(appConfig: AppConfig): MetaReducer<AppState>[] { export function getMetaReducers(appConfig: AppConfig): MetaReducer<AppState>[] {
return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers; return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers;
@@ -90,19 +70,15 @@ const IMPORTS = [
ScrollToModule.forRoot(), ScrollToModule.forRoot(),
NgbModule, NgbModule,
TranslateModule.forRoot(), TranslateModule.forRoot(),
NgxMaskModule.forRoot(),
EffectsModule.forRoot(appEffects), EffectsModule.forRoot(appEffects),
StoreModule.forRoot(appReducers, storeModuleConfig), StoreModule.forRoot(appReducers, storeModuleConfig),
StoreRouterConnectingModule.forRoot(), StoreRouterConnectingModule.forRoot(),
ThemedEntryComponentModule.withEntryComponents(), StoreDevModules,
EagerThemesModule,
RootModule,
]; ];
IMPORTS.push(
StoreDevtoolsModule.instrument({
maxAge: 1000,
logOnly: environment.production,
})
);
const PROVIDERS = [ const PROVIDERS = [
{ {
provide: APP_CONFIG, provide: APP_CONFIG,
@@ -110,8 +86,8 @@ const PROVIDERS = [
}, },
{ {
provide: APP_BASE_HREF, provide: APP_BASE_HREF,
useFactory: getBase, useFactory: getBaseHref,
deps: [APP_CONFIG] deps: [DOCUMENT, APP_CONFIG]
}, },
{ {
provide: USER_PROVIDED_META_REDUCERS, provide: USER_PROVIDED_META_REDUCERS,
@@ -165,29 +141,6 @@ const PROVIDERS = [
const DECLARATIONS = [ const DECLARATIONS = [
AppComponent, AppComponent,
RootComponent,
ThemedRootComponent,
HeaderComponent,
ThemedHeaderComponent,
HeaderNavbarWrapperComponent,
ThemedHeaderNavbarWrapperComponent,
AdminSidebarComponent,
ThemedAdminSidebarComponent,
AdminSidebarSectionComponent,
ExpandableAdminSidebarSectionComponent,
FooterComponent,
ThemedFooterComponent,
PageNotFoundComponent,
ThemedPageNotFoundComponent,
NotificationComponent,
NotificationsBoardComponent,
BreadcrumbsComponent,
ThemedBreadcrumbsComponent,
ForbiddenComponent,
ThemedForbiddenComponent,
IdleModalComponent,
ThemedPageInternalServerErrorComponent,
PageInternalServerErrorComponent
]; ];
const EXPORTS = [ const EXPORTS = [

View File

@@ -22,7 +22,7 @@ import {
nameVariantReducer nameVariantReducer
} from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer'; } from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
import { formReducer, FormState } from './shared/form/form.reducer'; import { formReducer, FormState } from './shared/form/form.reducer';
import { menusReducer, MenusState } from './shared/menu/menu.reducer'; import { menusReducer} from './shared/menu/menu.reducer';
import { import {
notificationsReducer, notificationsReducer,
NotificationsState NotificationsState
@@ -49,6 +49,7 @@ import {
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer'; import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer'; import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer';
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 {

View File

@@ -10,6 +10,9 @@ import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/re
import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component'; import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component';
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component'; import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
import { BitstreamBreadcrumbResolver } from '../core/breadcrumbs/bitstream-breadcrumb.resolver';
import { BitstreamBreadcrumbsService } from '../core/breadcrumbs/bitstream-breadcrumbs.service';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
const EDIT_BITSTREAM_PATH = ':id/edit'; const EDIT_BITSTREAM_PATH = ':id/edit';
const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
@@ -48,7 +51,8 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
path: EDIT_BITSTREAM_PATH, path: EDIT_BITSTREAM_PATH,
component: EditBitstreamPageComponent, component: EditBitstreamPageComponent,
resolve: { resolve: {
bitstream: BitstreamPageResolver bitstream: BitstreamPageResolver,
breadcrumb: BitstreamBreadcrumbResolver,
}, },
canActivate: [AuthenticatedGuard] canActivate: [AuthenticatedGuard]
}, },
@@ -67,15 +71,17 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
{ {
path: 'edit', path: 'edit',
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver,
resourcePolicy: ResourcePolicyResolver resourcePolicy: ResourcePolicyResolver
}, },
component: ResourcePolicyEditComponent, component: ResourcePolicyEditComponent,
data: { title: 'resource-policies.edit.page.title', showBreadcrumbs: true } data: { breadcrumbKey: 'item.edit', title: 'resource-policies.edit.page.title', showBreadcrumbs: true }
}, },
{ {
path: '', path: '',
resolve: { resolve: {
bitstream: BitstreamPageResolver bitstream: BitstreamPageResolver,
breadcrumb: BitstreamBreadcrumbResolver,
}, },
component: BitstreamAuthorizationsComponent, component: BitstreamAuthorizationsComponent,
data: { title: 'bitstream.edit.authorizations.title', showBreadcrumbs: true } data: { title: 'bitstream.edit.authorizations.title', showBreadcrumbs: true }
@@ -86,6 +92,8 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
], ],
providers: [ providers: [
BitstreamPageResolver, BitstreamPageResolver,
BitstreamBreadcrumbResolver,
BitstreamBreadcrumbsService
] ]
}) })
export class BitstreamPageRoutingModule { export class BitstreamPageRoutingModule {

View File

@@ -7,6 +7,15 @@ import { BitstreamDataService } from '../core/data/bitstream-data.service';
import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model'; import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { getFirstCompletedRemoteData } from '../core/shared/operators';
/**
* The self links defined in this list are expected to be requested somewhere in the near future
* Requesting them as embeds will limit the number of requests
*/
export const BITSTREAM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Bitstream>[] = [
followLink('bundle', {}, followLink('item')),
followLink('format')
];
/** /**
* This class represents a resolver that requests a specific bitstream before the route is activated * This class represents a resolver that requests a specific bitstream before the route is activated
*/ */
@@ -34,9 +43,6 @@ export class BitstreamPageResolver implements Resolve<RemoteData<Bitstream>> {
* Requesting them as embeds will limit the number of requests * Requesting them as embeds will limit the number of requests
*/ */
get followLinks(): FollowLinkConfig<Bitstream>[] { get followLinks(): FollowLinkConfig<Bitstream>[] {
return [ return BITSTREAM_PAGE_LINKS_TO_FOLLOW;
followLink('bundle', {}, followLink('item')),
followLink('format')
];
} }
} }

View File

@@ -2,8 +2,8 @@ import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
import { of as observableOf, EMPTY } from 'rxjs'; import { of as observableOf, 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 { RequestEntryState } from '../core/data/request.reducer';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { RequestEntryState } from '../core/data/request-entry-state.model';
describe(`LegacyBitstreamUrlResolver`, () => { describe(`LegacyBitstreamUrlResolver`, () => {
let resolver: LegacyBitstreamUrlResolver; let resolver: LegacyBitstreamUrlResolver;

View File

@@ -20,9 +20,9 @@ import { VarDirective } from '../../shared/utils/var.directive';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
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 { FindListOptions } from '../../core/data/request.models';
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('BrowseByDatePageComponent', () => { describe('BrowseByDatePageComponent', () => {
let comp: BrowseByDatePageComponent; let comp: BrowseByDatePageComponent;

View File

@@ -24,14 +24,19 @@
<section class="comcol-page-browse-section"> <section class="comcol-page-browse-section">
<div class="browse-by-metadata w-100"> <div class="browse-by-metadata w-100">
<ds-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100" <ds-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
title="{{'browse.title' | translate:{collection: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.' + browseId | translate, value: (value)? '&quot;' + value + '&quot;': ''} }}" title="{{'browse.title' | translate:
{
collection: (parent$ | async)?.payload?.name || '',
field: 'browse.metadata.' + browseId | translate,
startsWith: (startsWith)? ('browse.startsWith' | translate: { startsWith: '&quot;' + startsWith + '&quot;' }) : '',
value: (value)? '&quot;' + value + '&quot;': ''
} }}"
parentname="{{(parent$ | async)?.payload?.name || ''}}" parentname="{{(parent$ | async)?.payload?.name || ''}}"
[objects$]="(items$ !== undefined)? items$ : browseEntries$" [objects$]="(items$ !== undefined)? items$ : browseEntries$"
[paginationConfig]="(currentPagination$ |async)" [paginationConfig]="(currentPagination$ |async)"
[sortConfig]="(currentSort$ |async)" [sortConfig]="(currentSort$ |async)"
[type]="startsWithType" [type]="startsWithType"
[startsWithOptions]="startsWithOptions" [startsWithOptions]="startsWithOptions"
[enableArrows]="true"
(prev)="goPrev()" (prev)="goPrev()"
(next)="goNext()"> (next)="goNext()">
</ds-browse-by> </ds-browse-by>

View File

@@ -127,10 +127,10 @@ export class BrowseByMetadataPageComponent implements OnInit {
return [Object.assign({}, routeParams, queryParams),currentPage,currentSort]; return [Object.assign({}, routeParams, queryParams),currentPage,currentSort];
}) })
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
this.browseId = params.id || this.defaultBrowseId; this.browseId = params.id || this.defaultBrowseId;
this.authority = params.authority; this.authority = params.authority;
this.value = +params.value || params.value || ''; this.value = +params.value || params.value || '';
this.startsWith = +params.startsWith || params.startsWith; this.startsWith = +params.startsWith || params.startsWith;
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId); const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId);
if (isNotEmpty(this.value)) { if (isNotEmpty(this.value)) {
this.updatePageWithItems(searchOptions, this.value, this.authority); this.updatePageWithItems(searchOptions, this.value, this.authority);

View File

@@ -17,7 +17,7 @@ import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-bro
component: ThemedBrowseBySwitcherComponent, component: ThemedBrowseBySwitcherComponent,
canActivate: [BrowseByGuard], canActivate: [BrowseByGuard],
resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver }, resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver },
data: { title: 'browse.title', breadcrumbKey: 'browse.metadata' } data: { title: 'browse.title.page', breadcrumbKey: 'browse.metadata' }
} }
] ]
}]) }])

View File

@@ -20,9 +20,9 @@ import { VarDirective } from '../../shared/utils/var.directive';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
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 { FindListOptions } from '../../core/data/request.models';
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('BrowseByTitlePageComponent', () => { describe('BrowseByTitlePageComponent', () => {
let comp: BrowseByTitlePageComponent; let comp: BrowseByTitlePageComponent;

View File

@@ -45,7 +45,8 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
return [Object.assign({}, routeParams, queryParams),currentPage,currentSort]; return [Object.assign({}, routeParams, queryParams),currentPage,currentSort];
}) })
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
this.browseId = params.id || this.defaultBrowseId; this.startsWith = +params.startsWith || params.startsWith;
this.browseId = params.id || this.defaultBrowseId;
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId), undefined, undefined); this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId), undefined, undefined);
this.updateParent(params.scope); this.updateParent(params.scope);
})); }));

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 { EventEmitter } from '@angular/core'; import { ChangeDetectionStrategy, 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,6 +41,12 @@ 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 { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
import { GroupDataService } from '../../core/eperson/group-data.service';
import { LinkHeadService } from '../../core/services/link-head.service';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
describe('CollectionItemMapperComponent', () => { describe('CollectionItemMapperComponent', () => {
let comp: CollectionItemMapperComponent; let comp: CollectionItemMapperComponent;
@@ -110,15 +116,15 @@ describe('CollectionItemMapperComponent', () => {
}; };
const searchServiceStub = Object.assign(new SearchServiceStub(), { const searchServiceStub = Object.assign(new SearchServiceStub(), {
search: () => observableOf(emptyList), search: () => observableOf(emptyList),
/* tslint:disable:no-empty */ /* eslint-disable no-empty,@typescript-eslint/no-empty-function */
clearDiscoveryRequests: () => {} clearDiscoveryRequests: () => {}
/* tslint:enable:no-empty */ /* eslint-enable no-empty,@typescript-eslint/no-empty-function */
}); });
const collectionDataServiceStub = { const collectionDataServiceStub = {
getMappedItems: () => observableOf(emptyList), getMappedItems: () => observableOf(emptyList),
/* tslint:disable:no-empty */ /* eslint-disable no-empty,@typescript-eslint/no-empty-function */
clearMappedItemsRequests: () => {} clearMappedItemsRequests: () => {}
/* tslint:enable:no-empty */ /* eslint-enable no-empty, @typescript-eslint/no-empty-function */
}; };
const routeServiceStub = { const routeServiceStub = {
getRouteParameterValue: () => { getRouteParameterValue: () => {
@@ -141,6 +147,25 @@ describe('CollectionItemMapperComponent', () => {
isAuthorized: observableOf(true) isAuthorized: observableOf(true)
}); });
const linkHeadService = jasmine.createSpyObj('linkHeadService', {
addTag: ''
});
const groupDataService = jasmine.createSpyObj('groupsDataService', {
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
getGroupRegistryRouterLink: '',
getUUIDFromString: '',
});
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
name: 'test',
values: [
'org.dspace.ctask.general.ProfileFormats = test'
]
}))
});
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
@@ -157,8 +182,19 @@ describe('CollectionItemMapperComponent', () => {
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() },
{ provide: RouteService, useValue: routeServiceStub }, { provide: RouteService, useValue: routeServiceStub },
{ provide: AuthorizationDataService, useValue: authorizationDataService } { provide: AuthorizationDataService, useValue: authorizationDataService },
{ provide: GroupDataService, useValue: groupDataService },
{ provide: LinkHeadService, useValue: linkHeadService },
{ provide: ConfigurationDataService, useValue: configurationDataService },
] ]
}).overrideComponent(CollectionItemMapperComponent, {
set: {
providers: [
{
provide: SEARCH_CONFIG_SERVICE,
useClass: SearchConfigurationServiceStub
}
] }
}).compileComponents(); }).compileComponents();
})); }));

View File

@@ -18,9 +18,9 @@ import {
COLLECTION_CREATE_PATH COLLECTION_CREATE_PATH
} from './collection-page-routing-paths'; } from './collection-page-routing-paths';
import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard'; import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard';
import { MenuItemType } from '../shared/menu/initial-menus-state';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedCollectionPageComponent } from './themed-collection-page.component'; import { ThemedCollectionPageComponent } from './themed-collection-page.component';
import { MenuItemType } from '../shared/menu/menu-item-type.model';
@NgModule({ @NgModule({
imports: [ imports: [

View File

@@ -13,7 +13,6 @@
<!-- Collection logo --> <!-- Collection logo -->
<ds-comcol-page-logo *ngIf="logoRD$" <ds-comcol-page-logo *ngIf="logoRD$"
[logo]="(logoRD$ | async)?.payload" [logo]="(logoRD$ | async)?.payload"
[alternateText]="'Collection Logo'"
[alternateText]="'Collection Logo'"> [alternateText]="'Collection Logo'">
</ds-comcol-page-logo> </ds-comcol-page-logo>
@@ -34,7 +33,7 @@
[title]="'collection.page.news'"> [title]="'collection.page.news'">
</ds-comcol-page-content> </ds-comcol-page-content>
</header> </header>
<div class="pl-2"> <div class="pl-2 space-children-mr">
<ds-dso-page-edit-button *ngIf="isCollectionAdmin$ | async" [pageRoute]="collectionPageRoute$ | async" [dso]="collection" [tooltipMsg]="'collection.page.edit'"></ds-dso-page-edit-button> <ds-dso-page-edit-button *ngIf="isCollectionAdmin$ | async" [pageRoute]="collectionPageRoute$ | async" [dso]="collection" [tooltipMsg]="'collection.page.edit'"></ds-dso-page-edit-button>
</div> </div>
</div> </div>

View File

@@ -16,7 +16,6 @@ import { Item } from '../core/shared/item.model';
import { import {
getAllSucceededRemoteDataPayload, getAllSucceededRemoteDataPayload,
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
redirectOn4xx,
toDSpaceObjectListRD toDSpaceObjectListRD
} from '../core/shared/operators'; } from '../core/shared/operators';
@@ -28,6 +27,7 @@ import { PaginationService } from '../core/pagination/pagination.service';
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 { FeatureID } from '../core/data/feature-authorization/feature-id';
import { getCollectionPageRoute } from './collection-page-routing-paths'; import { getCollectionPageRoute } from './collection-page-routing-paths';
import { redirectOn4xx } from '../core/shared/authorized.operators';
@Component({ @Component({
selector: 'ds-collection-page', selector: 'ds-collection-page',

View File

@@ -5,11 +5,11 @@
<h2 id="header" class="border-bottom pb-2">{{ 'collection.delete.head' | translate}}</h2> <h2 id="header" class="border-bottom pb-2">{{ 'collection.delete.head' | translate}}</h2>
<p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dso.name } }}</p> <p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dso.name } }}</p>
<div class="form-group row"> <div class="form-group row">
<div class="col text-right"> <div class="col text-right space-children-mr">
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)"> <button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)">
<i class="fas fa-times"></i> {{'collection.delete.cancel' | translate}} <i class="fas fa-times"></i> {{'collection.delete.cancel' | translate}}
</button> </button>
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)" [disabled]="(processing$ | async)"> <button class="btn btn-danger" (click)="onConfirm(dso)" [disabled]="(processing$ | async)">
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'collection.delete.processing' | translate}}</span> <span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'collection.delete.processing' | translate}}</span>
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}</span> <span *ngIf="!(processing$ | async)"><i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}</span>
</button> </button>

View File

@@ -5,7 +5,6 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { RequestService } from '../../core/data/request.service';
/** /**
* Component that represents the page where a user can delete an existing Collection * Component that represents the page where a user can delete an existing Collection
@@ -24,8 +23,7 @@ export class DeleteCollectionPageComponent extends DeleteComColPageComponent<Col
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected notifications: NotificationsService, protected notifications: NotificationsService,
protected translate: TranslateService, protected translate: TranslateService,
protected requestService: RequestService
) { ) {
super(dsoDataService, router, route, notifications, translate, requestService); super(dsoDataService, router, route, notifications, translate);
} }
} }

View File

@@ -1,6 +1,6 @@
<div class="container-fluid mb-2" *ngVar="(itemTemplateRD$ | async) as itemTemplateRD"> <div class="container-fluid mb-2" *ngVar="(itemTemplateRD$ | async) as itemTemplateRD">
<label>{{ 'collection.edit.template.label' | translate}}</label> <label>{{ 'collection.edit.template.label' | translate}}</label>
<div class="button-row"> <div class="button-row space-children-mr">
<button *ngIf="!itemTemplateRD?.payload" class="btn btn-success" (click)="addItemTemplate()"> <button *ngIf="!itemTemplateRD?.payload" class="btn btn-success" (click)="addItemTemplate()">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
<span class="d-none d-sm-inline">&nbsp;{{"collection.edit.template.add-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"collection.edit.template.add-button" | translate}}</span>

View File

@@ -12,7 +12,6 @@ import { NotificationsService } from '../../../shared/notifications/notification
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { ItemTemplateDataService } from '../../../core/data/item-template-data.service'; import { ItemTemplateDataService } from '../../../core/data/item-template-data.service';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths'; import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
@@ -49,9 +48,6 @@ describe('CollectionMetadataComponent', () => {
success: {}, success: {},
error: {} error: {}
}); });
const objectCache = jasmine.createSpyObj('objectCache', {
remove: {}
});
const requestService = jasmine.createSpyObj('requestService', { const requestService = jasmine.createSpyObj('requestService', {
setStaleByHrefSubstring: {} setStaleByHrefSubstring: {}
}); });
@@ -65,8 +61,7 @@ describe('CollectionMetadataComponent', () => {
{ provide: ItemTemplateDataService, useValue: itemTemplateServiceStub }, { provide: ItemTemplateDataService, useValue: itemTemplateServiceStub },
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } }, { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } },
{ provide: NotificationsService, useValue: notificationsService }, { provide: NotificationsService, useValue: notificationsService },
{ provide: ObjectCacheService, useValue: objectCache }, { provide: RequestService, useValue: requestService },
{ provide: RequestService, useValue: requestService }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -95,20 +90,18 @@ describe('CollectionMetadataComponent', () => {
}); });
describe('deleteItemTemplate', () => { describe('deleteItemTemplate', () => {
describe('when delete returns a success', () => {
beforeEach(() => { beforeEach(() => {
(itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(true)); (itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(true));
comp.deleteItemTemplate(); comp.deleteItemTemplate();
}); });
it('should display a success notification', () => { it('should call ItemTemplateService.deleteByCollectionID', () => {
expect(notificationsService.success).toHaveBeenCalled(); expect(itemTemplateService.deleteByCollectionID).toHaveBeenCalledWith(template, 'collection-id');
}); });
it('should reset related object and request cache', () => { describe('when delete returns a success', () => {
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(collectionTemplateHref); it('should display a success notification', () => {
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(template.self); expect(notificationsService.success).toHaveBeenCalled();
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(collection.self);
}); });
}); });

View File

@@ -8,10 +8,9 @@ import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
import { switchMap, tap } from 'rxjs/operators'; import { switchMap } 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 { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths'; import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
@@ -38,8 +37,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected translate: TranslateService, protected translate: TranslateService,
protected objectCache: ObjectCacheService, protected requestService: RequestService,
protected requestService: RequestService
) { ) {
super(collectionDataService, router, route, notificationsService, translate); super(collectionDataService, router, route, notificationsService, translate);
} }
@@ -93,23 +91,9 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
getFirstSucceededRemoteDataPayload(), getFirstSucceededRemoteDataPayload(),
)), )),
); );
const templateHref$ = collection$.pipe( combineLatestObservable(collection$, template$).pipe(
switchMap((collection) => this.itemTemplateService.getCollectionEndpoint(collection.id)), switchMap(([collection, template]) => {
); return this.itemTemplateService.deleteByCollectionID(template, collection.uuid);
combineLatestObservable(collection$, template$, templateHref$).pipe(
switchMap(([collection, template, templateHref]) => {
return this.itemTemplateService.deleteByCollectionID(template, collection.uuid).pipe(
tap((success: boolean) => {
if (success) {
this.objectCache.remove(templateHref);
this.objectCache.remove(template.self);
this.requestService.setStaleByHrefSubstring(template.self);
this.requestService.setStaleByHrefSubstring(templateHref);
this.requestService.setStaleByHrefSubstring(collection.self);
}
})
);
}) })
).subscribe((success: boolean) => { ).subscribe((success: boolean) => {
if (success) { if (success) {

View File

@@ -13,6 +13,8 @@ import { RouterTestingModule } from '@angular/router/testing';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ComcolModule } from '../../../shared/comcol/comcol.module'; import { ComcolModule } from '../../../shared/comcol/comcol.module';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
describe('CollectionRolesComponent', () => { describe('CollectionRolesComponent', () => {
@@ -79,6 +81,7 @@ describe('CollectionRolesComponent', () => {
{ provide: ActivatedRoute, useValue: route }, { provide: ActivatedRoute, useValue: route },
{ provide: RequestService, useValue: requestService }, { provide: RequestService, useValue: requestService },
{ provide: GroupDataService, useValue: groupDataService }, { provide: GroupDataService, useValue: groupDataService },
{ provide: NotificationsService, useClass: NotificationsServiceStub }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -1,5 +1,5 @@
<div *ngVar="(contentSource$ |async) as contentSource"> <div *ngVar="(contentSource$ |async) as contentSource">
<div class="container-fluid" *ngIf="shouldShow"> <div class="container-fluid space-children-mr" *ngIf="shouldShow">
<h4>{{ 'collection.source.controls.head' | translate }}</h4> <h4>{{ 'collection.source.controls.head' | translate }}</h4>
<div> <div>
<span class="font-weight-bold">{{'collection.source.controls.harvest.status' | translate}}</span> <span class="font-weight-bold">{{'collection.source.controls.harvest.status' | translate}}</span>

View File

@@ -1,5 +1,5 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="d-inline-block float-right"> <div class="d-inline-block float-right space-children-mr">
<button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)" <button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)" [disabled]="!(hasChanges() | async)"
(click)="discard()"><i (click)="discard()"><i
@@ -43,7 +43,7 @@
<div class="container mt-2" *ngIf="(contentSource?.harvestType !== harvestTypeNone)"> <div class="container mt-2" *ngIf="(contentSource?.harvestType !== harvestTypeNone)">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="d-inline-block float-right ml-1"> <div class="d-inline-block float-right ml-1 space-children-mr">
<button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)" <button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)" [disabled]="!(hasChanges() | async)"
(click)="discard()"><i (click)="discard()"><i

View File

@@ -9,7 +9,6 @@ import { ContentSource, ContentSourceHarvestType } from '../../../core/shared/co
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { INotification, Notification } from '../../../shared/notifications/models/notification.model'; import { INotification, Notification } from '../../../shared/notifications/models/notification.model';
import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { NotificationType } from '../../../shared/notifications/models/notification-type';
import { FieldUpdate } from '../../../core/data/object-updates/object-updates.reducer';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { DynamicFormControlModel, DynamicFormService } from '@ng-dynamic-forms/core'; import { DynamicFormControlModel, DynamicFormService } from '@ng-dynamic-forms/core';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
@@ -20,6 +19,7 @@ import { Collection } from '../../../core/shared/collection.model';
import { CollectionDataService } from '../../../core/data/collection-data.service'; import { CollectionDataService } from '../../../core/data/collection-data.service';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { FieldUpdate } from '../../../core/data/object-updates/field-update.model';
const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');

View File

@@ -23,7 +23,6 @@ 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 { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
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';
@@ -31,6 +30,8 @@ import { MetadataConfig } from '../../../core/shared/metadata-config.model';
import { INotification } from '../../../shared/notifications/models/notification.model'; import { INotification } from '../../../shared/notifications/models/notification.model';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { FieldUpdate } from '../../../core/data/object-updates/field-update.model';
import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model';
/** /**
* Component for managing the content source of the collection * Component for managing the content source of the collection

View File

@@ -6,7 +6,7 @@ import { CollectionPageComponent } from './collection-page.component';
* Themed wrapper for CollectionPageComponent * Themed wrapper for CollectionPageComponent
*/ */
@Component({ @Component({
selector: 'ds-themed-community-page', selector: 'ds-themed-collection-page',
styleUrls: [], styleUrls: [],
templateUrl: '../shared/theme-support/themed.component.html', templateUrl: '../shared/theme-support/themed.component.html',
}) })

View File

@@ -1,9 +1,10 @@
import { FindListOptions } from '../core/data/request.models';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { CommunityListService, FlatNode } from './community-list-service'; import { CommunityListService} from './community-list-service';
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { finalize } from 'rxjs/operators'; import { finalize } from 'rxjs/operators';
import { FlatNode } from './flat-node.model';
import { FindListOptions } from '../core/data/find-list-options.model';
/** /**
* DataSource object needed by a CDK Tree to render its nodes. * DataSource object needed by a CDK Tree to render its nodes.

View File

@@ -7,13 +7,14 @@ import { SortDirection, SortOptions } from '../core/cache/models/sort-options.mo
import { buildPaginatedList } from '../core/data/paginated-list.model'; import { buildPaginatedList } from '../core/data/paginated-list.model';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { StoreMock } from '../shared/testing/store.mock'; import { StoreMock } from '../shared/testing/store.mock';
import { CommunityListService, FlatNode, toFlatNode } from './community-list-service'; import { CommunityListService, toFlatNode } from './community-list-service';
import { CollectionDataService } from '../core/data/collection-data.service'; import { CollectionDataService } from '../core/data/collection-data.service';
import { CommunityDataService } from '../core/data/community-data.service'; import { CommunityDataService } from '../core/data/community-data.service';
import { Community } from '../core/shared/community.model'; import { Community } from '../core/shared/community.model';
import { Collection } from '../core/shared/collection.model'; import { Collection } from '../core/shared/collection.model';
import { FindListOptions } from '../core/data/request.models';
import { PageInfo } from '../core/shared/page-info.model'; import { PageInfo } from '../core/shared/page-info.model';
import { FlatNode } from './flat-node.model';
import { FindListOptions } from '../core/data/find-list-options.model';
describe('CommunityListService', () => { describe('CommunityListService', () => {
let store: StoreMock<AppState>; let store: StoreMock<AppState>;

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-classes-per-file */
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { createSelector, Store } from '@ngrx/store'; import { createSelector, Store } from '@ngrx/store';
@@ -6,45 +7,22 @@ import { filter, map, switchMap } from 'rxjs/operators';
import { AppState } from '../app.reducer'; import { AppState } from '../app.reducer';
import { CommunityDataService } from '../core/data/community-data.service'; import { CommunityDataService } from '../core/data/community-data.service';
import { FindListOptions } from '../core/data/request.models';
import { Community } from '../core/shared/community.model'; import { Community } from '../core/shared/community.model';
import { Collection } from '../core/shared/collection.model'; import { Collection } from '../core/shared/collection.model';
import { PageInfo } from '../core/shared/page-info.model'; import { PageInfo } from '../core/shared/page-info.model';
import { hasValue, isNotEmpty } from '../shared/empty.util'; import { hasValue, isNotEmpty } from '../shared/empty.util';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { PaginatedList, buildPaginatedList } from '../core/data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../core/data/paginated-list.model';
import { CollectionDataService } from '../core/data/collection-data.service'; import { CollectionDataService } from '../core/data/collection-data.service';
import { CommunityListSaveAction } from './community-list.actions'; import { CommunityListSaveAction } from './community-list.actions';
import { CommunityListState } from './community-list.reducer'; import { CommunityListState } from './community-list.reducer';
import { getCommunityPageRoute } from '../community-page/community-page-routing-paths'; import { getCommunityPageRoute } from '../community-page/community-page-routing-paths';
import { getCollectionPageRoute } from '../collection-page/collection-page-routing-paths'; import { getCollectionPageRoute } from '../collection-page/collection-page-routing-paths';
import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../core/shared/operators'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../core/shared/operators';
import { followLink } from '../shared/utils/follow-link-config.model'; import { followLink } from '../shared/utils/follow-link-config.model';
import { FlatNode } from './flat-node.model';
/** import { ShowMoreFlatNode } from './show-more-flat-node.model';
* Each node in the tree is represented by a flatNode which contains info about the node itself and its position and import { FindListOptions } from '../core/data/find-list-options.model';
* state in the tree. There are nodes representing communities, collections and show more links.
*/
export interface FlatNode {
isExpandable$: Observable<boolean>;
name: string;
id: string;
level: number;
isExpanded?: boolean;
parent?: FlatNode;
payload: Community | Collection | ShowMoreFlatNode;
isShowMoreNode: boolean;
route?: string;
currentCommunityPage?: number;
currentCollectionPage?: number;
}
/**
* The show more links in the community tree are also represented by a flatNode so we know where in
* the tree it should be rendered an who its parent is (needed for the action resulting in clicking this link)
*/
export class ShowMoreFlatNode {
}
// Helper method to combine an flatten an array of observables of flatNode arrays // Helper method to combine an flatten an array of observables of flatNode arrays
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> => export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
@@ -108,7 +86,6 @@ export const MAX_COMCOLS_PER_PAGE = 20;
* Service class for the community list, responsible for the creating of the flat list used by communityList dataSource * Service class for the community list, responsible for the creating of the flat list used by communityList dataSource
* and connection to the store to retrieve and save the state of the community list * and connection to the store to retrieve and save the state of the community list
*/ */
// tslint:disable-next-line:max-classes-per-file
@Injectable() @Injectable()
export class CommunityListService { export class CommunityListService {

View File

@@ -1,6 +1,6 @@
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { type } from '../shared/ngrx/type'; import { type } from '../shared/ngrx/type';
import { FlatNode } from './community-list-service'; import { FlatNode } from './flat-node.model';
/** /**
* All the action types of the community-list * All the action types of the community-list

View File

@@ -1,5 +1,5 @@
import { FlatNode } from './community-list-service';
import { CommunityListActions, CommunityListActionTypes, CommunityListSaveAction } from './community-list.actions'; import { CommunityListActions, CommunityListActionTypes, CommunityListSaveAction } from './community-list.actions';
import { FlatNode } from './flat-node.model';
/** /**
* States we wish to put in store concerning the community list * States we wish to put in store concerning the community list

View File

@@ -8,7 +8,7 @@
<span class="fa fa-chevron-right invisible" aria-hidden="true"></span> <span class="fa fa-chevron-right invisible" aria-hidden="true"></span>
</button> </button>
<div class="align-middle pt-2"> <div class="align-middle pt-2">
<a *ngIf="node!==loadingNode" [routerLink]="" (click)="getNextPage(node)" <a *ngIf="node!==loadingNode" [routerLink]="[]" (click)="getNextPage(node)"
class="btn btn-outline-primary btn-sm" role="button"> class="btn btn-outline-primary btn-sm" role="button">
<i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }} <i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }}
</a> </a>

View File

@@ -1,7 +1,7 @@
import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { CommunityListComponent } from './community-list.component'; import { CommunityListComponent } from './community-list.component';
import { CommunityListService, FlatNode, showMoreFlatNode, toFlatNode } from '../community-list-service'; import { CommunityListService, showMoreFlatNode, toFlatNode } from '../community-list-service';
import { CdkTreeModule } from '@angular/cdk/tree'; import { CdkTreeModule } from '@angular/cdk/tree';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
@@ -15,6 +15,7 @@ import { Collection } from '../../core/shared/collection.model';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { FlatNode } from '../flat-node.model';
describe('CommunityListComponent', () => { describe('CommunityListComponent', () => {
let component: CommunityListComponent; let component: CommunityListComponent;

View File

@@ -1,11 +1,12 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { FindListOptions } from '../../core/data/request.models'; import { CommunityListService} from '../community-list-service';
import { CommunityListService, FlatNode } from '../community-list-service';
import { CommunityListDatasource } from '../community-list-datasource'; import { CommunityListDatasource } from '../community-list-datasource';
import { FlatTreeControl } from '@angular/cdk/tree'; import { FlatTreeControl } from '@angular/cdk/tree';
import { isEmpty } from '../../shared/empty.util'; import { isEmpty } from '../../shared/empty.util';
import { FlatNode } from '../flat-node.model';
import { FindListOptions } from '../../core/data/find-list-options.model';
/** /**
* A tree-structured list of nodes representing the communities, their subCommunities and collections. * A tree-structured list of nodes representing the communities, their subCommunities and collections.

View File

@@ -7,7 +7,8 @@ import { Component } from '@angular/core';
selector: 'ds-themed-community-list', selector: 'ds-themed-community-list',
styleUrls: [], styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html', templateUrl: '../../shared/theme-support/themed.component.html',
})export class ThemedCommunityListComponent extends ThemedComponent<CommunityListComponent> { })
export class ThemedCommunityListComponent extends ThemedComponent<CommunityListComponent> {
protected getComponentName(): string { protected getComponentName(): string {
return 'CommunityListComponent'; return 'CommunityListComponent';
} }

View File

@@ -0,0 +1,22 @@
import { Observable } from 'rxjs';
import { Community } from '../core/shared/community.model';
import { Collection } from '../core/shared/collection.model';
import { ShowMoreFlatNode } from './show-more-flat-node.model';
/**
* Each node in the tree is represented by a flatNode which contains info about the node itself and its position and
* state in the tree. There are nodes representing communities, collections and show more links.
*/
export interface FlatNode {
isExpandable$: Observable<boolean>;
name: string;
id: string;
level: number;
isExpanded?: boolean;
parent?: FlatNode;
payload: Community | Collection | ShowMoreFlatNode;
isShowMoreNode: boolean;
route?: string;
currentCommunityPage?: number;
currentCollectionPage?: number;
}

View File

@@ -0,0 +1,6 @@
/**
* The show more links in the community tree are also represented by a flatNode so we know where in
* the tree it should be rendered an who its parent is (needed for the action resulting in clicking this link)
*/
export class ShowMoreFlatNode {
}

View File

@@ -11,9 +11,9 @@ import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.servi
import { LinkService } from '../core/cache/builders/link.service'; import { LinkService } from '../core/cache/builders/link.service';
import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-routing-paths'; import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-routing-paths';
import { CommunityPageAdministratorGuard } from './community-page-administrator.guard'; import { CommunityPageAdministratorGuard } from './community-page-administrator.guard';
import { MenuItemType } from '../shared/menu/initial-menus-state';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedCommunityPageComponent } from './themed-community-page.component'; import { ThemedCommunityPageComponent } from './themed-community-page.component';
import { MenuItemType } from '../shared/menu/menu-item-type.model';
@NgModule({ @NgModule({
imports: [ imports: [

View File

@@ -20,7 +20,7 @@
[title]="'community.page.news'"> [title]="'community.page.news'">
</ds-comcol-page-content> </ds-comcol-page-content>
</header> </header>
<div class="pl-2"> <div class="pl-2 space-children-mr">
<ds-dso-page-edit-button *ngIf="isCommunityAdmin$ | async" [pageRoute]="communityPageRoute$ | async" [dso]="communityPayload" [tooltipMsg]="'community.page.edit'"></ds-dso-page-edit-button> <ds-dso-page-edit-button *ngIf="isCommunityAdmin$ | async" [pageRoute]="communityPageRoute$ | async" [dso]="communityPayload" [tooltipMsg]="'community.page.edit'"></ds-dso-page-edit-button>
</div> </div>
</div> </div>

View File

@@ -13,11 +13,12 @@ import { MetadataService } from '../core/metadata/metadata.service';
import { fadeInOut } from '../shared/animations/fade'; import { fadeInOut } from '../shared/animations/fade';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../core/shared/operators'; import { getAllSucceededRemoteDataPayload} from '../core/shared/operators';
import { AuthService } from '../core/auth/auth.service'; import { AuthService } from '../core/auth/auth.service';
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 { FeatureID } from '../core/data/feature-authorization/feature-id';
import { getCommunityPageRoute } from './community-page-routing-paths'; import { getCommunityPageRoute } from './community-page-routing-paths';
import { redirectOn4xx } from '../core/shared/authorized.operators';
@Component({ @Component({
selector: 'ds-community-page', selector: 'ds-community-page',

View File

@@ -5,11 +5,11 @@
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate}}</h2> <h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate}}</h2>
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p> <p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
<div class="form-group row"> <div class="form-group row">
<div class="col text-right"> <div class="col text-right space-children-mr">
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)"> <button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)">
<i class="fas fa-times"></i> {{'community.delete.cancel' | translate}} <i class="fas fa-times"></i> {{'community.delete.cancel' | translate}}
</button> </button>
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)" [disabled]="(processing$ | async)"> <button class="btn btn-danger" (click)="onConfirm(dso)" [disabled]="(processing$ | async)">
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'community.delete.processing' | translate}}</span> <span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'community.delete.processing' | translate}}</span>
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}</span> <span *ngIf="!(processing$ | async)"><i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}</span>
</button> </button>

View File

@@ -5,7 +5,6 @@ import { ActivatedRoute, Router } from '@angular/router';
import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component'; import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component';
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 { RequestService } from '../../core/data/request.service';
/** /**
* Component that represents the page where a user can delete an existing Community * Component that represents the page where a user can delete an existing Community
@@ -24,9 +23,8 @@ export class DeleteCommunityPageComponent extends DeleteComColPageComponent<Comm
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected notifications: NotificationsService, protected notifications: NotificationsService,
protected translate: TranslateService, protected translate: TranslateService,
protected requestService: RequestService
) { ) {
super(dsoDataService, router, route, notifications, translate, requestService); super(dsoDataService, router, route, notifications, translate);
} }
} }

View File

@@ -13,6 +13,8 @@ import { RouterTestingModule } from '@angular/router/testing';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ComcolModule } from '../../../shared/comcol/comcol.module'; import { ComcolModule } from '../../../shared/comcol/comcol.module';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
describe('CommunityRolesComponent', () => { describe('CommunityRolesComponent', () => {
@@ -64,6 +66,7 @@ describe('CommunityRolesComponent', () => {
{ provide: ActivatedRoute, useValue: route }, { provide: ActivatedRoute, useValue: route },
{ provide: RequestService, useValue: requestService }, { provide: RequestService, useValue: requestService },
{ provide: GroupDataService, useValue: groupDataService }, { provide: GroupDataService, useValue: groupDataService },
{ provide: NotificationsService, useClass: NotificationsServiceStub }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -11,7 +11,6 @@ import { CommunityPageSubCollectionListComponent } from './community-page-sub-co
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import { FindListOptions } from '../../core/data/request.models';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { buildPaginatedList } from '../../core/data/paginated-list.model'; import { buildPaginatedList } from '../../core/data/paginated-list.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
@@ -25,6 +24,15 @@ 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';
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';
import { GroupDataService } from '../../core/eperson/group-data.service';
import { LinkHeadService } from '../../core/services/link-head.service';
import { ConfigurationDataService } from '../../core/data/configuration-data.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 { createPaginatedList } from '../../shared/testing/utils.test';
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
describe('CommunityPageSubCollectionList Component', () => { describe('CommunityPageSubCollectionList Component', () => {
let comp: CommunityPageSubCollectionListComponent; let comp: CommunityPageSubCollectionListComponent;
@@ -122,6 +130,25 @@ describe('CommunityPageSubCollectionList Component', () => {
themeService = getMockThemeService(); themeService = getMockThemeService();
const linkHeadService = jasmine.createSpyObj('linkHeadService', {
addTag: ''
});
const groupDataService = jasmine.createSpyObj('groupsDataService', {
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
getGroupRegistryRouterLink: '',
getUUIDFromString: '',
});
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
name: 'test',
values: [
'org.dspace.ctask.general.ProfileFormats = test'
]
}))
});
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
@@ -138,6 +165,10 @@ describe('CommunityPageSubCollectionList Component', () => {
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: SelectableListService, useValue: {} }, { provide: SelectableListService, useValue: {} },
{ provide: ThemeService, useValue: themeService }, { provide: ThemeService, useValue: themeService },
{ provide: GroupDataService, useValue: groupDataService },
{ provide: LinkHeadService, useValue: linkHeadService },
{ provide: ConfigurationDataService, useValue: configurationDataService },
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -13,7 +13,6 @@ import { buildPaginatedList } from '../../core/data/paginated-list.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { FindListOptions } from '../../core/data/request.models';
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 { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
@@ -25,6 +24,14 @@ 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';
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';
import { GroupDataService } from '../../core/eperson/group-data.service';
import { LinkHeadService } from '../../core/services/link-head.service';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
import { createPaginatedList } from '../../shared/testing/utils.test';
describe('CommunityPageSubCommunityListComponent Component', () => { describe('CommunityPageSubCommunityListComponent Component', () => {
let comp: CommunityPageSubCommunityListComponent; let comp: CommunityPageSubCommunityListComponent;
@@ -119,6 +126,25 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
} }
}; };
const linkHeadService = jasmine.createSpyObj('linkHeadService', {
addTag: ''
});
const groupDataService = jasmine.createSpyObj('groupsDataService', {
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
getGroupRegistryRouterLink: '',
getUUIDFromString: '',
});
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
name: 'test',
values: [
'org.dspace.ctask.general.ProfileFormats = test'
]
}))
});
const paginationService = new PaginationServiceStub(); const paginationService = new PaginationServiceStub();
themeService = getMockThemeService(); themeService = getMockThemeService();
@@ -139,6 +165,10 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: SelectableListService, useValue: {} }, { provide: SelectableListService, useValue: {} },
{ provide: ThemeService, useValue: themeService }, { provide: ThemeService, useValue: themeService },
{ provide: GroupDataService, useValue: groupDataService },
{ provide: LinkHeadService, useValue: linkHeadService },
{ provide: ConfigurationDataService, useValue: configurationDataService },
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -3,7 +3,7 @@ import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxj
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { GetRequest, PostRequest, RestRequest, } from '../data/request.models'; import { GetRequest, PostRequest, } from '../data/request.models';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { getFirstCompletedRemoteData } from '../shared/operators'; import { getFirstCompletedRemoteData } from '../shared/operators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -11,13 +11,14 @@ import { RemoteData } from '../data/remote-data';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';
import { ShortLivedToken } from './models/short-lived-token.model'; import { ShortLivedToken } from './models/short-lived-token.model';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { RestRequest } from '../data/rest-request.model';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
/** /**
* Abstract service to send authentication requests * Abstract service to send authentication requests
*/ */
export abstract class AuthRequestService { export abstract class AuthRequestService {
protected linkName = 'authn'; protected linkName = 'authn';
protected browseEndpoint = '';
protected shortlivedtokensEndpoint = 'shortlivedtokens'; protected shortlivedtokensEndpoint = 'shortlivedtokens';
constructor(protected halService: HALEndpointService, constructor(protected halService: HALEndpointService,
@@ -26,14 +27,21 @@ export abstract class AuthRequestService {
) { ) {
} }
protected fetchRequest(request: RestRequest): Observable<RemoteData<AuthStatus>> { protected fetchRequest(request: RestRequest, ...linksToFollow: FollowLinkConfig<AuthStatus>[]): Observable<RemoteData<AuthStatus>> {
return this.rdbService.buildFromRequestUUID<AuthStatus>(request.uuid).pipe( return this.rdbService.buildFromRequestUUID<AuthStatus>(request.uuid, ...linksToFollow).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
); );
} }
protected getEndpointByMethod(endpoint: string, method: string): string { protected getEndpointByMethod(endpoint: string, method: string, ...linksToFollow: FollowLinkConfig<AuthStatus>[]): string {
return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`; let url = isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`;
if (linksToFollow?.length > 0) {
linksToFollow.forEach((link: FollowLinkConfig<AuthStatus>, index: number) => {
url += ((index === 0) ? '?' : '&') + `embed=${link.name}`;
});
}
return url;
} }
public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<RemoteData<AuthStatus>> { public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<RemoteData<AuthStatus>> {
@@ -47,14 +55,14 @@ export abstract class AuthRequestService {
distinctUntilChanged()); distinctUntilChanged());
} }
public getRequest(method: string, options?: HttpOptions): Observable<RemoteData<AuthStatus>> { public getRequest(method: string, options?: HttpOptions, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<AuthStatus>> {
return this.halService.getEndpoint(this.linkName).pipe( return this.halService.getEndpoint(this.linkName).pipe(
filter((href: string) => isNotEmpty(href)), filter((href: string) => isNotEmpty(href)),
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), map((endpointURL) => this.getEndpointByMethod(endpointURL, method, ...linksToFollow)),
distinctUntilChanged(), distinctUntilChanged(),
map((endpointURL: string) => new GetRequest(this.requestService.generateRequestId(), endpointURL, undefined, options)), map((endpointURL: string) => new GetRequest(this.requestService.generateRequestId(), endpointURL, undefined, options)),
tap((request: GetRequest) => this.requestService.send(request)), tap((request: GetRequest) => this.requestService.send(request)),
mergeMap((request: GetRequest) => this.fetchRequest(request)), mergeMap((request: GetRequest) => this.fetchRequest(request, ...linksToFollow)),
distinctUntilChanged()); distinctUntilChanged());
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-classes-per-file */
// import @ngrx // import @ngrx
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
// import type function // import type function
@@ -39,7 +40,6 @@ export const AuthActionTypes = {
UNSET_USER_AS_IDLE: type('dspace/auth/UNSET_USER_AS_IDLE') UNSET_USER_AS_IDLE: type('dspace/auth/UNSET_USER_AS_IDLE')
}; };
/* tslint:disable:max-classes-per-file */
/** /**
* Authenticate. * Authenticate.
@@ -411,7 +411,6 @@ export class SetUserAsIdleAction implements Action {
export class UnsetUserAsIdleAction implements Action { export class UnsetUserAsIdleAction implements Action {
public type: string = AuthActionTypes.UNSET_USER_AS_IDLE; public type: string = AuthActionTypes.UNSET_USER_AS_IDLE;
} }
/* tslint:enable:max-classes-per-file */
/** /**
* Actions type. * Actions type.

View File

@@ -1,4 +1,4 @@
import { fakeAsync, flush, TestBed } from '@angular/core/testing'; import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing'; import { provideMockActions } from '@ngrx/effects/testing';
import { Store, StoreModule } from '@ngrx/store'; import { Store, StoreModule } from '@ngrx/store';
@@ -396,44 +396,43 @@ describe('AuthEffects', () => {
}); });
describe('when auth loaded is false', () => { describe('when auth loaded is false', () => {
it('should not call removeToken method', (done) => { it('should not call removeToken method', fakeAsync(() => {
store.overrideSelector(isAuthenticatedLoaded, false); store.overrideSelector(isAuthenticatedLoaded, false);
actions = hot('--a-|', { a: { type: StoreActionTypes.REHYDRATE } }); actions = observableOf({ type: StoreActionTypes.REHYDRATE });
spyOn(authServiceStub, 'removeToken'); spyOn(authServiceStub, 'removeToken');
authEffects.clearInvalidTokenOnRehydrate$.subscribe(() => { authEffects.clearInvalidTokenOnRehydrate$.subscribe(() => {
expect(false).toBeTrue(); // subscribe to trigger taps, fail if the effect emits (we don't expect it to)
});
tick(1000);
expect(authServiceStub.removeToken).not.toHaveBeenCalled(); expect(authServiceStub.removeToken).not.toHaveBeenCalled();
}));
});
done();
});
}); });
describe('when auth loaded is true', () => { describe('when auth loaded is true', () => {
it('should call removeToken method', fakeAsync(() => { it('should call removeToken method', (done) => {
spyOn(console, 'log').and.callThrough();
store.overrideSelector(isAuthenticatedLoaded, true); store.overrideSelector(isAuthenticatedLoaded, true);
actions = hot('--a-|', { a: { type: StoreActionTypes.REHYDRATE } }); actions = observableOf({ type: StoreActionTypes.REHYDRATE });
spyOn(authServiceStub, 'removeToken'); spyOn(authServiceStub, 'removeToken');
authEffects.clearInvalidTokenOnRehydrate$.subscribe(() => { authEffects.clearInvalidTokenOnRehydrate$.subscribe(() => {
expect(authServiceStub.removeToken).toHaveBeenCalled(); expect(authServiceStub.removeToken).toHaveBeenCalled();
flush(); done();
});
}); });
}));
}); });
}); });
describe('invalidateAuthorizationsRequestCache$', () => { describe('invalidateAuthorizationsRequestCache$', () => {
it('should call invalidateAuthorizationsRequestCache method in response to a REHYDRATE action', (done) => { it('should call invalidateAuthorizationsRequestCache method in response to a REHYDRATE action', (done) => {
actions = hot('--a-|', { a: { type: StoreActionTypes.REHYDRATE } }); actions = observableOf({ type: StoreActionTypes.REHYDRATE });
authEffects.invalidateAuthorizationsRequestCache$.subscribe(() => { authEffects.invalidateAuthorizationsRequestCache$.subscribe(() => {
expect((authEffects as any).authorizationsService.invalidateAuthorizationsRequestCache).toHaveBeenCalled(); expect((authEffects as any).authorizationsService.invalidateAuthorizationsRequestCache).toHaveBeenCalled();
});
done(); done();
}); });
}); });
});
}); });

View File

@@ -10,7 +10,7 @@ import {
} from 'rxjs'; } from 'rxjs';
import { catchError, filter, map, observeOn, switchMap, take, tap } from 'rxjs/operators'; import { catchError, filter, map, observeOn, switchMap, take, tap } from 'rxjs/operators';
// import @ngrx // import @ngrx
import { Actions, Effect, ofType } from '@ngrx/effects'; import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Action, select, Store } from '@ngrx/store'; import { Action, select, Store } from '@ngrx/store';
// import services // import services
@@ -67,8 +67,7 @@ export class AuthEffects {
* Authenticate user. * Authenticate user.
* @method authenticate * @method authenticate
*/ */
@Effect() public authenticate$: Observable<Action> = createEffect(() => this.actions$.pipe(
public authenticate$: Observable<Action> = this.actions$.pipe(
ofType(AuthActionTypes.AUTHENTICATE), ofType(AuthActionTypes.AUTHENTICATE),
switchMap((action: AuthenticateAction) => { switchMap((action: AuthenticateAction) => {
return this.authService.authenticate(action.payload.email, action.payload.password).pipe( return this.authService.authenticate(action.payload.email, action.payload.password).pipe(
@@ -77,26 +76,23 @@ export class AuthEffects {
catchError((error) => observableOf(new AuthenticationErrorAction(error))) catchError((error) => observableOf(new AuthenticationErrorAction(error)))
); );
}) })
); ));
@Effect() public authenticateSuccess$: Observable<Action> = createEffect(() => this.actions$.pipe(
public authenticateSuccess$: Observable<Action> = this.actions$.pipe(
ofType(AuthActionTypes.AUTHENTICATE_SUCCESS), ofType(AuthActionTypes.AUTHENTICATE_SUCCESS),
map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)) map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload))
); ));
@Effect() public authenticated$: Observable<Action> = createEffect(() => this.actions$.pipe(
public authenticated$: Observable<Action> = this.actions$.pipe(
ofType(AuthActionTypes.AUTHENTICATED), ofType(AuthActionTypes.AUTHENTICATED),
switchMap((action: AuthenticatedAction) => { switchMap((action: AuthenticatedAction) => {
return this.authService.authenticatedUser(action.payload).pipe( return this.authService.authenticatedUser(action.payload).pipe(
map((userHref: string) => new AuthenticatedSuccessAction((userHref !== null), action.payload, userHref)), map((userHref: string) => new AuthenticatedSuccessAction((userHref !== null), action.payload, userHref)),
catchError((error) => observableOf(new AuthenticatedErrorAction(error))),); catchError((error) => observableOf(new AuthenticatedErrorAction(error))),);
}) })
); ));
@Effect() public authenticatedSuccess$: Observable<Action> = createEffect(() => this.actions$.pipe(
public authenticatedSuccess$: Observable<Action> = this.actions$.pipe(
ofType(AuthActionTypes.AUTHENTICATED_SUCCESS), ofType(AuthActionTypes.AUTHENTICATED_SUCCESS),
tap((action: AuthenticatedSuccessAction) => this.authService.storeToken(action.payload.authToken)), tap((action: AuthenticatedSuccessAction) => this.authService.storeToken(action.payload.authToken)),
switchMap((action: AuthenticatedSuccessAction) => this.authService.getRedirectUrl().pipe( switchMap((action: AuthenticatedSuccessAction) => this.authService.getRedirectUrl().pipe(
@@ -110,26 +106,23 @@ export class AuthEffects {
return new RetrieveAuthenticatedEpersonAction(action.payload.userHref); return new RetrieveAuthenticatedEpersonAction(action.payload.userHref);
} }
}) })
); ));
@Effect({ dispatch: false }) public redirectAfterLoginSuccess$: Observable<Action> = createEffect(() => this.actions$.pipe(
public redirectAfterLoginSuccess$: Observable<Action> = this.actions$.pipe(
ofType(AuthActionTypes.REDIRECT_AFTER_LOGIN_SUCCESS), ofType(AuthActionTypes.REDIRECT_AFTER_LOGIN_SUCCESS),
tap((action: RedirectAfterLoginSuccessAction) => { tap((action: RedirectAfterLoginSuccessAction) => {
this.authService.clearRedirectUrl(); this.authService.clearRedirectUrl();
this.authService.navigateToRedirectUrl(action.payload); this.authService.navigateToRedirectUrl(action.payload);
}) })
); ), { dispatch: false });
// It means "reacts to this action but don't send another" // It means "reacts to this action but don't send another"
@Effect({ dispatch: false }) public authenticatedError$: Observable<Action> = createEffect(() => this.actions$.pipe(
public authenticatedError$: Observable<Action> = this.actions$.pipe(
ofType(AuthActionTypes.AUTHENTICATED_ERROR), ofType(AuthActionTypes.AUTHENTICATED_ERROR),
tap((action: LogOutSuccessAction) => this.authService.removeToken()) tap((action: LogOutSuccessAction) => this.authService.removeToken())
); ), { dispatch: false });
@Effect() public retrieveAuthenticatedEperson$: Observable<Action> = createEffect(() => this.actions$.pipe(
public retrieveAuthenticatedEperson$: Observable<Action> = this.actions$.pipe(
ofType(AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON), ofType(AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON),
switchMap((action: RetrieveAuthenticatedEpersonAction) => { switchMap((action: RetrieveAuthenticatedEpersonAction) => {
const impersonatedUserID = this.authService.getImpersonateID(); const impersonatedUserID = this.authService.getImpersonateID();
@@ -143,20 +136,18 @@ export class AuthEffects {
map((user: EPerson) => new RetrieveAuthenticatedEpersonSuccessAction(user.id)), map((user: EPerson) => new RetrieveAuthenticatedEpersonSuccessAction(user.id)),
catchError((error) => observableOf(new RetrieveAuthenticatedEpersonErrorAction(error)))); catchError((error) => observableOf(new RetrieveAuthenticatedEpersonErrorAction(error))));
}) })
); ));
@Effect() public checkToken$: Observable<Action> = createEffect(() => this.actions$.pipe(ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN),
public checkToken$: Observable<Action> = this.actions$.pipe(ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN),
switchMap(() => { switchMap(() => {
return this.authService.hasValidAuthenticationToken().pipe( return this.authService.hasValidAuthenticationToken().pipe(
map((token: AuthTokenInfo) => new AuthenticatedAction(token)), map((token: AuthTokenInfo) => new AuthenticatedAction(token)),
catchError((error) => observableOf(new CheckAuthenticationTokenCookieAction())) catchError((error) => observableOf(new CheckAuthenticationTokenCookieAction()))
); );
}) })
); ));
@Effect() public checkTokenCookie$: Observable<Action> = createEffect(() => this.actions$.pipe(
public checkTokenCookie$: Observable<Action> = this.actions$.pipe(
ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE), ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE),
switchMap(() => { switchMap(() => {
return this.authService.checkAuthenticationCookie().pipe( return this.authService.checkAuthenticationCookie().pipe(
@@ -171,10 +162,9 @@ export class AuthEffects {
catchError((error) => observableOf(new AuthenticatedErrorAction(error))) catchError((error) => observableOf(new AuthenticatedErrorAction(error)))
); );
}) })
); ));
@Effect() public retrieveToken$: Observable<Action> = createEffect(() => this.actions$.pipe(
public retrieveToken$: Observable<Action> = this.actions$.pipe(
ofType(AuthActionTypes.RETRIEVE_TOKEN), ofType(AuthActionTypes.RETRIEVE_TOKEN),
switchMap((action: AuthenticateAction) => { switchMap((action: AuthenticateAction) => {
return this.authService.refreshAuthenticationToken(null).pipe( return this.authService.refreshAuthenticationToken(null).pipe(
@@ -183,55 +173,51 @@ export class AuthEffects {
catchError((error) => observableOf(new AuthenticationErrorAction(error))) catchError((error) => observableOf(new AuthenticationErrorAction(error)))
); );
}) })
); ));
@Effect() public refreshToken$: Observable<Action> = createEffect(() => this.actions$.pipe(ofType(AuthActionTypes.REFRESH_TOKEN),
public refreshToken$: Observable<Action> = this.actions$.pipe(ofType(AuthActionTypes.REFRESH_TOKEN),
switchMap((action: RefreshTokenAction) => { switchMap((action: RefreshTokenAction) => {
return this.authService.refreshAuthenticationToken(action.payload).pipe( return this.authService.refreshAuthenticationToken(action.payload).pipe(
map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token)), map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token)),
catchError((error) => observableOf(new RefreshTokenErrorAction())) catchError((error) => observableOf(new RefreshTokenErrorAction()))
); );
}) })
); ));
// It means "reacts to this action but don't send another" // It means "reacts to this action but don't send another"
@Effect({ dispatch: false }) public refreshTokenSuccess$: Observable<Action> = createEffect(() => this.actions$.pipe(
public refreshTokenSuccess$: Observable<Action> = this.actions$.pipe(
ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS), ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS),
tap((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload)) tap((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload))
); ), { dispatch: false });
/** /**
* When the store is rehydrated in the browser, * When the store is rehydrated in the browser,
* clear a possible invalid token or authentication errors * clear a possible invalid token or authentication errors
*/ */
@Effect({ dispatch: false }) public clearInvalidTokenOnRehydrate$: Observable<any> = createEffect(() => this.actions$.pipe(
public clearInvalidTokenOnRehydrate$: Observable<any> = this.actions$.pipe(
ofType(StoreActionTypes.REHYDRATE), ofType(StoreActionTypes.REHYDRATE),
switchMap(() => { switchMap(() => {
const isLoaded$ = this.store.pipe(select(isAuthenticatedLoaded)); const isLoaded$ = this.store.pipe(select(isAuthenticatedLoaded));
const authenticated$ = this.store.pipe(select(isAuthenticated)); const authenticated$ = this.store.pipe(select(isAuthenticated));
return observableCombineLatest(isLoaded$, authenticated$).pipe( return observableCombineLatest([isLoaded$, authenticated$]).pipe(
take(1), take(1),
filter(([loaded, authenticated]) => loaded && !authenticated), filter(([loaded, authenticated]) => loaded && !authenticated),
tap(() => this.authService.removeToken()), tap(() => this.authService.removeToken()),
tap(() => this.authService.resetAuthenticationError()) tap(() => this.authService.resetAuthenticationError())
); );
})); })), { dispatch: false });
/** /**
* When the store is rehydrated in the browser, invalidate all cache hits regarding the * When the store is rehydrated in the browser, invalidate all cache hits regarding the
* authorizations endpoint, to be sure to have consistent responses after a login with external idp * authorizations endpoint, to be sure to have consistent responses after a login with external idp
* *
*/ */
@Effect({ dispatch: false }) invalidateAuthorizationsRequestCache$ = this.actions$ invalidateAuthorizationsRequestCache$ = createEffect(() => this.actions$
.pipe(ofType(StoreActionTypes.REHYDRATE), .pipe(ofType(StoreActionTypes.REHYDRATE),
tap(() => this.authorizationsService.invalidateAuthorizationsRequestCache()) tap(() => this.authorizationsService.invalidateAuthorizationsRequestCache())
); ), { dispatch: false });
@Effect() public logOut$: Observable<Action> = createEffect(() => this.actions$
public logOut$: Observable<Action> = this.actions$
.pipe( .pipe(
ofType(AuthActionTypes.LOG_OUT), ofType(AuthActionTypes.LOG_OUT),
switchMap(() => { switchMap(() => {
@@ -241,26 +227,23 @@ export class AuthEffects {
catchError((error) => observableOf(new LogOutErrorAction(error))) catchError((error) => observableOf(new LogOutErrorAction(error)))
); );
}) })
); ));
@Effect({ dispatch: false }) public logOutSuccess$: Observable<Action> = createEffect(() => this.actions$
public logOutSuccess$: Observable<Action> = this.actions$
.pipe(ofType(AuthActionTypes.LOG_OUT_SUCCESS), .pipe(ofType(AuthActionTypes.LOG_OUT_SUCCESS),
tap(() => this.authService.removeToken()), tap(() => this.authService.removeToken()),
tap(() => this.authService.clearRedirectUrl()), tap(() => this.authService.clearRedirectUrl()),
tap(() => this.authService.refreshAfterLogout()) tap(() => this.authService.refreshAfterLogout())
); ), { dispatch: false });
@Effect({ dispatch: false }) public redirectToLoginTokenExpired$: Observable<Action> = createEffect(() => this.actions$
public redirectToLoginTokenExpired$: Observable<Action> = this.actions$
.pipe( .pipe(
ofType(AuthActionTypes.REDIRECT_TOKEN_EXPIRED), ofType(AuthActionTypes.REDIRECT_TOKEN_EXPIRED),
tap(() => this.authService.removeToken()), tap(() => this.authService.removeToken()),
tap(() => this.authService.redirectToLoginWhenTokenExpired()) tap(() => this.authService.redirectToLoginWhenTokenExpired())
); ), { dispatch: false });
@Effect() public retrieveMethods$: Observable<Action> = createEffect(() => this.actions$
public retrieveMethods$: Observable<Action> = this.actions$
.pipe( .pipe(
ofType(AuthActionTypes.RETRIEVE_AUTH_METHODS), ofType(AuthActionTypes.RETRIEVE_AUTH_METHODS),
switchMap((action: RetrieveAuthMethodsAction) => { switchMap((action: RetrieveAuthMethodsAction) => {
@@ -270,7 +253,7 @@ export class AuthEffects {
catchError((error) => observableOf(new RetrieveAuthMethodsErrorAction())) catchError((error) => observableOf(new RetrieveAuthMethodsErrorAction()))
); );
}) })
); ));
/** /**
* For any action that is not in {@link IDLE_TIMER_IGNORE_TYPES} that comes in => Start the idleness timer * For any action that is not in {@link IDLE_TIMER_IGNORE_TYPES} that comes in => Start the idleness timer
@@ -278,8 +261,7 @@ export class AuthEffects {
* => Return the action to set the user as idle ({@link SetUserAsIdleAction}) * => Return the action to set the user as idle ({@link SetUserAsIdleAction})
* @method trackIdleness * @method trackIdleness
*/ */
@Effect() public trackIdleness$: Observable<Action> = createEffect(() => this.actions$.pipe(
public trackIdleness$: Observable<Action> = this.actions$.pipe(
filter((action: Action) => !IDLE_TIMER_IGNORE_TYPES.includes(action.type)), filter((action: Action) => !IDLE_TIMER_IGNORE_TYPES.includes(action.type)),
// Using switchMap the effect will stop subscribing to the previous timer if a new action comes // Using switchMap the effect will stop subscribing to the previous timer if a new action comes
// in, and start a new timer // in, and start a new timer
@@ -290,7 +272,7 @@ export class AuthEffects {
// Re-enter the zone to dispatch the action // Re-enter the zone to dispatch the action
observeOn(new EnterZoneScheduler(this.zone, queueScheduler)), observeOn(new EnterZoneScheduler(this.zone, queueScheduler)),
map(() => new SetUserAsIdleAction()), map(() => new SetUserAsIdleAction()),
); ));
/** /**
* @constructor * @constructor

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