Merge branch 'main' into feature-relationship-versioning

This commit is contained in:
Bruno Roemers
2022-04-28 15:05:19 +02:00
739 changed files with 32842 additions and 26732 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"
}
}
]
}

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

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

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/.angular/cache
/__build__ /__build__
/__server_build__ /__server_build__
/node_modules /node_modules

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

@@ -101,7 +101,7 @@ Installing
### Configuring ### Configuring
Default configuration file is located in `config/` folder. Default runtime configuration file is located in `config/` folder. These configurations can be changed without rebuilding the distribution.
To override the default configuration values, create local files that override the parameters you need to change. You can use `config.example.yml` as a starting point. To override the default configuration values, create local files that override the parameters you need to change. You can use `config.example.yml` as a starting point.
@@ -167,6 +167,22 @@ These configuration sources are collected **at run time**, and written to `dist/
The configuration file can be externalized by using environment variable `DSPACE_APP_CONFIG_PATH`. The configuration file can be externalized by using environment variable `DSPACE_APP_CONFIG_PATH`.
#### Buildtime Configuring
Buildtime configuration must defined before build in order to include in transpiled JavaScript. This is primarily for the server. These settings can be found under `src/environment/` folder.
To override the default configuration values for development, create local file that override the build time parameters you need to change.
- Create a new `environment.(dev or development).ts` file in `src/environment/` for a `development` environment;
If needing to update default configurations values for production, update local file that override the build time parameters you need to change.
- Update `environment.production.ts` file in `src/environment/` for a `production` environment;
The environment object is provided for use as import in code and is extended with he runtime configuration on bootstrap of the application.
> Take caution moving runtime configs into the buildtime configuration. They will be overwritten by what is defined in the runtime config on bootstrap.
#### Using environment variables in code #### Using environment variables in code
To use environment variables in a UI component, use: To use environment variables in a UI component, use:
@@ -183,7 +199,6 @@ or
import { environment } from '../environment.ts'; import { environment } from '../environment.ts';
``` ```
Running the app Running the app
--------------- ---------------
@@ -193,7 +208,7 @@ After you have installed all dependencies you can now run the app. Run `yarn run
When building for production we're using Ahead of Time (AoT) compilation. With AoT, the browser downloads a pre-compiled version of the application, so it can render the application immediately, without waiting to compile the app first. The compiler is roughly half the size of Angular itself, so omitting it dramatically reduces the application payload. When building for production we're using Ahead of Time (AoT) compilation. With AoT, the browser downloads a pre-compiled version of the application, so it can render the application immediately, without waiting to compile the app first. The compiler is roughly half the size of Angular itself, so omitting it dramatically reduces the application payload.
To build the app for production and start the server run: To build the app for production and start the server (in one command) run:
```bash ```bash
yarn start yarn start
@@ -207,6 +222,10 @@ yarn run build:prod
``` ```
This will build the application and put the result in the `dist` folder. You can copy this folder to wherever you need it for your application server. If you will be using the built-in Express server, you'll also need a copy of the `node_modules` folder tucked inside your copy of `dist`. This will build the application and put the result in the `dist` folder. You can copy this folder to wherever you need it for your application server. If you will be using the built-in Express server, you'll also need a copy of the `node_modules` folder tucked inside your copy of `dist`.
After building the app for production, it can be started by running:
```bash
yarn run serve:ssr
```
### Running the application with Docker ### Running the application with Docker
NOTE: At this time, we do not have production-ready Docker images for DSpace. NOTE: At this time, we do not have production-ready Docker images for DSpace.
@@ -268,11 +287,29 @@ E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Con
The test files can be found in the `./cypress/integration/` folder. The test files can be found in the `./cypress/integration/` folder.
Before you can run e2e tests, two things are required: Before you can run e2e tests, two things are REQUIRED:
1. You MUST have a running backend (i.e. REST API). By default, the e2e tests look for this at http://localhost:8080/server/ or whatever `rest` backend is defined in your `config.prod.yml` or `config.yml`. You may override this using env variables, see [Configuring](#configuring). 1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo REST API (https://api7.dspace.org/server/), as that server is uncontrolled and may have content added/removed at any time.
2. Your backend MUST include our Entities Test Data set. Some tests run against a (currently hardcoded) Community/Collection/Item UUID. These UUIDs are all valid for our Entities Test Data set. The Entities Test Data set may be installed easily via Docker, see https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker-compose#ingest-option-2-ingest-entities-test-data * After starting up your backend on localhost, make sure either your `config.prod.yml` or `config.dev.yml` has its `rest` settings defined to use that localhost backend.
* If you'd prefer, you may instead use environment variables as described at [Configuring](#configuring). For example:
```
DSPACE_REST_SSL = false
DSPACE_REST_HOST = localhost
DSPACE_REST_PORT = 8080
```
2. Your backend MUST include our [Entities Test Data set](https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data). Some tests run against a specific Community/Collection/Item UUID. These UUIDs are all valid for our Entities Test Data set.
* (Recommended) The Entities Test Data set may be installed easily via Docker, see https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker-compose#ingest-option-2-ingest-entities-test-data
* Alternatively, the Entities Test Data set may be installed via a simple SQL import (e. g. `psql -U dspace < dspace7-entities-data.sql`). See instructions in link above.
Run `ng e2e` to kick off the tests. This will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results. After performing the above setup, you can run the e2e tests using
```
ng e2e
````
NOTE: By default these tests will run against the REST API backend configured via environment variables or in `config.prod.yml`. If you'd rather it use `config.dev.yml`, just set the NODE_ENV environment variable like this:
```
NODE_ENV=development ng e2e
```
The `ng e2e` command will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results.
#### Writing E2E Tests #### Writing 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:
@@ -228,6 +234,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

@@ -6,5 +6,20 @@
"pluginsFile": "cypress/plugins/index.ts", "pluginsFile": "cypress/plugins/index.ts",
"fixturesFolder": "cypress/fixtures", "fixturesFolder": "cypress/fixtures",
"baseUrl": "http://localhost:4000", "baseUrl": "http://localhost:4000",
"retries": 2 "retries": {
"runMode": 2,
"openMode": 0
},
"env": {
"DSPACE_TEST_ADMIN_USER": "dspacedemo+admin@gmail.com",
"DSPACE_TEST_ADMIN_PASSWORD": "dspace",
"DSPACE_TEST_COMMUNITY": "0958c910-2037-42a9-81c7-dca80e3892b4",
"DSPACE_TEST_COLLECTION": "282164f5-d325-4740-8dd1-fa4d6d3e7200",
"DSPACE_TEST_ENTITY_PUBLICATION": "e98b0f27-5c19-49a0-960d-eb6ad5287067",
"DSPACE_TEST_SEARCH_TERM": "test",
"DSPACE_TEST_SUBMIT_COLLECTION_NAME": "Sample Collection",
"DSPACE_TEST_SUBMIT_COLLECTION_UUID": "9d8334e9-25d3-4a67-9cea-3dffdef80144",
"DSPACE_TEST_SUBMIT_USER": "dspacedemo+submit@gmail.com",
"DSPACE_TEST_SUBMIT_USER_PASSWORD": "dspace"
}
} }

View File

@@ -16,8 +16,8 @@ describe('Homepage', () => {
it('should have a working search box', () => { it('should have a working search box', () => {
const queryString = 'test'; const queryString = 'test';
cy.get('ds-search-form input[name="query"]').type(queryString); cy.get('[data-test="search-box"]').type(queryString);
cy.get('ds-search-form button.search-button').click(); cy.get('[data-test="search-button"]').click();
cy.url().should('include', '/search'); cy.url().should('include', '/search');
cy.url().should('include', 'query=' + encodeURI(queryString)); cy.url().should('include', 'query=' + encodeURI(queryString));
}); });

View File

@@ -0,0 +1,126 @@
import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support';
const page = {
openLoginMenu() {
// Click the "Log In" dropdown menu in header
cy.get('ds-themed-navbar [data-test="login-menu"]').click();
},
openUserMenu() {
// Once logged in, click the User menu in header
cy.get('ds-themed-navbar [data-test="user-menu"]').click();
},
submitLoginAndPasswordByPressingButton(email, password) {
// Enter email
cy.get('ds-themed-navbar [data-test="email"]').type(email);
// Enter password
cy.get('ds-themed-navbar [data-test="password"]').type(password);
// Click login button
cy.get('ds-themed-navbar [data-test="login-button"]').click();
},
submitLoginAndPasswordByPressingEnter(email, password) {
// In opened Login modal, fill out email & password, then click Enter
cy.get('ds-themed-navbar [data-test="email"]').type(email);
cy.get('ds-themed-navbar [data-test="password"]').type(password);
cy.get('ds-themed-navbar [data-test="password"]').type('{enter}');
},
submitLogoutByPressingButton() {
// This is the POST command that will actually log us out
cy.intercept('POST', '/server/api/authn/logout').as('logout');
// Click logout button
cy.get('ds-themed-navbar [data-test="logout-button"]').click();
// Wait until above POST command responds before continuing
// (This ensures next action waits until logout completes)
cy.wait('@logout');
}
};
describe('Login Modal', () => {
it('should login when clicking button & stay on same page', () => {
const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION;
cy.visit(ENTITYPAGE);
// Login menu should exist
cy.get('ds-log-in').should('exist');
// Login, and the <ds-log-in> tag should no longer exist
page.openLoginMenu();
cy.get('.form-login').should('be.visible');
page.submitLoginAndPasswordByPressingButton(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
cy.get('ds-log-in').should('not.exist');
// Verify we are still on the same page
cy.url().should('include', ENTITYPAGE);
// Open user menu, verify user menu & logout button now available
page.openUserMenu();
cy.get('ds-user-menu').should('be.visible');
cy.get('ds-log-out').should('be.visible');
});
it('should login when clicking enter key & stay on same page', () => {
cy.visit('/home');
// Open login menu in header & verify <ds-log-in> tag is visible
page.openLoginMenu();
cy.get('.form-login').should('be.visible');
// Login, and the <ds-log-in> tag should no longer exist
page.submitLoginAndPasswordByPressingEnter(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
cy.get('.form-login').should('not.exist');
// Verify we are still on homepage
cy.url().should('include', '/home');
// Open user menu, verify user menu & logout button now available
page.openUserMenu();
cy.get('ds-user-menu').should('be.visible');
cy.get('ds-log-out').should('be.visible');
});
it('should support logout', () => {
// First authenticate & access homepage
cy.login(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
cy.visit('/');
// Verify ds-log-in tag doesn't exist, but ds-log-out tag does exist
cy.get('ds-log-in').should('not.exist');
cy.get('ds-log-out').should('exist');
// Click logout button
page.openUserMenu();
page.submitLogoutByPressingButton();
// Verify ds-log-in tag now exists
cy.get('ds-log-in').should('exist');
cy.get('ds-log-out').should('not.exist');
});
it('should allow new user registration', () => {
cy.visit('/');
page.openLoginMenu();
// Registration link should be visible
cy.get('ds-themed-navbar [data-test="register"]').should('be.visible');
// Click registration link & you should go to registration page
cy.get('ds-themed-navbar [data-test="register"]').click();
cy.location('pathname').should('eq', '/register');
cy.get('ds-register-email').should('exist');
});
it('should allow forgot password', () => {
cy.visit('/');
page.openLoginMenu();
// Forgot password link should be visible
cy.get('ds-themed-navbar [data-test="forgot"]').should('be.visible');
// Click link & you should go to Forgot Password page
cy.get('ds-themed-navbar [data-test="forgot"]').click();
cy.location('pathname').should('eq', '/forgot');
cy.get('ds-forgot-email').should('exist');
});
});

View File

@@ -0,0 +1,149 @@
import { Options } from 'cypress-axe';
import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME } from 'cypress/support';
import { testA11y } from 'cypress/support/utils';
describe('My DSpace page', () => {
it('should display recent submissions and pass accessibility tests', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
cy.visit('/mydspace');
cy.get('ds-my-dspace-page').should('exist');
// At least one recent submission should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
// Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well)
cy.get('.filter-toggle').click({ multiple: true });
// Analyze <ds-my-dspace-page> for accessibility issues
testA11y(
{
include: ['ds-my-dspace-page'],
exclude: [
['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175
],
},
{
rules: {
// Search filters fail these two "moderate" impact rules
'heading-order': { enabled: false },
'landmark-unique': { enabled: false }
}
} as Options
);
});
it('should have a working detailed view that passes accessibility tests', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
cy.visit('/mydspace');
cy.get('ds-my-dspace-page').should('exist');
// Click button in sidebar to display detailed view
cy.get('ds-search-sidebar [data-test="detail-view"]').click();
cy.get('ds-object-detail').should('exist');
// Analyze <ds-search-page> for accessibility issues
testA11y('ds-my-dspace-page',
{
rules: {
// Search filters fail these two "moderate" impact rules
'heading-order': { enabled: false },
'landmark-unique': { enabled: false }
}
} as Options
);
});
// NOTE: Deleting existing submissions is exercised by submission.spec.ts
it('should let you start a new submission & edit in-progress submissions', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
cy.visit('/mydspace');
// Open the New Submission dropdown
cy.get('#dropdownSubmission').click();
// Click on the "Item" type in that dropdown
cy.get('#entityControlsDropdownMenu button[title="none"]').click();
// This should display the <ds-create-item-parent-selector> (popup window)
cy.get('ds-create-item-parent-selector').should('be.visible');
// Type in a known Collection name in the search box
cy.get('ds-authorized-collection-selector input[type="search"]').type(TEST_SUBMIT_COLLECTION_NAME);
// Click on the button matching that known Collection name
cy.get('ds-authorized-collection-selector button[title="' + TEST_SUBMIT_COLLECTION_NAME + '"]').click();
// New URL should include /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
// The Submission edit form tag should be visible
cy.get('ds-submission-edit').should('be.visible');
// A Collection menu button should exist & its value should be the selected collection
cy.get('#collectionControlsMenuButton span').should('have.text', TEST_SUBMIT_COLLECTION_NAME);
// Now that we've created a submission, we'll test that we can go back and Edit it.
// Get our Submission URL, to parse out the ID of this new submission
cy.location().then(fullUrl => {
// This will be the full path (/workspaceitems/[id]/edit)
const path = fullUrl.pathname;
// Split on the slashes
const subpaths = path.split('/');
// Part 2 will be the [id] of the submission
const id = subpaths[2];
// Click the "Save for Later" button to save this submission
cy.get('button#saveForLater').click();
// "Save for Later" should send us to MyDSpace
cy.url().should('include', '/mydspace');
// Close any open notifications, to make sure they don't get in the way of next steps
cy.get('[data-dismiss="alert"]').click({multiple: true});
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// On MyDSpace, find the submission we just created via its ID
cy.get('[data-test="search-box"]').type(id);
cy.get('[data-test="search-button"]').click();
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// Click the Edit button for this in-progress submission
cy.get('#edit_' + id).click();
// Should send us back to the submission form
cy.url().should('include', '/workspaceitems/' + id + '/edit');
// Discard our new submission by clicking Discard in Submission form & confirming
cy.get('button#discard').click();
cy.get('button#discard_submit').click();
// Discarding should send us back to MyDSpace
cy.url().should('include', '/mydspace');
});
});
it('should let you import from external sources', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
cy.visit('/mydspace');
// Open the New Import dropdown
cy.get('#dropdownImport').click();
// Click on the "Item" type in that dropdown
cy.get('#importControlsDropdownMenu button[title="none"]').click();
// New URL should include /import-external, as we've moved to the import page
cy.url().should('include', '/import-external');
// The external import searchbox should be visible
cy.get('ds-submission-import-external-searchbar').should('be.visible');
});
});

View File

@@ -1,49 +1,66 @@
import { TEST_SEARCH_TERM } from 'cypress/support';
const page = { const page = {
fillOutQueryInNavBar(query) { fillOutQueryInNavBar(query) {
// Click the magnifying glass // Click the magnifying glass
cy.get('.navbar-container #search-navbar-container form a').click(); cy.get('ds-themed-navbar [data-test="header-search-icon"]').click();
// Fill out a query in input that appears // Fill out a query in input that appears
cy.get('.navbar-container #search-navbar-container form input[name = "query"]').type(query); cy.get('ds-themed-navbar [data-test="header-search-box"]').type(query);
}, },
submitQueryByPressingEnter() { submitQueryByPressingEnter() {
cy.get('.navbar-container #search-navbar-container form input[name = "query"]').type('{enter}'); cy.get('ds-themed-navbar [data-test="header-search-box"]').type('{enter}');
}, },
submitQueryByPressingIcon() { submitQueryByPressingIcon() {
cy.get('.navbar-container #search-navbar-container form .submit-icon').click(); cy.get('ds-themed-navbar [data-test="header-search-icon"]').click();
} }
}; };
describe('Search from Navigation Bar', () => { describe('Search from Navigation Bar', () => {
// NOTE: these tests currently assume this query will return results! // NOTE: these tests currently assume this query will return results!
const query = 'test'; const query = TEST_SEARCH_TERM;
it('should go to search page with correct query if submitted (from home)', () => { it('should go to search page with correct query if submitted (from home)', () => {
cy.visit('/'); cy.visit('/');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query); page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter(); page.submitQueryByPressingEnter();
// New URL should include query param // New URL should include query param
cy.url().should('include', 'query=' + query); cy.url().should('include', 'query=' + query);
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed // At least one search result should be displayed
cy.get('ds-item-search-result-list-element').should('be.visible'); cy.get('[data-test="list-object"]').should('be.visible');
}); });
it('should go to search page with correct query if submitted (from search)', () => { it('should go to search page with correct query if submitted (from search)', () => {
cy.visit('/search'); cy.visit('/search');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query); page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter(); page.submitQueryByPressingEnter();
// New URL should include query param // New URL should include query param
cy.url().should('include', 'query=' + query); cy.url().should('include', 'query=' + query);
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed // At least one search result should be displayed
cy.get('ds-item-search-result-list-element').should('be.visible'); cy.get('[data-test="list-object"]').should('be.visible');
}); });
it('should allow user to also submit query by clicking icon', () => { it('should allow user to also submit query by clicking icon', () => {
cy.visit('/'); cy.visit('/');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query); page.fillOutQueryInNavBar(query);
page.submitQueryByPressingIcon(); page.submitQueryByPressingIcon();
// New URL should include query param // New URL should include query param
cy.url().should('include', 'query=' + query); cy.url().should('include', 'query=' + query);
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed // At least one search result should be displayed
cy.get('ds-item-search-result-list-element').should('be.visible'); cy.get('[data-test="list-object"]').should('be.visible');
}); });
}); });

View File

@@ -1,31 +1,27 @@
import { Options } from 'cypress-axe'; import { Options } from 'cypress-axe';
import { TEST_SEARCH_TERM } from 'cypress/support';
import { testA11y } from 'cypress/support/utils'; import { testA11y } from 'cypress/support/utils';
describe('Search Page', () => { describe('Search Page', () => {
// unique ID of the search form (for selecting specific elements below)
const SEARCHFORM_ID = '#search-form';
it('should contain query value when navigating to page with query parameter', () => {
const queryString = 'test query';
cy.visit('/search?query=' + queryString);
cy.get(SEARCHFORM_ID + ' input[name="query"]').should('have.value', queryString);
});
it('should redirect to the correct url when query was set and submit button was triggered', () => { it('should redirect to the correct url when query was set and submit button was triggered', () => {
const queryString = 'Another interesting query string'; const queryString = 'Another interesting query string';
cy.visit('/search'); cy.visit('/search');
// Type query in searchbox & click search button // Type query in searchbox & click search button
cy.get(SEARCHFORM_ID + ' input[name="query"]').type(queryString); cy.get('[data-test="search-box"]').type(queryString);
cy.get(SEARCHFORM_ID + ' button.search-button').click(); cy.get('[data-test="search-button"]').click();
cy.url().should('include', 'query=' + encodeURI(queryString)); cy.url().should('include', 'query=' + encodeURI(queryString));
}); });
it('should pass accessibility tests', () => { it('should load results and pass accessibility tests', () => {
cy.visit('/search'); cy.visit('/search?query=' + TEST_SEARCH_TERM);
cy.get('[data-test="search-box"]').should('have.value', TEST_SEARCH_TERM);
// <ds-search-page> tag must be loaded // <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('exist'); cy.get('ds-search-page').should('exist');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
// 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('.filter-toggle').click({ multiple: true });
@@ -48,16 +44,18 @@ describe('Search Page', () => {
); );
}); });
it('should pass accessibility tests in Grid view', () => { it('should have a working grid view that passes accessibility tests', () => {
cy.visit('/search'); cy.visit('/search?query=' + TEST_SEARCH_TERM);
// Click to display grid view // Click button in sidebar to display grid view
// TODO: These buttons should likely have an easier way to uniquely select cy.get('ds-search-sidebar [data-test="grid-view"]').click();
cy.get('#search-sidebar-content > ds-view-mode-switch > .btn-group > [href="/search?view=grid"] > .fas').click();
// <ds-search-page> tag must be loaded // <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('exist'); cy.get('ds-search-page').should('exist');
// At least one grid object (card) should be displayed
cy.get('[data-test="grid-object"]').should('be.visible');
// Analyze <ds-search-page> for accessibility issues // Analyze <ds-search-page> for accessibility issues
testA11y('ds-search-page', testA11y('ds-search-page',
{ {

View File

@@ -0,0 +1,135 @@
import { Options } from 'cypress-axe';
import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID } from 'cypress/support';
import { testA11y } from 'cypress/support/utils';
describe('New Submission page', () => {
// NOTE: We already test that new submissions can be started from MyDSpace in my-dspace.spec.ts
it('should create a new submission when using /submit path & pass accessibility', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
// Test that calling /submit with collection & entityType will create a new submission
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
// Should redirect to /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
// The Submission edit form tag should be visible
cy.get('ds-submission-edit').should('be.visible');
// A Collection menu button should exist & it's value should be the selected collection
cy.get('#collectionControlsMenuButton span').should('have.text', TEST_SUBMIT_COLLECTION_NAME);
// 4 sections should be visible by default
cy.get('div#section_traditionalpageone').should('be.visible');
cy.get('div#section_traditionalpagetwo').should('be.visible');
cy.get('div#section_upload').should('be.visible');
cy.get('div#section_license').should('be.visible');
// Discard button should work
// Clicking it will display a confirmation, which we will confirm with another click
cy.get('button#discard').click();
cy.get('button#discard_submit').click();
});
it('should block submission & show errors if required fields are missing', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
// Create a new submission
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
// Attempt an immediate deposit without filling out any fields
cy.get('button#deposit').click();
// 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');
// First section should have an exclamation error in the header
// (as it has required fields)
cy.get('div#traditionalpageone-header i.fa-exclamation-circle').should('be.visible');
// Title field should have class "is-invalid" applied, as it's required
cy.get('input#dc_title').should('have.class', 'is-invalid');
// Date Year field should also have "is-valid" class
cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid');
// FINALLY, cleanup after ourselves. This also exercises the MyDSpace delete button.
// Get our Submission URL, to parse out the ID of this submission
cy.location().then(fullUrl => {
// This will be the full path (/workspaceitems/[id]/edit)
const path = fullUrl.pathname;
// Split on the slashes
const subpaths = path.split('/');
// Part 2 will be the [id] of the submission
const id = subpaths[2];
// Even though form is incomplete, the "Save for Later" button should still work
cy.get('button#saveForLater').click();
// "Save for Later" should send us to MyDSpace
cy.url().should('include', '/mydspace');
// A success alert should be visible
cy.get('ds-notification div.alert-success').should('be.visible');
// Now, dismiss any open alert boxes (may be multiple, as tests run quickly)
cy.get('[data-dismiss="alert"]').click({multiple: true});
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// On MyDSpace, find the submission we just saved via its ID
cy.get('[data-test="search-box"]').type(id);
cy.get('[data-test="search-button"]').click();
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// Delete our created submission & confirm deletion
cy.get('button#delete_' + id).click();
cy.get('button#delete_confirm').click();
});
});
it('should allow for deposit if all required fields completed & file uploaded', () => {
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
// Create a new submission
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
// Fill out all required fields (Title, Date)
cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests');
cy.get('input#dc_date_issued_year').type('2022');
// Confirm the required license by checking checkbox
// (NOTE: requires "force:true" cause Cypress claims this checkbox is covered by its own <span>)
cy.get('input#granted').check( {force: true} );
// Before using Cypress drag & drop, we have to manually trigger the "dragover" event.
// This ensures our UI displays the dropzone that covers the entire submission page.
// (For some reason Cypress drag & drop doesn't trigger this even itself & upload won't work without this trigger)
cy.get('ds-uploader').trigger('dragover');
// This is the POST command that will upload the file
cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload');
// Upload our DSpace logo via drag & drop onto submission form
// cy.get('div#section_upload')
cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.png', {
action: 'drag-drop'
});
// Wait for upload to complete before proceeding
cy.wait('@upload');
// Close the upload success notice
cy.get('[data-dismiss="alert"]').click({multiple: true});
// Wait for deposit button to not be disabled & click it.
cy.get('button#deposit').should('not.be.disabled').click();
// No warnings should exist. Instead, just successful deposit alert is displayed
cy.get('ds-notification div.alert-warning').should('not.exist');
cy.get('ds-notification div.alert-success').should('be.visible');
});
});

View File

@@ -1,15 +1,34 @@
const fs = require('fs');
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress // Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
// For more info, visit https://on.cypress.io/plugins-api // For more info, visit https://on.cypress.io/plugins-api
module.exports = (on, config) => { module.exports = (on, config) => {
// Define "log" and "table" tasks, used for logging accessibility errors during CI
// Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
on('task', { on('task', {
// Define "log" and "table" tasks, used for logging accessibility errors during CI
// Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
log(message: string) { log(message: string) {
console.log(message); console.log(message);
return null; return null;
}, },
table(message: string) { table(message: string) {
console.table(message); console.table(message);
return null;
},
// Cypress doesn't have access to the running application in Node.js.
// So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
// Instead, we'll read our running application's config.json, which contains the configs &
// is regenerated at runtime each time the Angular UI application starts up.
readUIConfig() {
// Check if we have a config.json in the src/assets. If so, use that.
// This is where it's written when running "ng e2e" or "yarn serve"
if (fs.existsSync('./src/assets/config.json')) {
return fs.readFileSync('./src/assets/config.json', 'utf8');
// Otherwise, check the dist/browser/assets
// This is where it's written when running "serve:ssr", which is what CI uses to start the frontend
} else if (fs.existsSync('./dist/browser/assets/config.json')) {
return fs.readFileSync('./dist/browser/assets/config.json', 'utf8');
}
return null; return null;
} }
}); });

View File

@@ -1,43 +1,83 @@
// *********************************************** // ***********************************************
// This example namespace declaration will help // This File is for Custom Cypress commands.
// with Intellisense and code completion in your // See docs at https://docs.cypress.io/api/cypress-api/custom-commands
// IDE or Text Editor.
// *********************************************** // ***********************************************
// declare namespace Cypress {
// interface Chainable<Subject = any> { import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
// customCommand(param: any): typeof customCommand; import { FALLBACK_TEST_REST_BASE_URL } from '.';
// }
// } // Declare Cypress namespace to help with Intellisense & code completion in IDEs
// // ALL custom commands MUST be listed here for code completion to work
// function customCommand(param: any): void { // tslint:disable-next-line:no-namespace
// console.warn(param); declare global {
// } namespace Cypress {
// interface Chainable<Subject = any> {
// NOTE: You can use it like so: /**
// Cypress.Commands.add('customCommand', customCommand); * Login to backend before accessing the next page. Ensures that the next
// * call to "cy.visit()" will be authenticated as this user.
// *********************************************** * @param email email to login as
// This example commands.js shows you how to * @param password password to login as
// create various custom commands and overwrite */
// existing commands. login(email: string, password: string): typeof login;
// }
// For more comprehensive examples of custom }
// commands please read more here: }
// https://on.cypress.io/custom-commands
// *********************************************** /**
// * Login user via REST API directly, and pass authentication token to UI via
// * the UI's dsAuthInfo cookie.
// -- This is a parent command -- * @param email email to login as
// Cypress.Commands.add("login", (email, password) => { ... }) * @param password password to login as
// */
// function login(email: string, password: string): void {
// -- This is a child command -- // Cypress doesn't have access to the running application in Node.js.
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) // So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
// // Instead, we'll read our running application's config.json, which contains the configs &
// // is regenerated at runtime each time the Angular UI application starts up.
// -- This is a dual command -- cy.task('readUIConfig').then((str: string) => {
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) // Parse config into a JSON object
// const config = JSON.parse(str);
//
// -- This will overwrite an existing command -- // Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found.
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) let baseRestUrl = FALLBACK_TEST_REST_BASE_URL;
if (!config.rest.baseUrl) {
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
} else {
console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: " + config.rest.baseUrl);
baseRestUrl = config.rest.baseUrl;
}
// To login via REST, first we have to do a GET to obtain a valid CSRF token
cy.request( baseRestUrl + '/api/authn/status' )
.then((response) => {
// We should receive a CSRF token returned in a response header
expect(response.headers).to.have.property('dspace-xsrf-token');
const csrfToken = response.headers['dspace-xsrf-token'];
// Now, send login POST request including that CSRF token
cy.request({
method: 'POST',
url: baseRestUrl + '/api/authn/login',
headers: { 'X-XSRF-TOKEN' : csrfToken},
form: true, // indicates the body should be form urlencoded
body: { user: email, password: password }
}).then((resp) => {
// We expect a successful login
expect(resp.status).to.eq(200);
// We expect to have a valid authorization header returned (with our auth token)
expect(resp.headers).to.have.property('authorization');
// Initialize our AuthTokenInfo object from the authorization header.
const authheader = resp.headers.authorization as string;
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
// Save our AuthTokenInfo object to our dsAuthInfo UI cookie
// This ensures the UI will recognize we are logged in on next "visit()"
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
});
});
});
}
// Add as a Cypress command (i.e. assign to 'cy.login')
Cypress.Commands.add('login', login);

View File

@@ -13,14 +13,51 @@
// https://on.cypress.io/configuration // https://on.cypress.io/configuration
// *********************************************************** // ***********************************************************
// When a command from ./commands is ready to use, import with `import './commands'` syntax // Import all custom Commands (from commands.ts) for all tests
// import './commands'; import './commands';
// Import Cypress Axe tools for all tests // Import Cypress Axe tools for all tests
// https://github.com/component-driven/cypress-axe // https://github.com/component-driven/cypress-axe
import 'cypress-axe'; import 'cypress-axe';
// Runs once before the first test in each "block"
before(() => {
// 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.
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true}');
});
// For better stability between tests, we visit "about:blank" (i.e. blank page) after each test.
// This ensures any remaining/outstanding XHR requests are killed, so they don't affect the next test.
// Borrowed from: https://glebbahmutov.com/blog/visit-blank-page-between-tests/
afterEach(() => {
cy.window().then((win) => {
win.location.href = 'about:blank';
});
});
// Global constants used in tests // Global constants used in tests
export const TEST_COLLECTION = '282164f5-d325-4740-8dd1-fa4d6d3e7200'; // May be overridden in our cypress.json config file using specified environment variables.
export const TEST_COMMUNITY = '0958c910-2037-42a9-81c7-dca80e3892b4'; // Default values listed here are all valid for the Demo Entities Data set available at
export const TEST_ENTITY_PUBLICATION = 'e98b0f27-5c19-49a0-960d-eb6ad5287067'; // https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
// (This is the data set used in our CI environment)
// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL
// from the Angular UI's config.json. See 'getBaseRESTUrl()' in commands.ts
export const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server';
// Admin account used for administrative tests
export const TEST_ADMIN_USER = Cypress.env('DSPACE_TEST_ADMIN_USER') || 'dspacedemo+admin@gmail.com';
export const TEST_ADMIN_PASSWORD = Cypress.env('DSPACE_TEST_ADMIN_PASSWORD') || 'dspace';
// Community/collection/publication used for view/edit tests
export const TEST_COLLECTION = Cypress.env('DSPACE_TEST_COLLECTION') || '282164f5-d325-4740-8dd1-fa4d6d3e7200';
export const TEST_COMMUNITY = Cypress.env('DSPACE_TEST_COMMUNITY') || '0958c910-2037-42a9-81c7-dca80e3892b4';
export const TEST_ENTITY_PUBLICATION = Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION') || 'e98b0f27-5c19-49a0-960d-eb6ad5287067';
// Search term (should return results) used in search tests
export const TEST_SEARCH_TERM = Cypress.env('DSPACE_TEST_SEARCH_TERM') || 'test';
// Collection used for submission tests
export const TEST_SUBMIT_COLLECTION_NAME = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME') || 'Sample Collection';
export const TEST_SUBMIT_COLLECTION_UUID = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID') || '9d8334e9-25d3-4a67-9cea-3dffdef80144';
export const TEST_SUBMIT_USER = Cypress.env('DSPACE_TEST_SUBMIT_USER') || 'dspacedemo+submit@gmail.com';
export const TEST_SUBMIT_USER_PASSWORD = Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD') || 'dspace';

View File

@@ -1,93 +1,93 @@
# Docker Compose files # Docker Compose files
*** ***
:warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario. :warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario.
*** ***
## 'Dockerfile' in root directory ## 'Dockerfile' in root directory
This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular' This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular'
``` ```
docker build -t dspace/dspace-angular:dspace-7_x . docker build -t dspace/dspace-angular:dspace-7_x .
``` ```
This image is built *automatically* after each commit is made to the `main` branch. This image is built *automatically* after each commit is made to the `main` branch.
Admins to our DockerHub repo can manually publish with the following command. Admins to our DockerHub repo can manually publish with the following command.
``` ```
docker push dspace/dspace-angular:dspace-7_x docker push dspace/dspace-angular:dspace-7_x
``` ```
## docker directory ## docker directory
- docker-compose.yml - docker-compose.yml
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker. - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
- docker-compose-rest.yml - docker-compose-rest.yml
- Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes - Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes
- docker-compose-ci.yml - docker-compose-ci.yml
- Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup. - Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup.
- cli.yml - cli.yml
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container. - Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
- cli.assetstore.yml - cli.assetstore.yml
- Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing. - Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing.
## To refresh / pull DSpace images from Dockerhub ## To refresh / pull DSpace images from Dockerhub
``` ```
docker-compose -f docker/docker-compose.yml pull docker-compose -f docker/docker-compose.yml pull
``` ```
## To build DSpace images using code in your branch ## To build DSpace images using code in your branch
``` ```
docker-compose -f docker/docker-compose.yml build docker-compose -f docker/docker-compose.yml build
``` ```
## To start DSpace (REST and Angular) from your branch ## To start DSpace (REST and Angular) from your branch
``` ```
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
``` ```
## Run DSpace REST and DSpace Angular from local branches. ## Run DSpace REST and DSpace Angular from local branches.
_The system will be started in 2 steps. Each step shares the same docker network._ _The system will be started in 2 steps. Each step shares the same docker network._
From DSpace/DSpace (build as needed) From DSpace/DSpace (build as needed)
``` ```
docker-compose -p d7 up -d docker-compose -p d7 up -d
``` ```
From DSpace/DSpace-angular From DSpace/DSpace-angular
``` ```
docker-compose -p d7 -f docker/docker-compose.yml up -d docker-compose -p d7 -f docker/docker-compose.yml up -d
``` ```
## Ingest test data from AIPDIR ## Ingest test data from AIPDIR
Create an administrator Create an administrator
``` ```
docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en
``` ```
Load content from AIP files Load content from AIP files
``` ```
docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
``` ```
## Alternative Ingest - Use Entities dataset ## Alternative Ingest - Use Entities dataset
_Delete your docker volumes or use a unique project (-p) name_ _Delete your docker volumes or use a unique project (-p) name_
Start DSpace with Database Content from a database dump Start DSpace with Database Content from a database dump
``` ```
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d
``` ```
Load assetstore content and trigger a re-index of the repository Load assetstore content and trigger a re-index of the repository
``` ```
docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
``` ```
## End to end testing of the rest api (runs in travis). ## End to end testing of the rest api (runs in travis).
_In this instance, only the REST api runs in Docker using the Entities dataset. Travis will perform CI testing of Angular using Node to drive the tests._ _In this instance, only the REST api runs in Docker using the Entities dataset. Travis will perform CI testing of Angular using Node to drive the tests._
``` ```
docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d
``` ```

View File

@@ -35,6 +35,6 @@ services:
tar xvfz /tmp/assetstore.tar.gz tar xvfz /tmp/assetstore.tar.gz
fi fi
/dspace/bin/dspace index-discovery /dspace/bin/dspace index-discovery -b
/dspace/bin/dspace oai import /dspace/bin/dspace oai import
/dspace/bin/dspace oai clean-cache /dspace/bin/dspace oai clean-cache

View File

@@ -20,7 +20,7 @@ services:
environment: environment:
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace # This LOADSQL should be kept in sync with the URL in DSpace/DSpace
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
- LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-2021-04-14.sql - LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
dspace: dspace:
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' #### ### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
# Ensure that the database is ready BEFORE starting tomcat # Ensure that the database is ready BEFORE starting tomcat

View File

@@ -63,7 +63,7 @@ services:
# This LOADSQL should be kept in sync with the LOADSQL in # This LOADSQL should be kept in sync with the LOADSQL in
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-2021-04-14.sql LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
PGDATA: /pgdata PGDATA: /pgdata
image: dspace/dspace-postgres-pgcrypto:loadsql image: dspace/dspace-postgres-pgcrypto:loadsql
networks: networks:

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,10 @@
"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", "serve": "ng serve -c 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 -c 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 +36,8 @@
"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",
"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,32 +48,35 @@
"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": "^14.0.1",
"@ngrx/store": "^11.1.1", "@ng-dynamic-forms/ui-ng-bootstrap": "^14.0.1",
"@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": "^10.0.0", "angulartics2": "^10.0.0",
"bootstrap": "4.3.1", "bootstrap": "4.3.1",
"caniuse-lite": "^1.0.30001165", "caniuse-lite": "^1.0.30001165",
@@ -102,7 +106,7 @@
"mirador-share-plugin": "^0.11.0", "mirador-share-plugin": "^0.11.0",
"moment": "^2.29.1", "moment": "^2.29.1",
"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 +115,34 @@
"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": "^6.6.3",
"sortablejs": "1.13.0", "sortablejs": "1.13.0",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"url-parse": "^1.5.3", "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"
}, },
"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,36 +151,43 @@
"@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": "^3.0.1", "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",
"cypress": "8.6.0", "cssnano": "^5.0.6",
"cypress": "9.5.1",
"cypress-axe": "^0.13.0", "cypress-axe": "^0.13.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",
"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.6.0", "jasmine-marbles": "0.6.0",
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "~5.0.0",
"karma": "^5.2.3", "karma": "^6.3.14",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2", "karma-coverage-istanbul-reporter": "~3.0.2",
"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": "^12.0.0",
"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",
@@ -178,15 +196,15 @@
"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": "^7.5.3",
"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"

View File

@@ -24,7 +24,7 @@ if (!fs.existsSync(envFullPath)) {
} }
try { try {
const env = require(envFullPath); const env = require(envFullPath).environment;
const config = yaml.dump(env); const config = yaml.dump(env);
if (args[1]) { if (args[1]) {

View File

@@ -3,7 +3,7 @@ import * as https from 'https';
import { AppConfig } from '../src/config/app-config.interface'; import { AppConfig } from '../src/config/app-config.interface';
import { buildAppConfig } from '../src/config/config.server'; import { buildAppConfig } from '../src/config/config.server';
const appConfig: AppConfig = buildAppConfig(); const appConfig: AppConfig = buildAppConfig();
/** /**

View File

@@ -15,7 +15,7 @@
* 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';

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

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

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

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

@@ -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">
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span> <a [ngClass]="{'btn-sm': small}" class="btn btn-secondary move-link" [routerLink]="[getMoveRoute()]" [title]="'admin.search.item.move' | translate">
</a> <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 [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

@@ -237,7 +237,24 @@ describe('AdminSidebarComponent', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
parentID: 'access_control', visible: false, parentID: 'access_control', visible: false,
})); }));
});
// We check that the menu section has not been called with visible set to true
// The reason why we don't check if it has been called with visible set to false
// Is because the function does not get called unless a user is authorised
it('should not show the import section', () => {
expect(menuService.addSection).not.toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'import', visible: true,
}));
});
// We check that the menu section has not been called with visible set to true
// The reason why we don't check if it has been called with visible set to false
// Is because the function does not get called unless a user is authorised
it('should not show the export section', () => {
expect(menuService.addSection).not.toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'export', visible: true,
}));
}); });
}); });
@@ -268,6 +285,15 @@ describe('AdminSidebarComponent', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'workflow', visible: true, id: 'workflow', visible: true,
})); }));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'workflow', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'import', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'export', visible: true,
}));
}); });
}); });

View File

@@ -1,18 +1,35 @@
import { Component, HostListener, Injector, OnInit } from '@angular/core'; import { Component, HostListener, Injector, OnInit } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { combineLatest, combineLatest as observableCombineLatest, Observable, BehaviorSubject } from 'rxjs'; import { BehaviorSubject, combineLatest as observableCombineLatest, combineLatest, Observable } from 'rxjs';
import { debounceTime, first, map, take, distinctUntilChanged, withLatestFrom } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, filter, first, map, take, 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 {
METADATA_EXPORT_SCRIPT_NAME,
METADATA_IMPORT_SCRIPT_NAME,
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 {
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; CreateCollectionParentSelectorComponent
import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; import {
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; CreateCommunityParentSelectorComponent
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
import { ExportMetadataSelectorComponent } from '../../shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; import {
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state'; 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 { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model'; import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model'; import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model'; import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
@@ -21,7 +38,9 @@ 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 { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { Router, ActivatedRoute } from '@angular/router'; import { MenuID } from '../../shared/menu/menu-id.model';
import { MenuItemType } from '../../shared/menu/menu-item-type.model';
import { ActivatedRoute } from '@angular/router';
/** /**
* Component representing the admin sidebar * Component representing the admin sidebar
@@ -63,13 +82,14 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
inFocus$: BehaviorSubject<boolean>; inFocus$: BehaviorSubject<boolean>;
constructor(protected menuService: MenuService, constructor(
protected menuService: MenuService,
protected injector: Injector, protected injector: Injector,
private variableService: CSSVariableService, protected variableService: CSSVariableService,
private authService: AuthService, protected authService: AuthService,
private modalService: NgbModal, protected modalService: NgbModal,
public authorizationService: AuthorizationDataService, public authorizationService: AuthorizationDataService,
private scriptDataService: ScriptDataService, protected scriptDataService: ScriptDataService,
public route: ActivatedRoute public route: ActivatedRoute
) { ) {
super(menuService, injector, authorizationService, route); super(menuService, injector, authorizationService, route);
@@ -80,12 +100,12 @@ 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()
.subscribe((loggedIn: boolean) => { .subscribe((loggedIn: boolean) => {
if (loggedIn) { if (loggedIn) {
this.createMenu();
this.menuService.showMenu(this.menuID); this.menuService.showMenu(this.menuID);
} }
}); });
@@ -320,19 +340,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
*/ */
createExportMenuSections() { createExportMenuSections() {
const menuList = [ 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 // TODO: enable this menu item once the feature has been implemented
// { // {
// id: 'export_community', // id: 'export_community',
@@ -375,14 +382,28 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
]; ];
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection)); menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
observableCombineLatest( observableCombineLatest([
this.authorizationService.isAuthorized(FeatureID.AdministratorOf), this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
// this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME) this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME)
).pipe( ]).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),
// filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists),
take(1) take(1)
).subscribe(() => { ).subscribe(() => {
// Hides the export menu for unauthorised people
// If in the future more sub-menus are added,
// it should be reviewed if they need to be in this subscribe
this.menuService.addSection(this.menuID, {
id: 'export',
active: false,
visible: true,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.export'
} as TextMenuItemModel,
icon: 'file-export',
index: 3,
shouldPersistOnRouteChange: true
});
this.menuService.addSection(this.menuID, { this.menuService.addSection(this.menuID, {
id: 'export_metadata', id: 'export_metadata',
parentID: 'export', parentID: 'export',
@@ -406,18 +427,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
*/ */
createImportMenuSections() { createImportMenuSections() {
const menuList = [ 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 // TODO: enable this menu item once the feature has been implemented
// { // {
// id: 'import_batch', // id: 'import_batch',
@@ -435,14 +444,27 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
shouldPersistOnRouteChange: true shouldPersistOnRouteChange: true
}))); })));
observableCombineLatest( observableCombineLatest([
this.authorizationService.isAuthorized(FeatureID.AdministratorOf), this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
// this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME) this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME)
).pipe( ]).pipe(
// TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists),
// filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists),
take(1) take(1)
).subscribe(() => { ).subscribe(() => {
// Hides the import menu for unauthorised people
// If in the future more sub-menus are added,
// it should be reviewed if they need to be in this subscribe
this.menuService.addSection(this.menuID, {
id: 'import',
active: false,
visible: true,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.import'
} as TextMenuItemModel,
icon: 'file-import',
index: 2
});
this.menuService.addSection(this.menuID, { this.menuService.addSection(this.menuID, {
id: 'import_metadata', id: 'import_metadata',
parentID: 'import', parentID: 'import',
@@ -551,10 +573,10 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
* Create menu sections dependent on whether or not the current user can manage access control groups * Create menu sections dependent on whether or not the current user can manage access control groups
*/ */
createAccessControlMenuSections() { createAccessControlMenuSections() {
observableCombineLatest( observableCombineLatest([
this.authorizationService.isAuthorized(FeatureID.AdministratorOf), this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
this.authorizationService.isAuthorized(FeatureID.CanManageGroups) this.authorizationService.isAuthorized(FeatureID.CanManageGroups)
).subscribe(([isSiteAdmin, canManageGroups]) => { ]).subscribe(([isSiteAdmin, canManageGroups]) => {
const menuList = [ const menuList = [
/* Access Control */ /* Access Control */
{ {

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

@@ -0,0 +1,25 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { AdminSidebarComponent } from './admin-sidebar.component';
/**
* Themed wrapper for AdminSidebarComponent
*/
@Component({
selector: 'ds-themed-admin-sidebar',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})
export class ThemedAdminSidebarComponent extends ThemedComponent<AdminSidebarComponent> {
protected getComponentName(): string {
return 'AdminSidebarComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/admin/admin-sidebar/admin-sidebar.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import('./admin-sidebar.component');
}
}

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

@@ -201,7 +201,6 @@ export class AppComponent implements OnInit, AfterViewInit {
if (event instanceof NavigationStart) { if (event instanceof NavigationStart) {
resolveEndFound = false; resolveEndFound = false;
this.isRouteLoading$.next(true); this.isRouteLoading$.next(true);
this.isThemeLoading$.next(true);
} else if (event instanceof ResolveEnd) { } else if (event instanceof ResolveEnd) {
resolveEndFound = true; resolveEndFound = true;
const activatedRouteSnapShot: ActivatedRouteSnapshot = event.state.root; const activatedRouteSnapShot: ActivatedRouteSnapshot = event.state.root;

View File

@@ -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,
@@ -56,8 +55,12 @@ import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component'; import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.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 { 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';
export function getConfig() { export function getConfig() {
return environment; return environment;
@@ -89,19 +92,14 @@ 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(), ThemedEntryComponentModule.withEntryComponents(),
StoreDevModules,
]; ];
IMPORTS.push(
StoreDevtoolsModule.instrument({
maxAge: 1000,
logOnly: environment.production,
})
);
const PROVIDERS = [ const PROVIDERS = [
{ {
provide: APP_CONFIG, provide: APP_CONFIG,
@@ -171,6 +169,7 @@ const DECLARATIONS = [
HeaderNavbarWrapperComponent, HeaderNavbarWrapperComponent,
ThemedHeaderNavbarWrapperComponent, ThemedHeaderNavbarWrapperComponent,
AdminSidebarComponent, AdminSidebarComponent,
ThemedAdminSidebarComponent,
AdminSidebarSectionComponent, AdminSidebarSectionComponent,
ExpandableAdminSidebarSectionComponent, ExpandableAdminSidebarSectionComponent,
FooterComponent, FooterComponent,

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

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

@@ -18,6 +18,7 @@ import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { isValidDate } from '../../shared/date.util';
@Component({ @Component({
selector: 'ds-browse-by-date-page', selector: 'ds-browse-by-date-page',
@@ -85,10 +86,10 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
let lowerLimit = environment.browseBy.defaultLowerLimit; let lowerLimit = environment.browseBy.defaultLowerLimit;
if (hasValue(firstItemRD.payload)) { if (hasValue(firstItemRD.payload)) {
const date = firstItemRD.payload.firstMetadataValue(metadataKeys); const date = firstItemRD.payload.firstMetadataValue(metadataKeys);
if (hasValue(date)) { if (isNotEmpty(date) && isValidDate(date)) {
const dateObj = new Date(date); const dateObj = new Date(date);
// TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC. // TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC.
lowerLimit = dateObj.getUTCFullYear(); lowerLimit = isNaN(dateObj.getUTCFullYear()) ? lowerLimit : dateObj.getUTCFullYear();
} }
} }
const options = []; const options = [];

View File

@@ -18,7 +18,7 @@
</ds-comcol-page-content> </ds-comcol-page-content>
</header> </header>
<!-- Browse-By Links --> <!-- Browse-By Links -->
<ds-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-comcol-page-browse-by> <ds-themed-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-themed-comcol-page-browse-by>
</ng-container></ng-container> </ng-container></ng-container>
<section class="comcol-page-browse-section"> <section class="comcol-page-browse-section">
@@ -31,7 +31,6 @@
[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

@@ -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,7 @@ 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.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,8 @@ 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';
describe('CollectionItemMapperComponent', () => { describe('CollectionItemMapperComponent', () => {
let comp: CollectionItemMapperComponent; let comp: CollectionItemMapperComponent;
@@ -110,15 +112,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: () => {
@@ -159,6 +161,14 @@ describe('CollectionItemMapperComponent', () => {
{ provide: RouteService, useValue: routeServiceStub }, { provide: RouteService, useValue: routeServiceStub },
{ provide: AuthorizationDataService, useValue: authorizationDataService } { provide: AuthorizationDataService, useValue: authorizationDataService }
] ]
}).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

@@ -34,16 +34,16 @@
[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>
<section class="comcol-page-browse-section"> <section class="comcol-page-browse-section">
<!-- Browse-By Links --> <!-- Browse-By Links -->
<ds-comcol-page-browse-by <ds-themed-comcol-page-browse-by
[id]="collection.id" [id]="collection.id"
[contentType]="collection.type"> [contentType]="collection.type">
</ds-comcol-page-browse-by> </ds-themed-comcol-page-browse-by>
<ng-container *ngVar="(itemRD$ | async) as itemRD"> <ng-container *ngVar="(itemRD$ | async) as itemRD">
<div class="mt-4" *ngIf="itemRD?.hasSucceeded" @fadeIn> <div class="mt-4" *ngIf="itemRD?.hasSucceeded" @fadeIn>

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

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

@@ -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>
@@ -51,4 +51,4 @@
</div> </div>
</div> </div>

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

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

@@ -1,4 +1,4 @@
<div class="container"> <div class="container">
<h2>{{ 'communityList.title' | translate }}</h2> <h2>{{ 'communityList.title' | translate }}</h2>
<ds-community-list></ds-community-list> <ds-themed-community-list></ds-themed-community-list>
</div> </div>

View File

@@ -5,12 +5,14 @@ import { CommunityListPageComponent } from './community-list-page.component';
import { CommunityListPageRoutingModule } from './community-list-page.routing.module'; import { CommunityListPageRoutingModule } from './community-list-page.routing.module';
import { CommunityListComponent } from './community-list/community-list.component'; import { CommunityListComponent } from './community-list/community-list.component';
import { ThemedCommunityListPageComponent } from './themed-community-list-page.component'; import { ThemedCommunityListPageComponent } from './themed-community-list-page.component';
import { ThemedCommunityListComponent } from './community-list/themed-community-list.component';
const DECLARATIONS = [ const DECLARATIONS = [
CommunityListPageComponent, CommunityListPageComponent,
CommunityListComponent, CommunityListComponent,
ThemedCommunityListPageComponent ThemedCommunityListPageComponent,
ThemedCommunityListComponent
]; ];
/** /**
* The page which houses a title and the community list, as described in community-list.component * The page which houses a title and the community list, as described in community-list.component

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

@@ -0,0 +1,24 @@
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { CommunityListComponent } from './community-list.component';
import { Component } from '@angular/core';
@Component({
selector: 'ds-themed-community-list',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})
export class ThemedCommunityListComponent extends ThemedComponent<CommunityListComponent> {
protected getComponentName(): string {
return 'CommunityListComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/community-list-page/community-list/community-list.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./community-list.component`);
}
}

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,14 +20,14 @@
[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>
<section class="comcol-page-browse-section"> <section class="comcol-page-browse-section">
<!-- Browse-By Links --> <!-- Browse-By Links -->
<ds-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type"> <ds-themed-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
</ds-comcol-page-browse-by> </ds-themed-comcol-page-browse-by>
<ds-community-page-sub-community-list [community]="communityPayload"></ds-community-page-sub-community-list> <ds-community-page-sub-community-list [community]="communityPayload"></ds-community-page-sub-community-list>
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list> <ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>

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

@@ -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,7 @@ 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';
describe('CommunityPageSubCollectionList Component', () => { describe('CommunityPageSubCollectionList Component', () => {
let comp: CommunityPageSubCollectionListComponent; let comp: CommunityPageSubCollectionListComponent;

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,7 @@ 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';
describe('CommunityPageSubCommunityListComponent Component', () => { describe('CommunityPageSubCommunityListComponent Component', () => {
let comp: CommunityPageSubCommunityListComponent; let comp: CommunityPageSubCommunityListComponent;

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,6 +11,7 @@ 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';
/** /**
* Abstract service to send authentication requests * Abstract service to send authentication requests

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';
@@ -219,6 +219,9 @@ describe('AuthEffects', () => {
const expected = cold('--b-', { b: new RetrieveTokenAction() }); const expected = cold('--b-', { b: new RetrieveTokenAction() });
expect(authEffects.checkTokenCookie$).toBeObservable(expected); expect(authEffects.checkTokenCookie$).toBeObservable(expected);
authEffects.checkTokenCookie$.subscribe(() => {
expect((authEffects as any).authorizationsService.invalidateAuthorizationsRequestCache).toHaveBeenCalled();
});
}); });
it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => { it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => {
@@ -393,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(authServiceStub.removeToken).not.toHaveBeenCalled(); expect(false).toBeTrue(); // subscribe to trigger taps, fail if the effect emits (we don't expect it to)
}); });
tick(1000);
done(); expect(authServiceStub.removeToken).not.toHaveBeenCalled();
}); }));
}); });
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,25 +136,24 @@ 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(
map((response: AuthStatus) => { map((response: AuthStatus) => {
if (response.authenticated) { if (response.authenticated) {
this.authorizationsService.invalidateAuthorizationsRequestCache();
return new RetrieveTokenAction(); return new RetrieveTokenAction();
} else { } else {
return new RetrieveAuthMethodsAction(response); return new RetrieveAuthMethodsAction(response);
@@ -170,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(
@@ -182,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(() => {
@@ -240,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) => {
@@ -269,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
@@ -277,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
@@ -289,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

View File

@@ -20,9 +20,9 @@ describe(`AuthInterceptor`, () => {
const authServiceStub = new AuthServiceStub(); const authServiceStub = new AuthServiceStub();
const store: Store<TruncatablesState> = jasmine.createSpyObj('store', { const store: Store<TruncatablesState> = jasmine.createSpyObj('store', {
/* tslint:disable:no-empty */ /* eslint-disable no-empty,@typescript-eslint/no-empty-function */
dispatch: {}, dispatch: {},
/* tslint:enable:no-empty */ /* eslint-enable no-empty, @typescript-eslint/no-empty-function */
select: observableOf(true) select: observableOf(true)
}); });

View File

@@ -144,7 +144,7 @@ export class AuthInterceptor implements HttpInterceptor {
const regex = /(\w+ (\w+=((".*?")|[^,]*)(, )?)*)/g; const regex = /(\w+ (\w+=((".*?")|[^,]*)(, )?)*)/g;
const realms = completeWWWauthenticateHeader.match(regex); const realms = completeWWWauthenticateHeader.match(regex);
// tslint:disable-next-line:forin // eslint-disable-next-line guard-for-in
for (const j in realms) { for (const j in realms) {
const splittedRealm = realms[j].split(', '); const splittedRealm = realms[j].split(', ');

View File

@@ -112,7 +112,7 @@ export class AuthService {
if (hasValue(rd.payload) && rd.payload.authenticated) { if (hasValue(rd.payload) && rd.payload.authenticated) {
return rd.payload; return rd.payload;
} else { } else {
throw(new Error('Invalid email or password')); throw (new Error('Invalid email or password'));
} }
})); }));
@@ -166,7 +166,7 @@ export class AuthService {
if (hasValue(status) && status.authenticated) { if (hasValue(status) && status.authenticated) {
return status._links.eperson.href; return status._links.eperson.href;
} else { } else {
throw(new Error('Not authenticated')); throw (new Error('Not authenticated'));
} }
})); }));
} }
@@ -249,7 +249,7 @@ export class AuthService {
if (hasValue(status) && status.authenticated) { if (hasValue(status) && status.authenticated) {
return status.token; return status.token;
} else { } else {
throw(new Error('Not authenticated')); throw (new Error('Not authenticated'));
} }
})); }));
} }
@@ -288,7 +288,7 @@ export class AuthService {
if (hasValue(status) && !status.authenticated) { if (hasValue(status) && !status.authenticated) {
return true; return true;
} else { } else {
throw(new Error('auth.errors.invalid-user')); throw (new Error('auth.errors.invalid-user'));
} }
})); }));
} }

View File

@@ -11,9 +11,9 @@ import { Observable } from 'rxjs';
import { map, find, switchMap } from 'rxjs/operators'; import { map, find, switchMap } from 'rxjs/operators';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { isAuthenticated, isAuthenticationLoading } from './selectors'; import { isAuthenticated, isAuthenticationLoading } from './selectors';
import { AuthService, LOGIN_ROUTE } from './auth.service'; import { AuthService, LOGIN_ROUTE } from './auth.service';
import { CoreState } from '../core-state.model';
/** /**
* Prevent unauthorized activating and loading of routes * Prevent unauthorized activating and loading of routes

View File

@@ -2,7 +2,6 @@ import { autoserialize, deserialize, deserializeAs } from 'cerialize';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { link, typedObject } from '../../cache/builders/build-decorators'; import { link, typedObject } from '../../cache/builders/build-decorators';
import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer';
import { CacheableObject } from '../../cache/object-cache.reducer';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
import { EPerson } from '../../eperson/models/eperson.model'; import { EPerson } from '../../eperson/models/eperson.model';
import { EPERSON } from '../../eperson/models/eperson.resource-type'; import { EPERSON } from '../../eperson/models/eperson.resource-type';
@@ -13,6 +12,7 @@ import { AuthError } from './auth-error.model';
import { AUTH_STATUS } from './auth-status.resource-type'; import { AUTH_STATUS } from './auth-status.resource-type';
import { AuthTokenInfo } from './auth-token-info.model'; import { AuthTokenInfo } from './auth-token-info.model';
import { AuthMethod } from './auth.method'; import { AuthMethod } from './auth.method';
import { CacheableObject } from '../../cache/cacheable-object.model';
/** /**
* Object that represents the authenticated status of a user * Object that represents the authenticated status of a user

View File

@@ -1,10 +1,10 @@
import { CacheableObject } from '../../cache/object-cache.reducer';
import { typedObject } from '../../cache/builders/build-decorators'; import { typedObject } from '../../cache/builders/build-decorators';
import { excludeFromEquals } from '../../utilities/equals.decorators'; import { excludeFromEquals } from '../../utilities/equals.decorators';
import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; import { autoserialize, autoserializeAs, deserialize } from 'cerialize';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
import { SHORT_LIVED_TOKEN } from './short-lived-token.resource-type'; import { SHORT_LIVED_TOKEN } from './short-lived-token.resource-type';
import { HALLink } from '../../shared/hal-link.model'; import { HALLink } from '../../shared/hal-link.model';
import { CacheableObject } from '../../cache/cacheable-object.model';
/** /**
* A short-lived token that can be used to authenticate a rest request * A short-lived token that can be used to authenticate a rest request

View File

@@ -8,6 +8,8 @@ import { createSelector } from '@ngrx/store';
*/ */
import { AuthState } from './auth.reducer'; import { AuthState } from './auth.reducer';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { CoreState } from '../core-state.model';
import { coreSelector } from '../core.selectors';
/** /**
* Returns the user state. * Returns the user state.
@@ -15,7 +17,7 @@ import { AppState } from '../../app.reducer';
* @param {AppState} state Top level state. * @param {AppState} state Top level state.
* @return {AuthState} * @return {AuthState}
*/ */
export const getAuthState = (state: any) => state.core.auth; export const getAuthState = createSelector(coreSelector, (state: CoreState) => state.auth);
/** /**
* Returns true if the user is authenticated. * Returns true if the user is authenticated.

View File

@@ -36,7 +36,7 @@ export class ServerAuthService extends AuthService {
if (hasValue(status) && status.authenticated) { if (hasValue(status) && status.authenticated) {
return status._links.eperson.href; return status._links.eperson.href;
} else { } else {
throw(new Error('Not authenticated')); throw (new Error('Not authenticated'));
} }
})); }));
} }

View File

@@ -1,9 +1,9 @@
import { ResponseParsingService } from '../data/parsing.service'; import { ResponseParsingService } from '../data/parsing.service';
import { RestRequest } from '../data/request.models';
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
import { RestResponse, TokenResponse } from '../cache/response.models'; import { RestResponse, TokenResponse } from '../cache/response.models';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { RestRequest } from '../data/rest-request.model';
@Injectable() @Injectable()
/** /**

View File

@@ -1,7 +1,7 @@
import { BrowseDefinitionDataService } from './browse-definition-data.service'; import { BrowseDefinitionDataService } from './browse-definition-data.service';
import { FindListOptions } from '../data/request.models';
import { followLink } from '../../shared/utils/follow-link-config.model'; import { followLink } from '../../shared/utils/follow-link-config.model';
import { EMPTY } from 'rxjs'; import { EMPTY } from 'rxjs';
import { FindListOptions } from '../data/find-list-options.model';
describe(`BrowseDefinitionDataService`, () => { describe(`BrowseDefinitionDataService`, () => {
let service: BrowseDefinitionDataService; let service: BrowseDefinitionDataService;

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