mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'master' into scripts-processes
This commit is contained in:
@@ -21,7 +21,7 @@
|
|||||||
"path": "./webpack/webpack.common.ts",
|
"path": "./webpack/webpack.common.ts",
|
||||||
"mergeStrategies": {
|
"mergeStrategies": {
|
||||||
"loaders": "prepend"
|
"loaders": "prepend"
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"outputPath": "dist/browser",
|
"outputPath": "dist/browser",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
- 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.
|
||||||
- environment.dev.js
|
- environment.dev.ts
|
||||||
- Environment file for running DSpace Angular in Docker
|
- Environment file for running DSpace Angular in Docker
|
||||||
- local.cfg
|
- local.cfg
|
||||||
- Environment file for running the DSpace 7 REST API in Docker.
|
- Environment file for running the DSpace 7 REST API in Docker.
|
||||||
|
@@ -23,4 +23,4 @@ services:
|
|||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
volumes:
|
volumes:
|
||||||
- ./environment.dev.js:/app/src/environments/environment.dev.ts
|
- ./environment.dev.ts:/app/src/environments/environment.dev.ts
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
/*
|
/**
|
||||||
* The contents of this file are subject to the license and copyright
|
* The contents of this file are subject to the license and copyright
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
* tree and available online at
|
* tree and available online at
|
||||||
*
|
*
|
||||||
* http://www.dspace.org/license/
|
* http://www.dspace.org/license/
|
||||||
*/
|
*/
|
||||||
import { GlobalConfig } from '../src/config/global-config.interface';
|
// This file is based on environment.template.ts provided by Angular UI
|
||||||
|
export const environment = {
|
||||||
export const environment: Partial<GlobalConfig> = {
|
// Default to using the local REST API (running in Docker)
|
||||||
rest: {
|
rest: {
|
||||||
ssl: false,
|
ssl: false,
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
dspace.dir=/dspace
|
dspace.dir=/dspace
|
||||||
db.url=jdbc:postgresql://dspacedb:5432/dspace
|
db.url=jdbc:postgresql://dspacedb:5432/dspace
|
||||||
dspace.server.url=http://localhost:8080/server
|
dspace.server.url=http://localhost:8080/server
|
||||||
|
dspace.ui.url=http://localhost:4000
|
||||||
dspace.name=DSpace Started with Docker Compose
|
dspace.name=DSpace Started with Docker Compose
|
||||||
solr.server=http://dspacesolr:8983/solr
|
solr.server=http://dspacesolr:8983/solr
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
Default configuration file is located in `config/` folder. All configuration options should be listed in the default configuration file `config/environment.default.js`. Please do not change this file directly! To change the default configuration values, create local files that override the parameters you need to change:
|
Default configuration file is located in `src/environments/` folder. All configuration options should be listed in the default configuration file `src/environments/environment.common.ts`. Please do not change this file directly! To change the default configuration values, create local files that override the parameters you need to change. You can use `environment.template.ts` as a starting point.
|
||||||
|
|
||||||
- Create a new `environment.dev.js` file in `config/` for `devel` environment;
|
- Create a new `environment.dev.ts` file in `src/environments/` for `development` environment;
|
||||||
- Create a new `environment.prod.js` file in `config/` for `production` environment;
|
- Create a new `environment.prod.ts` file in `src/environments/` for `production` environment;
|
||||||
|
|
||||||
Some few configuration options can be overridden by setting environment variables. These and the variable names are listed below.
|
Some few configuration options can be overridden by setting environment variables. These and the variable names are listed below.
|
||||||
|
|
||||||
@@ -12,8 +12,8 @@ When you start dspace-angular on node, it spins up an http server on which it li
|
|||||||
|
|
||||||
To change this configuration, change the options `ui.host`, `ui.port` and `ui.ssl` in the appropriate configuration file (see above):
|
To change this configuration, change the options `ui.host`, `ui.port` and `ui.ssl` in the appropriate configuration file (see above):
|
||||||
```
|
```
|
||||||
module.exports = {
|
export const environment = {
|
||||||
// Angular Universal server settings.
|
// Angular UI settings.
|
||||||
ui: {
|
ui: {
|
||||||
ssl: false,
|
ssl: false,
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
@@ -35,14 +35,14 @@ Alternately you can set the following environment variables. If any of these are
|
|||||||
dspace-angular connects to your DSpace installation by using its REST endpoint. To do so, you have to define the ip address, port and if ssl should be enabled. You can do this in a configuration file (see above) by adding the following options:
|
dspace-angular connects to your DSpace installation by using its REST endpoint. To do so, you have to define the ip address, port and if ssl should be enabled. You can do this in a configuration file (see above) by adding the following options:
|
||||||
|
|
||||||
```
|
```
|
||||||
module.exports = {
|
export const environment = {
|
||||||
// The REST API server settings.
|
// The REST API server settings.
|
||||||
rest: {
|
rest: {
|
||||||
ssl: true,
|
ssl: true,
|
||||||
host: 'dspace7.4science.it',
|
host: 'dspace7.4science.cloud',
|
||||||
port: 443,
|
port: 443,
|
||||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||||
nameSpace: '/dspace-spring-rest/api'
|
nameSpace: '/server/api'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
@@ -50,9 +50,9 @@ module.exports = {
|
|||||||
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
||||||
```
|
```
|
||||||
DSPACE_REST_SSL=true
|
DSPACE_REST_SSL=true
|
||||||
DSPACE_REST_HOST=localhost
|
DSPACE_REST_HOST=dspace7.4science.cloud
|
||||||
DSPACE_REST_PORT=4000
|
DSPACE_REST_PORT=443
|
||||||
DSPACE_REST_NAMESPACE=/
|
DSPACE_REST_NAMESPACE=/server/api
|
||||||
```
|
```
|
||||||
|
|
||||||
## Supporting analytics services other than Google Analytics
|
## Supporting analytics services other than Google Analytics
|
||||||
|
14
package.json
14
package.json
@@ -17,7 +17,7 @@
|
|||||||
"pree2e": "yarn run config:prod",
|
"pree2e": "yarn run config:prod",
|
||||||
"pree2e:ci": "yarn run config:prod",
|
"pree2e:ci": "yarn run config:prod",
|
||||||
"start": "yarn run start:prod",
|
"start": "yarn run start:prod",
|
||||||
"serve": "ng serve",
|
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
||||||
"start:dev": "npm-run-all --parallel config:dev:watch serve",
|
"start:dev": "npm-run-all --parallel config:dev:watch serve",
|
||||||
"start:prod": "yarn run build:prod && yarn run serve:ssr",
|
"start:prod": "yarn run build:prod && yarn run serve:ssr",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json && yarn run clean:bld",
|
"clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json && yarn run clean:bld",
|
||||||
"clean": "yarn run clean:prod && yarn run clean:node && yarn run clean:env",
|
"clean": "yarn run clean:prod && yarn run clean:node && yarn run clean:env",
|
||||||
"clean:env": "rimraf src/environments/environment.ts",
|
"clean:env": "rimraf src/environments/environment.ts",
|
||||||
"sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts"
|
"sync-i18n": "yarn run config:dev && ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts"
|
||||||
},
|
},
|
||||||
"browser": {
|
"browser": {
|
||||||
"fs": false,
|
"fs": false,
|
||||||
@@ -52,6 +52,9 @@
|
|||||||
"https": false
|
"https": false
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"resolutions": {
|
||||||
|
"minimist": "^1.2.5"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~8.2.14",
|
"@angular/animations": "~8.2.14",
|
||||||
"@angular/cdk": "8.2.3",
|
"@angular/cdk": "8.2.3",
|
||||||
@@ -88,6 +91,7 @@
|
|||||||
"express": "4.16.2",
|
"express": "4.16.2",
|
||||||
"fast-json-patch": "^2.0.7",
|
"fast-json-patch": "^2.0.7",
|
||||||
"file-saver": "^1.3.8",
|
"file-saver": "^1.3.8",
|
||||||
|
"filesize": "^6.1.0",
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
"https": "1.0.0",
|
"https": "1.0.0",
|
||||||
"js-cookie": "2.2.0",
|
"js-cookie": "2.2.0",
|
||||||
@@ -146,7 +150,7 @@
|
|||||||
"jasmine-core": "^3.3.0",
|
"jasmine-core": "^3.3.0",
|
||||||
"jasmine-marbles": "0.3.1",
|
"jasmine-marbles": "0.3.1",
|
||||||
"jasmine-spec-reporter": "~4.2.1",
|
"jasmine-spec-reporter": "~4.2.1",
|
||||||
"karma": "~4.1.0",
|
"karma": "^5.0.9",
|
||||||
"karma-chrome-launcher": "~2.2.0",
|
"karma-chrome-launcher": "~2.2.0",
|
||||||
"karma-coverage-istanbul-reporter": "~2.0.1",
|
"karma-coverage-istanbul-reporter": "~2.0.1",
|
||||||
"karma-jasmine": "2.0.1",
|
"karma-jasmine": "2.0.1",
|
||||||
@@ -157,10 +161,10 @@
|
|||||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||||
"postcss-apply": "0.11.0",
|
"postcss-apply": "0.11.0",
|
||||||
"postcss-cssnext": "3.1.0",
|
"postcss-cssnext": "3.1.0",
|
||||||
|
"postcss-import": "^12.0.1",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
"postcss-responsive-type": "1.0.0",
|
"postcss-responsive-type": "1.0.0",
|
||||||
"postcss-smart-import": "^0.7.6",
|
"protractor": "^7.0.0",
|
||||||
"protractor": "~5.4.0",
|
|
||||||
"protractor-istanbul-plugin": "2.0.0",
|
"protractor-istanbul-plugin": "2.0.0",
|
||||||
"raw-loader": "0.5.1",
|
"raw-loader": "0.5.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
require('postcss-smart-import')(),
|
require('postcss-import')(),
|
||||||
require('postcss-cssnext')(),
|
require('postcss-cssnext')(),
|
||||||
require('postcss-apply')(),
|
require('postcss-apply')(),
|
||||||
require('postcss-responsive-type')()
|
require('postcss-responsive-type')()
|
||||||
|
11
scripts/serve.ts
Normal file
11
scripts/serve.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { environment } from '../src/environments/environment';
|
||||||
|
|
||||||
|
import * as child from 'child_process';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls `ng serve` with the following arguments configured for the UI in the environment file: host, port, nameSpace, ssl
|
||||||
|
*/
|
||||||
|
child.spawn(
|
||||||
|
`ng serve --host ${environment.ui.host} --port ${environment.ui.port} --servePath ${environment.ui.nameSpace} --ssl ${environment.ui.ssl}`,
|
||||||
|
{ stdio:'inherit', shell: true }
|
||||||
|
);
|
213
server.ts
213
server.ts
@@ -17,26 +17,67 @@
|
|||||||
|
|
||||||
import 'zone.js/dist/zone-node';
|
import 'zone.js/dist/zone-node';
|
||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
|
import 'rxjs';
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as pem from 'pem';
|
||||||
|
import * as https from 'https';
|
||||||
|
import * as morgan from 'morgan';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import { join } from 'path';
|
|
||||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
|
||||||
import * as bodyParser from 'body-parser';
|
import * as bodyParser from 'body-parser';
|
||||||
|
import * as compression from 'compression';
|
||||||
import * as cookieParser from 'cookie-parser';
|
import * as cookieParser from 'cookie-parser';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { enableProdMode, NgModuleFactory, Type } from '@angular/core';
|
||||||
|
|
||||||
|
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||||
import { environment } from './src/environments/environment';
|
import { environment } from './src/environments/environment';
|
||||||
|
|
||||||
// Express server
|
/*
|
||||||
const app = express();
|
* Set path for the browser application's dist folder
|
||||||
|
*/
|
||||||
const PORT = environment.ui.port || 4000;
|
|
||||||
const DIST_FOLDER = join(process.cwd(), 'dist/browser');
|
const DIST_FOLDER = join(process.cwd(), 'dist/browser');
|
||||||
|
|
||||||
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
|
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
|
||||||
const { ServerAppModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap } = require('./dist/server/main');
|
const { ServerAppModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap } = require('./dist/server/main');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create a new express application
|
||||||
|
*/
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If production mode is enabled in the environment file:
|
||||||
|
* - Enable Angular's production mode
|
||||||
|
* - Enable compression for response bodies. See [compression](https://github.com/expressjs/compression)
|
||||||
|
*/
|
||||||
|
if (environment.production) {
|
||||||
|
enableProdMode();
|
||||||
|
app.use(compression());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enable request logging
|
||||||
|
* See [morgan](https://github.com/expressjs/morgan)
|
||||||
|
*/
|
||||||
|
app.use(morgan('dev'));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add cookie parser middleware
|
||||||
|
* See [morgan](https://github.com/expressjs/cookie-parser)
|
||||||
|
*/
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add parser for request bodies
|
||||||
|
* See [morgan](https://github.com/expressjs/body-parser)
|
||||||
|
*/
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Render html pages by running angular server side
|
||||||
|
*/
|
||||||
app.engine('html', (_, options, callback) =>
|
app.engine('html', (_, options, callback) =>
|
||||||
ngExpressEngine({
|
ngExpressEngine({
|
||||||
bootstrap: ServerAppModuleNgFactory,
|
bootstrap: ServerAppModuleNgFactory,
|
||||||
@@ -51,25 +92,155 @@ app.engine('html', (_, options, callback) =>
|
|||||||
},
|
},
|
||||||
provideModuleMap(LAZY_MODULE_MAP)
|
provideModuleMap(LAZY_MODULE_MAP)
|
||||||
],
|
],
|
||||||
})(_, options, callback)
|
})(_, (options as any), callback)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Register the view engines for html and ejs
|
||||||
|
*/
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
app.set('view engine', 'html');
|
app.set('view engine', 'html');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set views folder path to directory where template files are stored
|
||||||
|
*/
|
||||||
app.set('views', DIST_FOLDER);
|
app.set('views', DIST_FOLDER);
|
||||||
|
|
||||||
// Example Express Rest API endpoints
|
/*
|
||||||
// app.get('/api/**', (req, res) => { });
|
* Adds a cache control header to the response
|
||||||
// Serve static files from /browser
|
* The cache control value can be configured in the environments file and defaults to max-age=60
|
||||||
app.get('*.*', express.static(DIST_FOLDER, {
|
*/
|
||||||
maxAge: '1y'
|
function cacheControl(req, res, next) {
|
||||||
}));
|
// instruct browser to revalidate
|
||||||
|
res.header('Cache-Control', environment.cache.control || 'max-age=60');
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
// All regular routes use the Universal engine
|
/*
|
||||||
app.get('*', (req, res) => {
|
* Serve static resources (images, i18n messages, …)
|
||||||
res.render('index', { req });
|
*/
|
||||||
});
|
app.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false }));
|
||||||
|
|
||||||
// Start up the Node server
|
/*
|
||||||
app.listen(PORT, () => {
|
* The callback function to serve server side angular
|
||||||
console.log(`Node Express server listening on http://localhost:${PORT}`);
|
*/
|
||||||
});
|
function ngApp(req, res) {
|
||||||
|
// Object to be set to window.dspace when CSR is used
|
||||||
|
// this allows us to pass the info in the original request
|
||||||
|
// to the dspace7-angular instance running in the client's browser
|
||||||
|
const dspace = {
|
||||||
|
originalRequest: {
|
||||||
|
headers: req.headers,
|
||||||
|
body: req.body,
|
||||||
|
method: req.method,
|
||||||
|
params: req.params,
|
||||||
|
reportProgress: req.reportProgress,
|
||||||
|
withCredentials: req.withCredentials,
|
||||||
|
responseType: req.responseType,
|
||||||
|
urlWithParams: req.urlWithParams
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// callback function for the case when SSR throws an error.
|
||||||
|
function onHandleError(parentZoneDelegate, currentZone, targetZone, error) {
|
||||||
|
if (!res._headerSent) {
|
||||||
|
console.warn('Error in SSR, serving for direct CSR. Error details : ', error);
|
||||||
|
res.sendFile('index.csr.ejs', {
|
||||||
|
root: DIST_FOLDER,
|
||||||
|
scripts: `<script>window.dspace = ${JSON.stringify(dspace)}</script>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (environment.universal.preboot) {
|
||||||
|
// If preboot is enabled, create a new zone for SSR, and
|
||||||
|
// register the error handler for when it throws an error
|
||||||
|
Zone.current.fork({ name: 'CSR fallback', onHandleError }).run(() => {
|
||||||
|
res.render(DIST_FOLDER + '/index.html', {
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
preboot: environment.universal.preboot,
|
||||||
|
async: environment.universal.async,
|
||||||
|
time: environment.universal.time,
|
||||||
|
baseUrl: environment.ui.nameSpace,
|
||||||
|
originUrl: environment.ui.baseUrl,
|
||||||
|
requestUrl: req.originalUrl
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If preboot is disabled, just serve the client side ejs template and pass it the required
|
||||||
|
// variables
|
||||||
|
console.log('Universal off, serving for direct CSR');
|
||||||
|
res.render('index-csr.ejs', {
|
||||||
|
root: DIST_FOLDER,
|
||||||
|
scripts: `<script>window.dspace = ${JSON.stringify(dspace)}</script>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the ngApp callback function to handle incoming requests
|
||||||
|
app.get('*', ngApp);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback function for when the server has started
|
||||||
|
*/
|
||||||
|
function serverStarted() {
|
||||||
|
console.log(`[${new Date().toTimeString()}] Listening at ${environment.ui.baseUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create an HTTPS server with the configured port and host
|
||||||
|
* @param keys SSL credentials
|
||||||
|
*/
|
||||||
|
function createHttpsServer(keys) {
|
||||||
|
https.createServer({
|
||||||
|
key: keys.serviceKey,
|
||||||
|
cert: keys.certificate
|
||||||
|
}, app).listen(environment.ui.port, environment.ui.host, () => {
|
||||||
|
serverStarted();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If SSL is enabled
|
||||||
|
* - Read credentials from configuration files
|
||||||
|
* - Call script to start an HTTPS server with these credentials
|
||||||
|
* When SSL is disabled
|
||||||
|
* - Start an HTTP server on the configured port and host
|
||||||
|
*/
|
||||||
|
if (environment.ui.ssl) {
|
||||||
|
let serviceKey;
|
||||||
|
try {
|
||||||
|
serviceKey = fs.readFileSync('./config/ssl/key.pem');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Service key not found at ./config/ssl/key.pem');
|
||||||
|
}
|
||||||
|
|
||||||
|
let certificate;
|
||||||
|
try {
|
||||||
|
certificate = fs.readFileSync('./config/ssl/cert.pem');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Certificate not found at ./config/ssl/key.pem');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceKey && certificate) {
|
||||||
|
createHttpsServer({
|
||||||
|
serviceKey: serviceKey,
|
||||||
|
certificate: certificate
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
|
||||||
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||||
|
|
||||||
|
pem.createCertificate({
|
||||||
|
days: 1,
|
||||||
|
selfSigned: true
|
||||||
|
}, (error, keys) => {
|
||||||
|
createHttpsServer(keys);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app.listen(environment.ui.port, environment.ui.host, () => {
|
||||||
|
serverStarted();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
|
|||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { getAccessControlModulePath } from '../admin-routing.module';
|
import { getAccessControlModulePath } from '../admin-routing.module';
|
||||||
|
|
||||||
const GROUP_EDIT_PATH = 'groups';
|
export const GROUP_EDIT_PATH = 'groups';
|
||||||
|
|
||||||
export function getGroupEditPath(id: string) {
|
export function getGroupEditPath(id: string) {
|
||||||
return new URLCombiner(getAccessControlModulePath(), GROUP_EDIT_PATH, id).toString();
|
return new URLCombiner(getAccessControlModulePath(), GROUP_EDIT_PATH, id).toString();
|
||||||
|
@@ -14,6 +14,18 @@
|
|||||||
[formLayout]="formLayout"
|
[formLayout]="formLayout"
|
||||||
(cancel)="onCancel()"
|
(cancel)="onCancel()"
|
||||||
(submitForm)="onSubmit()">
|
(submitForm)="onSubmit()">
|
||||||
|
<button class="btn btn-light" [disabled]="!(canReset$ | async)">
|
||||||
|
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-light" [disabled]="!(canDelete$ | async)">
|
||||||
|
<i class="fa fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
||||||
|
</button>
|
||||||
|
<button *ngIf="!isImpersonated" class="btn btn-light" [disabled]="!(canImpersonate$ | async)" (click)="impersonate()">
|
||||||
|
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
||||||
|
</button>
|
||||||
|
<button *ngIf="isImpersonated" class="btn btn-light" (click)="stopImpersonating()">
|
||||||
|
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
|
||||||
|
</button>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
|
|
||||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||||
|
@@ -31,6 +31,8 @@ import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder
|
|||||||
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
|
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
|
||||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||||
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
||||||
|
import { AuthService } from '../../../../core/auth/auth.service';
|
||||||
|
import { AuthServiceStub } from '../../../../shared/testing/auth-service.stub';
|
||||||
|
|
||||||
describe('EPersonFormComponent', () => {
|
describe('EPersonFormComponent', () => {
|
||||||
let component: EPersonFormComponent;
|
let component: EPersonFormComponent;
|
||||||
@@ -40,6 +42,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
|
|
||||||
let mockEPeople;
|
let mockEPeople;
|
||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
|
let authService: AuthServiceStub;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
mockEPeople = [EPersonMock, EPersonMock2];
|
mockEPeople = [EPersonMock, EPersonMock2];
|
||||||
@@ -104,6 +107,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
};
|
};
|
||||||
builderService = getMockFormBuilderService();
|
builderService = getMockFormBuilderService();
|
||||||
translateService = getMockTranslateService();
|
translateService = getMockTranslateService();
|
||||||
|
authService = new AuthServiceStub();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
@@ -125,6 +129,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
{ provide: Store, useValue: {} },
|
{ provide: Store, useValue: {} },
|
||||||
{ provide: RemoteDataBuildService, useValue: {} },
|
{ provide: RemoteDataBuildService, useValue: {} },
|
||||||
{ provide: HALEndpointService, useValue: {} },
|
{ provide: HALEndpointService, useValue: {} },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
EPeopleRegistryComponent
|
EPeopleRegistryComponent
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -228,4 +233,40 @@ describe('EPersonFormComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('impersonate', () => {
|
||||||
|
let ePersonId;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(authService, 'impersonate').and.callThrough();
|
||||||
|
ePersonId = 'testEPersonId';
|
||||||
|
component.epersonInitial = Object.assign(new EPerson(), {
|
||||||
|
id: ePersonId
|
||||||
|
});
|
||||||
|
component.impersonate();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call authService.impersonate', () => {
|
||||||
|
expect(authService.impersonate).toHaveBeenCalledWith(ePersonId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set isImpersonated to true', () => {
|
||||||
|
expect(component.isImpersonated).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stopImpersonating', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(authService, 'stopImpersonatingAndRefresh').and.callThrough();
|
||||||
|
component.stopImpersonating();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call authService.stopImpersonatingAndRefresh', () => {
|
||||||
|
expect(authService.stopImpersonatingAndRefresh).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set isImpersonated to false', () => {
|
||||||
|
expect(component.isImpersonated).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -7,7 +7,7 @@ import {
|
|||||||
DynamicInputModel
|
DynamicInputModel
|
||||||
} from '@ng-dynamic-forms/core';
|
} from '@ng-dynamic-forms/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Subscription, combineLatest } from 'rxjs';
|
import { Subscription, combineLatest, of } from 'rxjs';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { RestResponse } from '../../../../core/cache/response.models';
|
import { RestResponse } from '../../../../core/cache/response.models';
|
||||||
@@ -22,6 +22,7 @@ import { hasValue } from '../../../../shared/empty.util';
|
|||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { AuthService } from '../../../../core/auth/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-eperson-form',
|
selector: 'ds-eperson-form',
|
||||||
@@ -105,6 +106,24 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@Output() cancelForm: EventEmitter<any> = new EventEmitter();
|
@Output() cancelForm: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable whether or not the admin is allowed to reset the EPerson's password
|
||||||
|
* TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false)
|
||||||
|
*/
|
||||||
|
canReset$: Observable<boolean> = of(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable whether or not the admin is allowed to delete the EPerson
|
||||||
|
* TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false)
|
||||||
|
*/
|
||||||
|
canDelete$: Observable<boolean> = of(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable whether or not the admin is allowed to impersonate the EPerson
|
||||||
|
* TODO: Initialize the observable once the REST API supports this (currently hardcoded to return true)
|
||||||
|
*/
|
||||||
|
canImpersonate$: Observable<boolean> = of(true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of subscriptions
|
* List of subscriptions
|
||||||
*/
|
*/
|
||||||
@@ -129,13 +148,22 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
epersonInitial: EPerson;
|
epersonInitial: EPerson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not this EPerson is currently being impersonated
|
||||||
|
*/
|
||||||
|
isImpersonated = false;
|
||||||
|
|
||||||
constructor(public epersonService: EPersonDataService,
|
constructor(public epersonService: EPersonDataService,
|
||||||
public groupsDataService: GroupDataService,
|
public groupsDataService: GroupDataService,
|
||||||
private formBuilderService: FormBuilderService,
|
private formBuilderService: FormBuilderService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private notificationsService: NotificationsService,) {
|
private notificationsService: NotificationsService,
|
||||||
|
private authService: AuthService) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||||
this.epersonInitial = eperson;
|
this.epersonInitial = eperson;
|
||||||
|
if (hasValue(eperson)) {
|
||||||
|
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,6 +392,22 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start impersonating the EPerson
|
||||||
|
*/
|
||||||
|
impersonate() {
|
||||||
|
this.authService.impersonate(this.epersonInitial.id);
|
||||||
|
this.isImpersonated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop impersonating the EPerson
|
||||||
|
*/
|
||||||
|
stopImpersonating() {
|
||||||
|
this.authService.stopImpersonatingAndRefresh();
|
||||||
|
this.isImpersonated = false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||||
*/
|
*/
|
||||||
|
@@ -4,6 +4,7 @@ import { NgModule } from '@angular/core';
|
|||||||
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
|
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { getRegistriesModulePath } from '../admin-routing.module';
|
import { getRegistriesModulePath } from '../admin-routing.module';
|
||||||
|
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
const BITSTREAMFORMATS_MODULE_PATH = 'bitstream-formats';
|
const BITSTREAMFORMATS_MODULE_PATH = 'bitstream-formats';
|
||||||
|
|
||||||
@@ -14,16 +15,28 @@ export function getBitstreamFormatsModulePath() {
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{path: 'metadata', component: MetadataRegistryComponent, data: {title: 'admin.registries.metadata.title'}},
|
|
||||||
{
|
{
|
||||||
path: 'metadata/:schemaName',
|
path: 'metadata',
|
||||||
component: MetadataSchemaComponent,
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
data: {title: 'admin.registries.schema.title'}
|
data: {title: 'admin.registries.metadata.title', breadcrumbKey: 'admin.registries.metadata'},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: MetadataRegistryComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':schemaName',
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
component: MetadataSchemaComponent,
|
||||||
|
data: {title: 'admin.registries.schema.title', breadcrumbKey: 'admin.registries.schema'}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: BITSTREAMFORMATS_MODULE_PATH,
|
path: BITSTREAMFORMATS_MODULE_PATH,
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
loadChildren: './bitstream-formats/bitstream-formats.module#BitstreamFormatsModule',
|
loadChildren: './bitstream-formats/bitstream-formats.module#BitstreamFormatsModule',
|
||||||
data: {title: 'admin.registries.bitstream-formats.title'}
|
data: {title: 'admin.registries.bitstream-formats.title', breadcrumbKey: 'admin.registries.bitstream-formats'}
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
|
@@ -4,6 +4,7 @@ import { BitstreamFormatsResolver } from './bitstream-formats.resolver';
|
|||||||
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
|
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
|
||||||
import { BitstreamFormatsComponent } from './bitstream-formats.component';
|
import { BitstreamFormatsComponent } from './bitstream-formats.component';
|
||||||
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
|
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
const BITSTREAMFORMAT_EDIT_PATH = ':id/edit';
|
const BITSTREAMFORMAT_EDIT_PATH = ':id/edit';
|
||||||
const BITSTREAMFORMAT_ADD_PATH = 'add';
|
const BITSTREAMFORMAT_ADD_PATH = 'add';
|
||||||
@@ -17,14 +18,18 @@ const BITSTREAMFORMAT_ADD_PATH = 'add';
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: BITSTREAMFORMAT_ADD_PATH,
|
path: BITSTREAMFORMAT_ADD_PATH,
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
component: AddBitstreamFormatComponent,
|
component: AddBitstreamFormatComponent,
|
||||||
|
data: {breadcrumbKey: 'admin.registries.bitstream-formats.create'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: BITSTREAMFORMAT_EDIT_PATH,
|
path: BITSTREAMFORMAT_EDIT_PATH,
|
||||||
component: EditBitstreamFormatComponent,
|
component: EditBitstreamFormatComponent,
|
||||||
resolve: {
|
resolve: {
|
||||||
bitstreamFormat: BitstreamFormatsResolver
|
bitstreamFormat: BitstreamFormatsResolver,
|
||||||
}
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
|
},
|
||||||
|
data: {breadcrumbKey: 'admin.registries.bitstream-formats.edit'}
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
@@ -11,7 +11,6 @@
|
|||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
|
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="(metadataSchemas | async)?.payload"
|
|
||||||
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
|
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
|
@@ -4,7 +4,7 @@ import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
|||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { map, take } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
import { zip } from 'rxjs/internal/observable/zip';
|
import { zip } from 'rxjs/internal/observable/zip';
|
||||||
@@ -12,6 +12,8 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
|||||||
import { Route, Router } from '@angular/router';
|
import { Route, Router } from '@angular/router';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||||
|
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||||
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-metadata-registry',
|
selector: 'ds-metadata-registry',
|
||||||
@@ -37,6 +39,11 @@ export class MetadataRegistryComponent {
|
|||||||
pageSize: 25
|
pageSize: 25
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the list of MetadataSchemas needs an update
|
||||||
|
*/
|
||||||
|
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
constructor(private registryService: RegistryService,
|
constructor(private registryService: RegistryService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -50,14 +57,17 @@ export class MetadataRegistryComponent {
|
|||||||
*/
|
*/
|
||||||
onPageChange(event) {
|
onPageChange(event) {
|
||||||
this.config.currentPage = event;
|
this.config.currentPage = event;
|
||||||
this.updateSchemas();
|
this.forceUpdateSchemas();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the list of schemas by fetching it from the rest api or cache
|
* Update the list of schemas by fetching it from the rest api or cache
|
||||||
*/
|
*/
|
||||||
private updateSchemas() {
|
private updateSchemas() {
|
||||||
this.metadataSchemas = this.registryService.getMetadataSchemas(this.config);
|
this.metadataSchemas = this.needsUpdate$.pipe(
|
||||||
|
filter((update) => update === true),
|
||||||
|
switchMap(() => this.registryService.getMetadataSchemas(toFindListOptions(this.config)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,8 +75,7 @@ export class MetadataRegistryComponent {
|
|||||||
* a new REST call
|
* a new REST call
|
||||||
*/
|
*/
|
||||||
public forceUpdateSchemas() {
|
public forceUpdateSchemas() {
|
||||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
this.needsUpdate$.next(true);
|
||||||
this.updateSchemas();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,6 +134,7 @@ export class MetadataRegistryComponent {
|
|||||||
* Delete all the selected metadata schemas
|
* Delete all the selected metadata schemas
|
||||||
*/
|
*/
|
||||||
deleteSchemas() {
|
deleteSchemas() {
|
||||||
|
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||||
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
||||||
(schemas) => {
|
(schemas) => {
|
||||||
const tasks$ = [];
|
const tasks$ = [];
|
||||||
|
@@ -21,7 +21,8 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
const registryServiceStub = {
|
const registryServiceStub = {
|
||||||
getActiveMetadataSchema: () => observableOf(undefined),
|
getActiveMetadataSchema: () => observableOf(undefined),
|
||||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
||||||
cancelEditMetadataSchema: () => {}
|
cancelEditMetadataSchema: () => {},
|
||||||
|
clearMetadataSchemaRequests: () => observableOf(undefined)
|
||||||
};
|
};
|
||||||
const formBuilderServiceStub = {
|
const formBuilderServiceStub = {
|
||||||
createFormGroup: () => {
|
createFormGroup: () => {
|
||||||
|
@@ -128,6 +128,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created schema using the EventEmitter submitForm
|
* Emit the updated/created schema using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
|
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||||
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
||||||
(schema) => {
|
(schema) => {
|
||||||
const values = {
|
const values = {
|
||||||
@@ -139,7 +140,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
this.submitForm.emit(newSchema);
|
this.submitForm.emit(newSchema);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), {
|
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
||||||
id: schema.id,
|
id: schema.id,
|
||||||
prefix: (values.prefix ? values.prefix : schema.prefix),
|
prefix: (values.prefix ? values.prefix : schema.prefix),
|
||||||
namespace: (values.namespace ? values.namespace : schema.namespace)
|
namespace: (values.namespace ? values.namespace : schema.namespace)
|
||||||
@@ -148,6 +149,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.clearFields();
|
this.clearFields();
|
||||||
|
this.registryService.cancelEditMetadataSchema();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -30,6 +30,7 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
||||||
cancelEditMetadataField: () => {},
|
cancelEditMetadataField: () => {},
|
||||||
cancelEditMetadataSchema: () => {},
|
cancelEditMetadataSchema: () => {},
|
||||||
|
clearMetadataFieldRequests: () => observableOf(undefined)
|
||||||
};
|
};
|
||||||
const formBuilderServiceStub = {
|
const formBuilderServiceStub = {
|
||||||
createFormGroup: () => {
|
createFormGroup: () => {
|
||||||
|
@@ -153,6 +153,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created field using the EventEmitter submitForm
|
* Emit the updated/created field using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
|
this.registryService.clearMetadataFieldRequests().subscribe();
|
||||||
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
||||||
(field) => {
|
(field) => {
|
||||||
const values = {
|
const values = {
|
||||||
@@ -166,7 +167,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
this.submitForm.emit(newField);
|
this.submitForm.emit(newField);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), {
|
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), field, {
|
||||||
id: field.id,
|
id: field.id,
|
||||||
schema: this.metadataSchema,
|
schema: this.metadataSchema,
|
||||||
element: (values.element ? values.element : field.element),
|
element: (values.element ? values.element : field.element),
|
||||||
@@ -177,6 +178,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.clearFields();
|
this.clearFields();
|
||||||
|
this.registryService.cancelEditMetadataField();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,36 +1,37 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="metadata-schema row">
|
<div class="metadata-schema row">
|
||||||
<div class="col-12">
|
<div class="col-12" *ngVar="(metadataSchema$ | async) as schema">
|
||||||
|
|
||||||
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.schema.head' | translate}}: "{{(metadataSchema | async)?.payload?.prefix}}"</h2>
|
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.schema.head' | translate}}: "{{schema?.prefix}}"</h2>
|
||||||
|
|
||||||
<p id="description" class="pb-2">{{'admin.registries.schema.description' | translate:namespace }}</p>
|
<p id="description" class="pb-2">{{'admin.registries.schema.description' | translate:{ namespace: schema?.namespace } }}</p>
|
||||||
|
|
||||||
<ds-metadata-field-form
|
<ds-metadata-field-form
|
||||||
[metadataSchema]="(metadataSchema | async)?.payload"
|
[metadataSchema]="schema"
|
||||||
(submitForm)="forceUpdateFields()"></ds-metadata-field-form>
|
(submitForm)="forceUpdateFields()"></ds-metadata-field-form>
|
||||||
|
|
||||||
<h3>{{'admin.registries.schema.fields.head' | translate}}</h3>
|
<h3>{{'admin.registries.schema.fields.head' | translate}}</h3>
|
||||||
|
|
||||||
<ds-pagination
|
<ng-container *ngVar="(metadataFields$ | async)?.payload as fields">
|
||||||
*ngIf="(metadataFields | async)?.payload?.totalElements > 0"
|
<ds-pagination
|
||||||
[paginationOptions]="config"
|
*ngIf="fields?.totalElements > 0"
|
||||||
[pageInfoState]="(metadataFields | async)?.payload"
|
[paginationOptions]="config"
|
||||||
[collectionSize]="(metadataFields | async)?.payload?.totalElements"
|
[pageInfoState]="fields"
|
||||||
[hideGear]="false"
|
[collectionSize]="fields?.totalElements"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hideGear]="false"
|
||||||
(pageChange)="onPageChange($event)">
|
[hidePagerWhenSinglePage]="true"
|
||||||
<div class="table-responsive">
|
(pageChange)="onPageChange($event)">
|
||||||
<table id="metadata-fields" class="table table-striped table-hover">
|
<div class="table-responsive">
|
||||||
<thead>
|
<table id="metadata-fields" class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
|
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
|
||||||
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
|
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let field of (metadataFields | async)?.payload?.page"
|
<tr *ngFor="let field of fields?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
[ngClass]="{'table-primary' : isActive(field) | async}">
|
||||||
<td>
|
<td>
|
||||||
<label>
|
<label>
|
||||||
@@ -39,22 +40,23 @@
|
|||||||
(change)="selectMetadataField(field, $event)">
|
(change)="selectMetadataField(field, $event)">
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{(metadataSchema | async)?.payload?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
</ds-pagination>
|
||||||
|
|
||||||
|
<div *ngIf="fields?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||||
|
{{'admin.registries.schema.fields.no-items' | translate}}
|
||||||
</div>
|
</div>
|
||||||
</ds-pagination>
|
|
||||||
|
|
||||||
<div *ngIf="(metadataFields | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
<div>
|
||||||
{{'admin.registries.schema.fields.no-items' | translate}}
|
<button [routerLink]="['/admin/registries/metadata']" class="btn btn-primary">{{'admin.registries.schema.return' | translate}}</button>
|
||||||
</div>
|
<button *ngIf="fields?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
|
||||||
|
</div>
|
||||||
<div>
|
</ng-container>
|
||||||
<button [routerLink]="['/admin/registries/metadata']" class="btn btn-primary">{{'admin.registries.schema.return' | translate}}</button>
|
|
||||||
<button *ngIf="(metadataFields | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -22,6 +22,7 @@ import { RestResponse } from '../../../core/cache/response.models';
|
|||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
|
|
||||||
describe('MetadataSchemaComponent', () => {
|
describe('MetadataSchemaComponent', () => {
|
||||||
let comp: MetadataSchemaComponent;
|
let comp: MetadataSchemaComponent;
|
||||||
@@ -124,7 +125,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||||
declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe],
|
declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryServiceStub },
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
@@ -5,7 +5,7 @@ import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
|||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { map, take } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
import { zip } from 'rxjs/internal/observable/zip';
|
import { zip } from 'rxjs/internal/observable/zip';
|
||||||
@@ -13,6 +13,10 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||||
|
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||||
|
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||||
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
|
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-metadata-schema',
|
selector: 'ds-metadata-schema',
|
||||||
@@ -24,21 +28,15 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
|||||||
* The admin can create, edit or delete metadata fields here.
|
* The admin can create, edit or delete metadata fields here.
|
||||||
*/
|
*/
|
||||||
export class MetadataSchemaComponent implements OnInit {
|
export class MetadataSchemaComponent implements OnInit {
|
||||||
|
|
||||||
/**
|
|
||||||
* The namespace of the metadata schema
|
|
||||||
*/
|
|
||||||
namespace;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The metadata schema
|
* The metadata schema
|
||||||
*/
|
*/
|
||||||
metadataSchema: Observable<RemoteData<MetadataSchema>>;
|
metadataSchema$: Observable<MetadataSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all the fields attached to this metadata schema
|
* A list of all the fields attached to this metadata schema
|
||||||
*/
|
*/
|
||||||
metadataFields: Observable<RemoteData<PaginatedList<MetadataField>>>;
|
metadataFields$: Observable<RemoteData<PaginatedList<MetadataField>>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination config used to display the list of metadata fields
|
* Pagination config used to display the list of metadata fields
|
||||||
@@ -49,6 +47,11 @@ export class MetadataSchemaComponent implements OnInit {
|
|||||||
pageSizeOptions: [25, 50, 100, 200]
|
pageSizeOptions: [25, 50, 100, 200]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the list of MetadataFields needs an update
|
||||||
|
*/
|
||||||
|
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
constructor(private registryService: RegistryService,
|
constructor(private registryService: RegistryService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
@@ -68,7 +71,7 @@ export class MetadataSchemaComponent implements OnInit {
|
|||||||
* @param params
|
* @param params
|
||||||
*/
|
*/
|
||||||
initialize(params) {
|
initialize(params) {
|
||||||
this.metadataSchema = this.registryService.getMetadataSchemaByName(params.schemaName);
|
this.metadataSchema$ = this.registryService.getMetadataSchemaByName(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||||
this.updateFields();
|
this.updateFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,18 +81,20 @@ export class MetadataSchemaComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
onPageChange(event) {
|
onPageChange(event) {
|
||||||
this.config.currentPage = event;
|
this.config.currentPage = event;
|
||||||
this.updateFields();
|
this.forceUpdateFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the list of fields by fetching it from the rest api or cache
|
* Update the list of fields by fetching it from the rest api or cache
|
||||||
*/
|
*/
|
||||||
private updateFields() {
|
private updateFields() {
|
||||||
this.metadataSchema.subscribe((schemaData) => {
|
this.metadataFields$ = combineLatest(this.metadataSchema$, this.needsUpdate$).pipe(
|
||||||
const schema = schemaData.payload;
|
switchMap(([schema, update]: [MetadataSchema, boolean]) => {
|
||||||
this.metadataFields = this.registryService.getMetadataFieldsBySchema(schema, this.config);
|
if (update) {
|
||||||
this.namespace = {namespace: schemaData.payload.namespace};
|
return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config));
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,8 +102,7 @@ export class MetadataSchemaComponent implements OnInit {
|
|||||||
* a new REST call
|
* a new REST call
|
||||||
*/
|
*/
|
||||||
public forceUpdateFields() {
|
public forceUpdateFields() {
|
||||||
this.registryService.clearMetadataFieldRequests().subscribe();
|
this.needsUpdate$.next(true);
|
||||||
this.updateFields();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,6 +161,7 @@ export class MetadataSchemaComponent implements OnInit {
|
|||||||
* Delete all the selected metadata fields
|
* Delete all the selected metadata fields
|
||||||
*/
|
*/
|
||||||
deleteFields() {
|
deleteFields() {
|
||||||
|
this.registryService.clearMetadataFieldRequests().subscribe();
|
||||||
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
|
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
|
||||||
(fields) => {
|
(fields) => {
|
||||||
const tasks$ = [];
|
const tasks$ = [];
|
||||||
|
@@ -3,10 +3,12 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { getAdminModulePath } from '../app-routing.module';
|
import { getAdminModulePath } from '../app-routing.module';
|
||||||
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
|
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
|
||||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
||||||
|
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
|
|
||||||
const REGISTRIES_MODULE_PATH = 'registries';
|
const REGISTRIES_MODULE_PATH = 'registries';
|
||||||
const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
export const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
||||||
|
|
||||||
export function getRegistriesModulePath() {
|
export function getRegistriesModulePath() {
|
||||||
return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString();
|
return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString();
|
||||||
@@ -32,8 +34,18 @@ export function getAccessControlModulePath() {
|
|||||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
component: AdminSearchPageComponent,
|
component: AdminSearchPageComponent,
|
||||||
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }
|
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }
|
||||||
}
|
},
|
||||||
]),
|
{
|
||||||
|
path: 'workflow',
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
component: AdminWorkflowPageComponent,
|
||||||
|
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }
|
||||||
|
},
|
||||||
|
])
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
I18nBreadcrumbResolver,
|
||||||
|
I18nBreadcrumbsService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AdminRoutingModule {
|
export class AdminRoutingModule {
|
||||||
|
@@ -445,6 +445,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
icon: 'cogs',
|
icon: 'cogs',
|
||||||
index: 9
|
index: 9
|
||||||
},
|
},
|
||||||
|
/* Processes */
|
||||||
{
|
{
|
||||||
id: 'processes',
|
id: 'processes',
|
||||||
active: false,
|
active: false,
|
||||||
@@ -457,6 +458,19 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
icon: 'terminal',
|
icon: 'terminal',
|
||||||
index: 10
|
index: 10
|
||||||
},
|
},
|
||||||
|
/* Workflow */
|
||||||
|
{
|
||||||
|
id: 'workflow',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.workflow',
|
||||||
|
link: '/admin/workflow'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'user-check',
|
||||||
|
index: 10
|
||||||
|
},
|
||||||
];
|
];
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
||||||
|
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
<ds-configuration-search-page configuration="workflowAdmin" [context]="context"></ds-configuration-search-page>
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AdminWorkflowPageComponent } from './admin-workflow-page.component';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
|
||||||
|
describe('AdminSearchPageComponent', () => {
|
||||||
|
let component: AdminWorkflowPageComponent;
|
||||||
|
let fixture: ComponentFixture<AdminWorkflowPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ AdminWorkflowPageComponent ],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AdminWorkflowPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,18 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Context } from '../../core/shared/context.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-admin-workflow-page',
|
||||||
|
templateUrl: './admin-workflow-page.component.html',
|
||||||
|
styleUrls: ['./admin-workflow-page.component.scss']
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents a workflow item search page for administrators
|
||||||
|
*/
|
||||||
|
export class AdminWorkflowPageComponent {
|
||||||
|
/**
|
||||||
|
* The context of this page
|
||||||
|
*/
|
||||||
|
context: Context = Context.AdminWorkflowSearch;
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
<ng-template dsListableObject>
|
||||||
|
</ng-template>
|
||||||
|
<div #badges class="position-absolute ml-1">
|
||||||
|
<div class="workflow-badge">
|
||||||
|
<span class="badge badge-info">{{ "admin.workflow.item.workflow" | translate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul #buttons class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<ds-workflow-item-admin-workflow-actions-element *ngIf="object" class="d-flex justify-content-between" [wfi]="dso" [small]="true"></ds-workflow-item-admin-workflow-actions-element>
|
||||||
|
</li>
|
||||||
|
</ul>
|
@@ -0,0 +1,84 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './workflow-item-search-result-admin-workflow-grid-element.component';
|
||||||
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { PublicationGridElementComponent } from '../../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component';
|
||||||
|
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||||
|
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||||
|
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
|
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
||||||
|
|
||||||
|
describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
|
||||||
|
let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent;
|
||||||
|
let fixture: ComponentFixture<WorkflowItemSearchResultAdminWorkflowGridElementComponent>;
|
||||||
|
let id;
|
||||||
|
let wfi;
|
||||||
|
let itemRD$;
|
||||||
|
let linkService;
|
||||||
|
let object;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
itemRD$ = createSuccessfulRemoteDataObject$(new Item());
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
object = new WorkflowItemSearchResult()
|
||||||
|
wfi = new WorkflowItem();
|
||||||
|
wfi.item = itemRD$;
|
||||||
|
object.indexableObject = wfi;
|
||||||
|
linkService = getMockLinkService();
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule(
|
||||||
|
{
|
||||||
|
declarations: [WorkflowItemSearchResultAdminWorkflowGridElementComponent, PublicationGridElementComponent, ListableObjectDirective],
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: TruncatableService, useValue: {} },
|
||||||
|
{ provide: BitstreamDataService, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.overrideComponent(WorkflowItemSearchResultAdminWorkflowGridElementComponent, {
|
||||||
|
set: {
|
||||||
|
entryComponents: [PublicationGridElementComponent]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
linkService.resolveLink.and.callFake((a) => a);
|
||||||
|
fixture = TestBed.createComponent(WorkflowItemSearchResultAdminWorkflowGridElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = object;
|
||||||
|
component.linkTypes = CollectionElementLinkType;
|
||||||
|
component.index = 0;
|
||||||
|
component.viewModes = ViewMode;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve the item using the link service', () => {
|
||||||
|
expect(linkService.resolveLink).toHaveBeenCalledWith(wfi, followLink('item'));
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,98 @@
|
|||||||
|
import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { getListableObjectComponent, listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
|
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||||
|
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
|
||||||
|
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||||
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
|
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||||
|
|
||||||
|
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-workflow-item-search-result-admin-workflow-grid-element',
|
||||||
|
styleUrls: ['./workflow-item-search-result-admin-workflow-grid-element.component.scss'],
|
||||||
|
templateUrl: './workflow-item-search-result-admin-workflow-grid-element.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a grid element for an workflow item on the admin workflow search page
|
||||||
|
*/
|
||||||
|
export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent<WorkflowItemSearchResult, WorkflowItem> {
|
||||||
|
/**
|
||||||
|
* Directive used to render the dynamic component in
|
||||||
|
*/
|
||||||
|
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The html child that contains the badges html
|
||||||
|
*/
|
||||||
|
@ViewChild('badges', { static: true }) badges: ElementRef;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The html child that contains the button html
|
||||||
|
*/
|
||||||
|
@ViewChild('buttons', { static: true }) buttons: ElementRef;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item linked to the workflow item
|
||||||
|
*/
|
||||||
|
public item$: Observable<Item>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private componentFactoryResolver: ComponentFactoryResolver,
|
||||||
|
private linkService: LinkService,
|
||||||
|
protected truncatableService: TruncatableService,
|
||||||
|
protected bitstreamDataService: BitstreamDataService
|
||||||
|
) {
|
||||||
|
super(truncatableService, bitstreamDataService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the dynamic child component
|
||||||
|
* Initialize the item object from the workflow item
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.dso = this.linkService.resolveLink(this.dso, followLink('item'));
|
||||||
|
this.item$ = (this.dso.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload());
|
||||||
|
this.item$.pipe(take(1)).subscribe((item: Item) => {
|
||||||
|
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item));
|
||||||
|
|
||||||
|
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
||||||
|
viewContainerRef.clear();
|
||||||
|
|
||||||
|
const componentRef = viewContainerRef.createComponent(
|
||||||
|
componentFactory,
|
||||||
|
0,
|
||||||
|
undefined,
|
||||||
|
[
|
||||||
|
[this.badges.nativeElement],
|
||||||
|
[this.buttons.nativeElement]
|
||||||
|
]);
|
||||||
|
(componentRef.instance as any).object = item;
|
||||||
|
(componentRef.instance as any).index = this.index;
|
||||||
|
(componentRef.instance as any).linkType = this.linkType;
|
||||||
|
(componentRef.instance as any).listID = this.listID;
|
||||||
|
componentRef.changeDetectorRef.detectChanges();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the component depending on the item's relationship type, view mode and context
|
||||||
|
* @returns {GenericConstructor<Component>}
|
||||||
|
*/
|
||||||
|
private getComponent(item: Item): GenericConstructor<Component> {
|
||||||
|
return getListableObjectComponent(item.getRenderTypes(), ViewMode.GridElement, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="workflow-badge">
|
||||||
|
<span class="badge badge-info">{{ "admin.workflow.item.workflow" | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<ds-listable-object-component-loader *ngIf="item$ | async"
|
||||||
|
[object]="item$ | async"
|
||||||
|
[viewMode]="viewModes.ListElement"
|
||||||
|
[index]="index"
|
||||||
|
[linkType]="linkType"
|
||||||
|
[listID]="listID"></ds-listable-object-component-loader>
|
||||||
|
<ds-workflow-item-admin-workflow-actions-element [wfi]="dso" [small]="false"></ds-workflow-item-admin-workflow-actions-element>
|
@@ -0,0 +1,76 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
|
import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './workflow-item-search-result-admin-workflow-list-element.component';
|
||||||
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
|
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
||||||
|
|
||||||
|
describe('WorkflowItemAdminWorkflowListElementComponent', () => {
|
||||||
|
let component: WorkflowItemSearchResultAdminWorkflowListElementComponent;
|
||||||
|
let fixture: ComponentFixture<WorkflowItemSearchResultAdminWorkflowListElementComponent>;
|
||||||
|
let id;
|
||||||
|
let wfi;
|
||||||
|
let itemRD$;
|
||||||
|
let linkService;
|
||||||
|
let object;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
itemRD$ = createSuccessfulRemoteDataObject$(new Item());
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
object = new WorkflowItemSearchResult()
|
||||||
|
wfi = new WorkflowItem();
|
||||||
|
wfi.item = itemRD$;
|
||||||
|
object.indexableObject = wfi;
|
||||||
|
linkService = getMockLinkService();
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule(
|
||||||
|
{
|
||||||
|
declarations: [WorkflowItemSearchResultAdminWorkflowListElementComponent],
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
linkService.resolveLink.and.callFake((a) => a);
|
||||||
|
fixture = TestBed.createComponent(WorkflowItemSearchResultAdminWorkflowListElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = object;
|
||||||
|
component.linkTypes = CollectionElementLinkType;
|
||||||
|
component.index = 0;
|
||||||
|
component.viewModes = ViewMode;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve the item using the link service', () => {
|
||||||
|
expect(linkService.resolveLink).toHaveBeenCalledWith(wfi, followLink('item'));
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,44 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
|
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||||
|
|
||||||
|
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-workflow-item-search-result-admin-workflow-list-element',
|
||||||
|
styleUrls: ['./workflow-item-search-result-admin-workflow-list-element.component.scss'],
|
||||||
|
templateUrl: './workflow-item-search-result-admin-workflow-list-element.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a list element for an workflow item on the admin workflow search page
|
||||||
|
*/
|
||||||
|
export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends SearchResultListElementComponent<WorkflowItemSearchResult, WorkflowItem> implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item linked to the workflow item
|
||||||
|
*/
|
||||||
|
public item$: Observable<Item>;
|
||||||
|
|
||||||
|
constructor(private linkService: LinkService, protected truncatableService: TruncatableService) {
|
||||||
|
super(truncatableService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the item object from the workflow item
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.dso = this.linkService.resolveLink(this.dso, followLink('item'));
|
||||||
|
this.item$ = (this.dso.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload());
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeletePath()]" [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>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 send-back-link" [routerLink]="[getSendBackPath()]" [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>
|
||||||
|
</a>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,68 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import {
|
||||||
|
ITEM_EDIT_DELETE_PATH,
|
||||||
|
ITEM_EDIT_MOVE_PATH,
|
||||||
|
ITEM_EDIT_PRIVATE_PATH,
|
||||||
|
ITEM_EDIT_PUBLIC_PATH,
|
||||||
|
ITEM_EDIT_REINSTATE_PATH,
|
||||||
|
ITEM_EDIT_WITHDRAW_PATH
|
||||||
|
} from '../../../+item-page/edit-item-page/edit-item-page.routing.module';
|
||||||
|
import { getItemEditPath } from '../../../+item-page/item-page-routing.module';
|
||||||
|
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||||
|
import { WorkflowItemAdminWorkflowActionsComponent } from './workflow-item-admin-workflow-actions.component';
|
||||||
|
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
|
||||||
|
import { getWorkflowItemDeletePath, getWorkflowItemSendBackPath } from '../../../+workflowitems-edit-page/workflowitems-edit-page-routing.module';
|
||||||
|
|
||||||
|
describe('WorkflowItemAdminWorkflowActionsComponent', () => {
|
||||||
|
let component: WorkflowItemAdminWorkflowActionsComponent;
|
||||||
|
let fixture: ComponentFixture<WorkflowItemAdminWorkflowActionsComponent>;
|
||||||
|
let id;
|
||||||
|
let wfi;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
wfi = new WorkflowItem();
|
||||||
|
wfi.id = id;
|
||||||
|
}
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [WorkflowItemAdminWorkflowActionsComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(WorkflowItemAdminWorkflowActionsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.wfi = wfi;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a delete button with the correct link', () => {
|
||||||
|
const button = fixture.debugElement.query(By.css('a.delete-link'));
|
||||||
|
const link = button.nativeElement.href;
|
||||||
|
expect(link).toContain(new URLCombiner(getWorkflowItemDeletePath(wfi.id)).toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a move button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.send-back-link'));
|
||||||
|
const link = a.nativeElement.href;
|
||||||
|
expect(link).toContain(new URLCombiner(getWorkflowItemSendBackPath(wfi.id)).toString());
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,39 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
|
||||||
|
import { getWorkflowItemDeletePath, getWorkflowItemSendBackPath } from '../../../+workflowitems-edit-page/workflowitems-edit-page-routing.module';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-workflow-item-admin-workflow-actions-element',
|
||||||
|
styleUrls: ['./workflow-item-admin-workflow-actions.component.scss'],
|
||||||
|
templateUrl: './workflow-item-admin-workflow-actions.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying the actions for a list element for an item on the admin workflow search page
|
||||||
|
*/
|
||||||
|
export class WorkflowItemAdminWorkflowActionsComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The workflow item to perform the actions on
|
||||||
|
*/
|
||||||
|
@Input() public wfi: WorkflowItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to use small buttons
|
||||||
|
*/
|
||||||
|
@Input() public small: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the delete page of this workflow item
|
||||||
|
*/
|
||||||
|
getDeletePath(): string {
|
||||||
|
|
||||||
|
return getWorkflowItemDeletePath(this.wfi.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the send back page of this workflow item
|
||||||
|
*/
|
||||||
|
getSendBackPath(): string {
|
||||||
|
return getWorkflowItemSendBackPath(this.wfi.id);
|
||||||
|
}
|
||||||
|
}
|
@@ -12,6 +12,10 @@ import { ItemAdminSearchResultGridElementComponent } from './admin-search-page/a
|
|||||||
import { CommunityAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component';
|
import { CommunityAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component';
|
||||||
import { CollectionAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component';
|
import { CollectionAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component';
|
||||||
import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin-search-results/item-admin-search-result-actions.component';
|
import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin-search-results/item-admin-search-result-actions.component';
|
||||||
|
import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component';
|
||||||
|
import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component';
|
||||||
|
import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component';
|
||||||
|
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -23,13 +27,19 @@ import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AdminSearchPageComponent,
|
AdminSearchPageComponent,
|
||||||
|
AdminWorkflowPageComponent,
|
||||||
ItemAdminSearchResultListElementComponent,
|
ItemAdminSearchResultListElementComponent,
|
||||||
CommunityAdminSearchResultListElementComponent,
|
CommunityAdminSearchResultListElementComponent,
|
||||||
CollectionAdminSearchResultListElementComponent,
|
CollectionAdminSearchResultListElementComponent,
|
||||||
ItemAdminSearchResultGridElementComponent,
|
ItemAdminSearchResultGridElementComponent,
|
||||||
CommunityAdminSearchResultGridElementComponent,
|
CommunityAdminSearchResultGridElementComponent,
|
||||||
CollectionAdminSearchResultGridElementComponent,
|
CollectionAdminSearchResultGridElementComponent,
|
||||||
ItemAdminSearchResultActionsComponent
|
ItemAdminSearchResultActionsComponent,
|
||||||
|
|
||||||
|
WorkflowItemSearchResultAdminWorkflowListElementComponent,
|
||||||
|
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
|
||||||
|
WorkflowItemAdminWorkflowActionsComponent
|
||||||
|
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
ItemAdminSearchResultListElementComponent,
|
ItemAdminSearchResultListElementComponent,
|
||||||
@@ -38,7 +48,11 @@ import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin
|
|||||||
ItemAdminSearchResultGridElementComponent,
|
ItemAdminSearchResultGridElementComponent,
|
||||||
CommunityAdminSearchResultGridElementComponent,
|
CommunityAdminSearchResultGridElementComponent,
|
||||||
CollectionAdminSearchResultGridElementComponent,
|
CollectionAdminSearchResultGridElementComponent,
|
||||||
ItemAdminSearchResultActionsComponent
|
ItemAdminSearchResultActionsComponent,
|
||||||
|
|
||||||
|
WorkflowItemSearchResultAdminWorkflowListElementComponent,
|
||||||
|
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
|
||||||
|
WorkflowItemAdminWorkflowActionsComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AdminModule {
|
export class AdminModule {
|
||||||
|
@@ -5,7 +5,6 @@ import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
|
|||||||
import { EditItemPageComponent } from './edit-item-page.component';
|
import { EditItemPageComponent } from './edit-item-page.component';
|
||||||
import { ItemStatusComponent } from './item-status/item-status.component';
|
import { ItemStatusComponent } from './item-status/item-status.component';
|
||||||
import { ItemOperationComponent } from './item-operation/item-operation.component';
|
import { ItemOperationComponent } from './item-operation/item-operation.component';
|
||||||
import { ModifyItemOverviewComponent } from './modify-item-overview/modify-item-overview.component';
|
|
||||||
import { ItemWithdrawComponent } from './item-withdraw/item-withdraw.component';
|
import { ItemWithdrawComponent } from './item-withdraw/item-withdraw.component';
|
||||||
import { ItemReinstateComponent } from './item-reinstate/item-reinstate.component';
|
import { ItemReinstateComponent } from './item-reinstate/item-reinstate.component';
|
||||||
import { AbstractSimpleItemActionComponent } from './simple-item-action/abstract-simple-item-action.component';
|
import { AbstractSimpleItemActionComponent } from './simple-item-action/abstract-simple-item-action.component';
|
||||||
@@ -30,6 +29,9 @@ import { ItemEditBitstreamDragHandleComponent } from './item-bitstreams/item-edi
|
|||||||
import { PaginatedDragAndDropBitstreamListComponent } from './item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component';
|
import { PaginatedDragAndDropBitstreamListComponent } from './item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component';
|
||||||
import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component';
|
import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component';
|
||||||
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
||||||
|
import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component';
|
||||||
|
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
|
||||||
|
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module that contains all components related to the Edit Item page administrator functionality
|
* Module that contains all components related to the Edit Item page administrator functionality
|
||||||
@@ -47,7 +49,6 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version
|
|||||||
ItemOperationComponent,
|
ItemOperationComponent,
|
||||||
AbstractSimpleItemActionComponent,
|
AbstractSimpleItemActionComponent,
|
||||||
AbstractItemUpdateComponent,
|
AbstractItemUpdateComponent,
|
||||||
ModifyItemOverviewComponent,
|
|
||||||
ItemWithdrawComponent,
|
ItemWithdrawComponent,
|
||||||
ItemReinstateComponent,
|
ItemReinstateComponent,
|
||||||
ItemPrivateComponent,
|
ItemPrivateComponent,
|
||||||
@@ -69,6 +70,9 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version
|
|||||||
ItemMoveComponent,
|
ItemMoveComponent,
|
||||||
ItemEditBitstreamDragHandleComponent,
|
ItemEditBitstreamDragHandleComponent,
|
||||||
VirtualMetadataComponent,
|
VirtualMetadataComponent,
|
||||||
|
ItemAuthorizationsComponent,
|
||||||
|
ResourcePolicyEditComponent,
|
||||||
|
ResourcePolicyCreateComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
BundleDataService
|
BundleDataService
|
||||||
|
@@ -14,6 +14,12 @@ import { ItemMoveComponent } from './item-move/item-move.component';
|
|||||||
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
||||||
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
||||||
|
import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component';
|
||||||
|
import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver';
|
||||||
|
import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
|
||||||
|
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
|
||||||
|
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
|
||||||
|
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
|
|
||||||
export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
||||||
export const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
export const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
||||||
@@ -21,6 +27,7 @@ export const ITEM_EDIT_PRIVATE_PATH = 'private';
|
|||||||
export const ITEM_EDIT_PUBLIC_PATH = 'public';
|
export const ITEM_EDIT_PUBLIC_PATH = 'public';
|
||||||
export const ITEM_EDIT_DELETE_PATH = 'delete';
|
export const ITEM_EDIT_DELETE_PATH = 'delete';
|
||||||
export const ITEM_EDIT_MOVE_PATH = 'move';
|
export const ITEM_EDIT_MOVE_PATH = 'move';
|
||||||
|
export const ITEM_EDIT_AUTHORIZATIONS_PATH = 'authorizations';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing module that handles the routing for the Edit Item page administrator functionality
|
* Routing module that handles the routing for the Edit Item page administrator functionality
|
||||||
@@ -111,12 +118,43 @@ export const ITEM_EDIT_MOVE_PATH = 'move';
|
|||||||
path: ITEM_EDIT_MOVE_PATH,
|
path: ITEM_EDIT_MOVE_PATH,
|
||||||
component: ItemMoveComponent,
|
component: ItemMoveComponent,
|
||||||
data: { title: 'item.edit.move.title' },
|
data: { title: 'item.edit.move.title' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ITEM_EDIT_AUTHORIZATIONS_PATH,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
resolve: {
|
||||||
|
resourcePolicyTarget: ResourcePolicyTargetResolver
|
||||||
|
},
|
||||||
|
component: ResourcePolicyCreateComponent,
|
||||||
|
data: { title: 'resource-policies.create.page.title' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit',
|
||||||
|
resolve: {
|
||||||
|
resourcePolicy: ResourcePolicyResolver
|
||||||
|
},
|
||||||
|
component: ResourcePolicyEditComponent,
|
||||||
|
data: { title: 'resource-policies.edit.page.title' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ItemAuthorizationsComponent,
|
||||||
|
data: { title: 'item.edit.authorizations.title' }
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: []
|
providers: [
|
||||||
|
I18nBreadcrumbResolver,
|
||||||
|
I18nBreadcrumbsService,
|
||||||
|
ResourcePolicyResolver,
|
||||||
|
ResourcePolicyTargetResolver
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class EditItemPageRoutingModule {
|
export class EditItemPageRoutingModule {
|
||||||
|
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
<div class="container">
|
||||||
|
<ds-alert [type]="'alert-info'" [content]="'item.edit.authorizations.heading'"></ds-alert>
|
||||||
|
<ds-resource-policies [resourceType]="'item'" [resourceUUID]="(getItemUUID() | async)"></ds-resource-policies>
|
||||||
|
<ng-container *ngFor="let bundle of (getItemBundles() | async); trackById">
|
||||||
|
<ds-resource-policies [resourceType]="'bundle'"
|
||||||
|
[resourceUUID]="bundle.id"></ds-resource-policies>
|
||||||
|
<ng-container *ngFor="let bitstream of (bundleBitstreamsMap.get(bundle.id) | async)?.page; trackById">
|
||||||
|
<ds-resource-policies [resourceType]="'bitstream'"
|
||||||
|
[resourceUUID]="bitstream.id"></ds-resource-policies>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
@@ -0,0 +1,183 @@
|
|||||||
|
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
|
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
|
|
||||||
|
import { ItemAuthorizationsComponent } from './item-authorizations.component';
|
||||||
|
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||||
|
import { Bundle } from '../../../core/shared/bundle.model';
|
||||||
|
import { createMockRDPaginatedObs } from '../item-bitstreams/item-bitstreams.component.spec';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { LinkService } from '../../../core/cache/builders/link.service';
|
||||||
|
import { getMockLinkService } from '../../../shared/mocks/link-service.mock';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||||
|
import { createTestComponent } from '../../../shared/testing/utils.test';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
|
|
||||||
|
describe('ItemAuthorizationsComponent test suite', () => {
|
||||||
|
let comp: ItemAuthorizationsComponent;
|
||||||
|
let compAsAny: any;
|
||||||
|
let fixture: ComponentFixture<ItemAuthorizationsComponent>;
|
||||||
|
let de;
|
||||||
|
|
||||||
|
const linkService: any = getMockLinkService();
|
||||||
|
|
||||||
|
const bitstream1 = Object.assign(new Bitstream(), {
|
||||||
|
id: 'bitstream1',
|
||||||
|
uuid: 'bitstream1'
|
||||||
|
});
|
||||||
|
const bitstream2 = Object.assign(new Bitstream(), {
|
||||||
|
id: 'bitstream2',
|
||||||
|
uuid: 'bitstream2'
|
||||||
|
});
|
||||||
|
const bitstream3 = Object.assign(new Bitstream(), {
|
||||||
|
id: 'bitstream3',
|
||||||
|
uuid: 'bitstream3'
|
||||||
|
});
|
||||||
|
const bitstream4 = Object.assign(new Bitstream(), {
|
||||||
|
id: 'bitstream4',
|
||||||
|
uuid: 'bitstream4'
|
||||||
|
});
|
||||||
|
const bundle1 = Object.assign(new Bundle(), {
|
||||||
|
id: 'bundle1',
|
||||||
|
uuid: 'bundle1',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'bundle1-selflink' }
|
||||||
|
},
|
||||||
|
bitstreams: createMockRDPaginatedObs([bitstream1, bitstream2])
|
||||||
|
});
|
||||||
|
const bundle2 = Object.assign(new Bundle(), {
|
||||||
|
id: 'bundle2',
|
||||||
|
uuid: 'bundle2',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'bundle2-selflink' }
|
||||||
|
},
|
||||||
|
bitstreams: createMockRDPaginatedObs([bitstream3, bitstream4])
|
||||||
|
});
|
||||||
|
const bundles = [bundle1, bundle2];
|
||||||
|
const bitstreamList1: PaginatedList<Bitstream> = new PaginatedList(new PageInfo(), [bitstream1, bitstream2]);
|
||||||
|
const bitstreamList2: PaginatedList<Bitstream> = new PaginatedList(new PageInfo(), [bitstream3, bitstream4]);
|
||||||
|
|
||||||
|
const item = Object.assign(new Item(), {
|
||||||
|
uuid: 'item',
|
||||||
|
id: 'item',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'item-selflink' }
|
||||||
|
},
|
||||||
|
bundles: createMockRDPaginatedObs([bundle1, bundle2])
|
||||||
|
});
|
||||||
|
|
||||||
|
const routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
item: createSuccessfulRemoteDataObject(item)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot()
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
ItemAuthorizationsComponent,
|
||||||
|
TestComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
|
ItemAuthorizationsComponent
|
||||||
|
],
|
||||||
|
schemas: [
|
||||||
|
NO_ERRORS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
let testComp: TestComponent;
|
||||||
|
let testFixture: ComponentFixture<TestComponent>;
|
||||||
|
|
||||||
|
// synchronous beforeEach
|
||||||
|
beforeEach(() => {
|
||||||
|
const html = `
|
||||||
|
<ds-item-authorizations></ds-item-authorizations>`;
|
||||||
|
|
||||||
|
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||||
|
testComp = testFixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testFixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create ItemAuthorizationsComponent', inject([ItemAuthorizationsComponent], (app: ItemAuthorizationsComponent) => {
|
||||||
|
|
||||||
|
expect(app).toBeDefined();
|
||||||
|
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemAuthorizationsComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
compAsAny = fixture.componentInstance;
|
||||||
|
linkService.resolveLink.and.callFake((object, link) => object);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
comp = null;
|
||||||
|
compAsAny = null;
|
||||||
|
de = null;
|
||||||
|
fixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should init bundles and bitstreams map properly', () => {
|
||||||
|
expect(compAsAny.subs.length).toBe(2);
|
||||||
|
expect(compAsAny.bundles$.value).toEqual(bundles);
|
||||||
|
expect(compAsAny.bundleBitstreamsMap.has('bundle1')).toBeTruthy();
|
||||||
|
expect(compAsAny.bundleBitstreamsMap.has('bundle2')).toBeTruthy();
|
||||||
|
let bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle1');
|
||||||
|
expect(bitstreamList).toBeObservable(cold('(a|)', {
|
||||||
|
a: bitstreamList1
|
||||||
|
}));
|
||||||
|
|
||||||
|
bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle2');
|
||||||
|
expect(bitstreamList).toBeObservable(cold('(a|)', {
|
||||||
|
a: bitstreamList2
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get the item UUID', () => {
|
||||||
|
|
||||||
|
expect(comp.getItemUUID()).toBeObservable(cold('(a|)', {
|
||||||
|
a: item.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get the item\'s bundle', () => {
|
||||||
|
|
||||||
|
expect(comp.getItemBundles()).toBeObservable(cold('a', {
|
||||||
|
a: bundles
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// declare a test component
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-test-cmp',
|
||||||
|
template: ``
|
||||||
|
})
|
||||||
|
class TestComponent {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,155 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||||
|
import { catchError, filter, first, flatMap, map, take } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import {
|
||||||
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
getFirstSucceededRemoteDataWithNotEmptyPayload
|
||||||
|
} from '../../../core/shared/operators';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { LinkService } from '../../../core/cache/builders/link.service';
|
||||||
|
import { Bundle } from '../../../core/shared/bundle.model';
|
||||||
|
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
|
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||||
|
import { FindListOptions } from '../../../core/data/request.models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a bundle's bitstream map entry
|
||||||
|
*/
|
||||||
|
interface BundleBitstreamsMapEntry {
|
||||||
|
id: string;
|
||||||
|
bitstreams: Observable<PaginatedList<Bitstream>>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-authorizations',
|
||||||
|
templateUrl: './item-authorizations.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component that handles the item Authorizations
|
||||||
|
*/
|
||||||
|
export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map that contains all bitstream of the item's bundles
|
||||||
|
* @type {Observable<Map<string, Observable<PaginatedList<Bitstream>>>>}
|
||||||
|
*/
|
||||||
|
public bundleBitstreamsMap: Map<string, Observable<PaginatedList<Bitstream>>> = new Map<string, Observable<PaginatedList<Bitstream>>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of bundle for the item
|
||||||
|
* @type {Observable<PaginatedList<Bundle>>}
|
||||||
|
*/
|
||||||
|
private bundles$: BehaviorSubject<Bundle[]> = new BehaviorSubject<Bundle[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The target editing item
|
||||||
|
* @type {Observable<Item>}
|
||||||
|
*/
|
||||||
|
private item$: Observable<Item>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize instance variables
|
||||||
|
*
|
||||||
|
* @param {LinkService} linkService
|
||||||
|
* @param {ActivatedRoute} route
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private linkService: LinkService,
|
||||||
|
private route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the component, setting up the bundle and bitstream within the item
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.item$ = this.route.data.pipe(
|
||||||
|
map((data) => data.item),
|
||||||
|
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
||||||
|
map((item: Item) => this.linkService.resolveLink(
|
||||||
|
item,
|
||||||
|
followLink('bundles', new FindListOptions(), true, followLink('bitstreams'))
|
||||||
|
))
|
||||||
|
) as Observable<Item>;
|
||||||
|
|
||||||
|
const bundles$: Observable<PaginatedList<Bundle>> = this.item$.pipe(
|
||||||
|
filter((item: Item) => isNotEmpty(item.bundles)),
|
||||||
|
flatMap((item: Item) => item.bundles),
|
||||||
|
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
||||||
|
catchError((error) => {
|
||||||
|
console.error(error);
|
||||||
|
return observableOf(new PaginatedList(null, []))
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subs.push(
|
||||||
|
bundles$.pipe(
|
||||||
|
take(1),
|
||||||
|
map((list: PaginatedList<Bundle>) => list.page)
|
||||||
|
).subscribe((bundles: Bundle[]) => {
|
||||||
|
this.bundles$.next(bundles);
|
||||||
|
}),
|
||||||
|
bundles$.pipe(
|
||||||
|
take(1),
|
||||||
|
flatMap((list: PaginatedList<Bundle>) => list.page),
|
||||||
|
map((bundle: Bundle) => ({ id: bundle.id, bitstreams: this.getBundleBitstreams(bundle) }))
|
||||||
|
).subscribe((entry: BundleBitstreamsMapEntry) => {
|
||||||
|
this.bundleBitstreamsMap.set(entry.id, entry.bitstreams)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the item's UUID
|
||||||
|
*/
|
||||||
|
getItemUUID(): Observable<string> {
|
||||||
|
return this.item$.pipe(
|
||||||
|
map((item: Item) => item.id),
|
||||||
|
first((UUID: string) => isNotEmpty(UUID))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all item's bundles
|
||||||
|
*
|
||||||
|
* @return an observable that emits all item's bundles
|
||||||
|
*/
|
||||||
|
getItemBundles(): Observable<Bundle[]> {
|
||||||
|
return this.bundles$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all bundle's bitstreams
|
||||||
|
*
|
||||||
|
* @return an observable that emits all item's bundles
|
||||||
|
*/
|
||||||
|
private getBundleBitstreams(bundle: Bundle): Observable<PaginatedList<Bitstream>> {
|
||||||
|
return bundle.bitstreams.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
catchError((error) => {
|
||||||
|
console.error(error);
|
||||||
|
return observableOf(new PaginatedList(null, []))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from all subscriptions
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs
|
||||||
|
.filter((subscription) => hasValue(subscription))
|
||||||
|
.forEach((subscription) => subscription.unsubscribe())
|
||||||
|
}
|
||||||
|
}
|
@@ -68,6 +68,7 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
The value is supposed to be a href for the button
|
The value is supposed to be a href for the button
|
||||||
*/
|
*/
|
||||||
this.operations = [];
|
this.operations = [];
|
||||||
|
this.operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations'));
|
||||||
this.operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper'));
|
this.operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper'));
|
||||||
if (item.isWithdrawn) {
|
if (item.isWithdrawn) {
|
||||||
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate'));
|
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate'));
|
||||||
|
@@ -7,6 +7,7 @@ import { Item } from '../core/shared/item.model';
|
|||||||
import { hasValue } from '../shared/empty.util';
|
import { hasValue } from '../shared/empty.util';
|
||||||
import { find } from 'rxjs/operators';
|
import { find } from 'rxjs/operators';
|
||||||
import { followLink } from '../shared/utils/follow-link-config.model';
|
import { followLink } from '../shared/utils/follow-link-config.model';
|
||||||
|
import { FindListOptions } from '../core/data/request.models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents a resolver that requests a specific item before the route is activated
|
* This class represents a resolver that requests a specific item before the route is activated
|
||||||
@@ -26,7 +27,7 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
|||||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
|
||||||
return this.itemService.findById(route.params.id,
|
return this.itemService.findById(route.params.id,
|
||||||
followLink('owningCollection'),
|
followLink('owningCollection'),
|
||||||
followLink('bundles'),
|
followLink('bundles', new FindListOptions(), true, followLink('bitstreams')),
|
||||||
followLink('relationships'),
|
followLink('relationships'),
|
||||||
followLink('version', undefined, true, followLink('versionhistory')),
|
followLink('version', undefined, true, followLink('versionhistory')),
|
||||||
).pipe(
|
).pipe(
|
||||||
|
@@ -3,12 +3,17 @@ import { RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { LoginPageComponent } from './login-page.component';
|
import { LoginPageComponent } from './login-page.component';
|
||||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{ path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } }
|
{ path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } }
|
||||||
])
|
])
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
I18nBreadcrumbResolver,
|
||||||
|
I18nBreadcrumbsService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class LoginPageRoutingModule {
|
export class LoginPageRoutingModule {
|
||||||
|
@@ -6,9 +6,11 @@ import { ConfigurationSearchPageComponent } from './configuration-search-page.co
|
|||||||
import { SearchPageComponent } from './search-page.component';
|
import { SearchPageComponent } from './search-page.component';
|
||||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
|
import { SearchPageModule } from './search-page.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
SearchPageModule,
|
||||||
RouterModule.forChild([{
|
RouterModule.forChild([{
|
||||||
path: '',
|
path: '',
|
||||||
resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'search' },
|
resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'search' },
|
||||||
|
@@ -2,10 +2,8 @@ import { NgModule } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CoreModule } from '../core/core.module';
|
import { CoreModule } from '../core/core.module';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { SearchPageRoutingModule } from './search-page-routing.module';
|
|
||||||
import { SearchComponent } from './search.component';
|
import { SearchComponent } from './search.component';
|
||||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
|
||||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||||
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
||||||
import { SearchTrackerComponent } from './search-tracker.component';
|
import { SearchTrackerComponent } from './search-tracker.component';
|
||||||
@@ -14,7 +12,6 @@ import { SearchPageComponent } from './search-page.component';
|
|||||||
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
|
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
|
||||||
import { SearchFilterService } from '../core/shared/search/search-filter.service';
|
import { SearchFilterService } from '../core/shared/search/search-filter.service';
|
||||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
|
||||||
|
|
||||||
const components = [
|
const components = [
|
||||||
SearchPageComponent,
|
SearchPageComponent,
|
||||||
@@ -25,7 +22,6 @@ const components = [
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
SearchPageRoutingModule,
|
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
CoreModule.forRoot(),
|
CoreModule.forRoot(),
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="container" *ngVar="item$ | async as item">
|
||||||
|
<h2>{{'workflow-item.' + type + '.header' | translate}}</h2>
|
||||||
|
<ds-modify-item-overview *ngIf="item" [item]="item"></ds-modify-item-overview>
|
||||||
|
<button class="btn btn-default" (click)="previousPage()">{{'workflow-item.' + type + '.button.cancel' | translate}}</button>
|
||||||
|
<button class="btn btn-danger" (click)="performAction()">{{'workflow-item.' + type + '.button.confirm' | translate}}</button>
|
||||||
|
</div>
|
@@ -0,0 +1,124 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
import { WorkflowItemActionPageComponent } from './workflow-item-action-page.component';
|
||||||
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
|
import { RouteService } from '../core/services/route.service';
|
||||||
|
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { WorkflowItem } from '../core/submission/models/workflowitem.model';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { VarDirective } from '../shared/utils/var.directive';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
|
import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
|
||||||
|
import { ActivatedRouteStub } from '../shared/testing/active-router.stub';
|
||||||
|
import { RouterStub } from '../shared/testing/router.stub';
|
||||||
|
import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub';
|
||||||
|
|
||||||
|
const type = 'testType';
|
||||||
|
describe('WorkflowItemActionPageComponent', () => {
|
||||||
|
let component: WorkflowItemActionPageComponent;
|
||||||
|
let fixture: ComponentFixture<WorkflowItemActionPageComponent>;
|
||||||
|
let wfiService;
|
||||||
|
let wfi;
|
||||||
|
let itemRD$;
|
||||||
|
let id;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
wfiService = jasmine.createSpyObj('workflowItemService', {
|
||||||
|
sendBack: observableOf(true)
|
||||||
|
});
|
||||||
|
itemRD$ = createSuccessfulRemoteDataObject$(itemRD$);
|
||||||
|
wfi = new WorkflowItem();
|
||||||
|
wfi.item = itemRD$;
|
||||||
|
id = 'de11b5e5-064a-4e98-a7ac-a1a6a65ddf80';
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [TestComponent, VarDirective],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) },
|
||||||
|
{ provide: Router, useClass: RouterStub },
|
||||||
|
{ provide: RouteService, useValue: {} },
|
||||||
|
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||||
|
{ provide: WorkflowItemDataService, useValue: wfiService },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(TestComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the initial type correctly', () => {
|
||||||
|
expect(component.type).toEqual(type);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clicking the button with class btn-danger', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(component, 'performAction');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call performAction on clicking the btn-danger', () => {
|
||||||
|
const button = fixture.debugElement.query(By.css('.btn-danger')).nativeElement;
|
||||||
|
button.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component.performAction).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clicking the button with class btn-default', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(component, 'previousPage');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call performAction on clicking the btn-default', () => {
|
||||||
|
const button = fixture.debugElement.query(By.css('.btn-default')).nativeElement;
|
||||||
|
button.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component.previousPage).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-workflow-item-test-action-page',
|
||||||
|
templateUrl: 'workflow-item-action-page.component.html'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
class TestComponent extends WorkflowItemActionPageComponent {
|
||||||
|
constructor(protected route: ActivatedRoute,
|
||||||
|
protected workflowItemService: WorkflowItemDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected routeService: RouteService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translationService: TranslateService) {
|
||||||
|
super(route, workflowItemService, router, routeService, notificationsService, translationService);
|
||||||
|
}
|
||||||
|
|
||||||
|
getType(): string {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRequest(id: string): Observable<boolean> {
|
||||||
|
return observableOf(true);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,86 @@
|
|||||||
|
import { OnInit } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { WorkflowItem } from '../core/submission/models/workflowitem.model';
|
||||||
|
import { Item } from '../core/shared/item.model';
|
||||||
|
import { ActivatedRoute, Data, Router } from '@angular/router';
|
||||||
|
import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service';
|
||||||
|
import { RouteService } from '../core/services/route.service';
|
||||||
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../core/shared/operators';
|
||||||
|
import { isEmpty } from '../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract component representing a page to perform an action on a workflow item
|
||||||
|
*/
|
||||||
|
export abstract class WorkflowItemActionPageComponent implements OnInit {
|
||||||
|
public type;
|
||||||
|
public wfi$: Observable<WorkflowItem>;
|
||||||
|
public item$: Observable<Item>;
|
||||||
|
|
||||||
|
constructor(protected route: ActivatedRoute,
|
||||||
|
protected workflowItemService: WorkflowItemDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected routeService: RouteService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translationService: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the type, workflow item and its item object
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
this.type = this.getType();
|
||||||
|
this.wfi$ = this.route.data.pipe(map((data: Data) => data.wfi as RemoteData<WorkflowItem>), getRemoteDataPayload());
|
||||||
|
this.item$ = this.wfi$.pipe(switchMap((wfi: WorkflowItem) => (wfi.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the action and shows a notification based on the outcome of the action
|
||||||
|
*/
|
||||||
|
performAction() {
|
||||||
|
this.wfi$.pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((wfi: WorkflowItem) => this.sendRequest(wfi.id))
|
||||||
|
).subscribe((successful: boolean) => {
|
||||||
|
if (successful) {
|
||||||
|
const title = this.translationService.get('workflow-item.' + this.type + '.notification.success.title');
|
||||||
|
const content = this.translationService.get('workflow-item.' + this.type + '.notification.success.content');
|
||||||
|
this.notificationsService.success(title, content)
|
||||||
|
} else {
|
||||||
|
const title = this.translationService.get('workflow-item.' + this.type + '.notification.error.title');
|
||||||
|
const content = this.translationService.get('workflow-item.' + this.type + '.notification.error.content');
|
||||||
|
this.notificationsService.error(title, content)
|
||||||
|
}
|
||||||
|
this.previousPage();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to the previous url
|
||||||
|
* If there's not previous url, it continues to the mydspace page instead
|
||||||
|
*/
|
||||||
|
previousPage() {
|
||||||
|
this.routeService.getPreviousUrl().pipe(take(1))
|
||||||
|
.subscribe((url) => {
|
||||||
|
if (isEmpty(url)) {
|
||||||
|
url = '/mydspace';
|
||||||
|
}
|
||||||
|
this.router.navigateByUrl(url);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the action of this workflow item action page
|
||||||
|
* @param id The id of the WorkflowItem
|
||||||
|
*/
|
||||||
|
abstract sendRequest(id: string): Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of page
|
||||||
|
*/
|
||||||
|
abstract getType(): string;
|
||||||
|
}
|
@@ -0,0 +1,76 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { WorkflowItemDeleteComponent } from './workflow-item-delete.component';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { RouteService } from '../../core/services/route.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
|
||||||
|
import { WorkflowItem } from '../../core/submission/models/workflowitem.model';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { RequestService } from '../../core/data/request.service';
|
||||||
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
|
import { RouterStub } from '../../shared/testing/router.stub';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
|
|
||||||
|
describe('WorkflowItemDeleteComponent', () => {
|
||||||
|
let component: WorkflowItemDeleteComponent;
|
||||||
|
let fixture: ComponentFixture<WorkflowItemDeleteComponent>;
|
||||||
|
let wfiService;
|
||||||
|
let wfi;
|
||||||
|
let itemRD$;
|
||||||
|
let id;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
wfiService = jasmine.createSpyObj('workflowItemService', {
|
||||||
|
delete: observableOf(true)
|
||||||
|
});
|
||||||
|
itemRD$ = createSuccessfulRemoteDataObject$(itemRD$);
|
||||||
|
wfi = new WorkflowItem();
|
||||||
|
wfi.item = itemRD$;
|
||||||
|
id = 'de11b5e5-064a-4e98-a7ac-a1a6a65ddf80';
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [WorkflowItemDeleteComponent, VarDirective],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) },
|
||||||
|
{ provide: Router, useClass: RouterStub },
|
||||||
|
{ provide: RouteService, useValue: {} },
|
||||||
|
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||||
|
{ provide: WorkflowItemDataService, useValue: wfiService },
|
||||||
|
{ provide: RequestService, useValue: getMockRequestService() },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(WorkflowItemDeleteComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call delete on the workflow-item service when sendRequest is called', () => {
|
||||||
|
component.sendRequest(id);
|
||||||
|
expect(wfiService.delete).toHaveBeenCalledWith(id);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,44 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { WorkflowItemActionPageComponent } from '../workflow-item-action-page.component';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
|
||||||
|
import { RouteService } from '../../core/services/route.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { RequestService } from '../../core/data/request.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-workflow-item-delete',
|
||||||
|
templateUrl: '../workflow-item-action-page.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component representing a page to delete a workflow item
|
||||||
|
*/
|
||||||
|
export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent {
|
||||||
|
constructor(protected route: ActivatedRoute,
|
||||||
|
protected workflowItemService: WorkflowItemDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected routeService: RouteService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translationService: TranslateService,
|
||||||
|
protected requestService: RequestService) {
|
||||||
|
super(route, workflowItemService, router, routeService, notificationsService, translationService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of page
|
||||||
|
*/
|
||||||
|
getType(): string {
|
||||||
|
return 'delete';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the action of this workflow item action page
|
||||||
|
* @param id The id of the WorkflowItem
|
||||||
|
*/
|
||||||
|
sendRequest(id: string): Observable<boolean> {
|
||||||
|
this.requestService.removeByHrefSubstring('/discover');
|
||||||
|
return this.workflowItemService.delete(id);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { WorkflowItemPageResolver } from './workflow-item-page.resolver';
|
||||||
|
import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service';
|
||||||
|
|
||||||
|
describe('WorkflowItemPageResolver', () => {
|
||||||
|
describe('resolve', () => {
|
||||||
|
let resolver: WorkflowItemPageResolver;
|
||||||
|
let wfiService: WorkflowItemDataService;
|
||||||
|
const uuid = '1234-65487-12354-1235';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wfiService = {
|
||||||
|
findById: (id: string) => observableOf({ payload: { id }, hasSucceeded: true }) as any
|
||||||
|
} as any;
|
||||||
|
resolver = new WorkflowItemPageResolver(wfiService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve a workflow item with the correct id', () => {
|
||||||
|
resolver.resolve({ params: { id: uuid } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(resolved) => {
|
||||||
|
expect(resolved.payload.id).toEqual(uuid);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,33 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
import { find } from 'rxjs/operators';
|
||||||
|
import { followLink } from '../shared/utils/follow-link-config.model';
|
||||||
|
import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service';
|
||||||
|
import { WorkflowItem } from '../core/submission/models/workflowitem.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that requests a specific workflow item before the route is activated
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class WorkflowItemPageResolver implements Resolve<RemoteData<WorkflowItem>> {
|
||||||
|
constructor(private workflowItemService: WorkflowItemDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving a workflow item based on the parameters in the current route
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns Observable<<RemoteData<Item>> Emits the found workflow item based on the parameters in the current route,
|
||||||
|
* or an error if something went wrong
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<WorkflowItem>> {
|
||||||
|
return this.workflowItemService.findById(route.params.id,
|
||||||
|
followLink('item'),
|
||||||
|
).pipe(
|
||||||
|
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,76 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { RouteService } from '../../core/services/route.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
|
||||||
|
import { WorkflowItem } from '../../core/submission/models/workflowitem.model';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { WorkflowItemSendBackComponent } from './workflow-item-send-back.component';
|
||||||
|
import { RequestService } from '../../core/data/request.service';
|
||||||
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
|
import { RouterStub } from '../../shared/testing/router.stub';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
|
|
||||||
|
describe('WorkflowItemSendBackComponent', () => {
|
||||||
|
let component: WorkflowItemSendBackComponent;
|
||||||
|
let fixture: ComponentFixture<WorkflowItemSendBackComponent>;
|
||||||
|
let wfiService;
|
||||||
|
let wfi;
|
||||||
|
let itemRD$;
|
||||||
|
let id;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
wfiService = jasmine.createSpyObj('workflowItemService', {
|
||||||
|
sendBack: observableOf(true)
|
||||||
|
});
|
||||||
|
itemRD$ = createSuccessfulRemoteDataObject$(itemRD$);
|
||||||
|
wfi = new WorkflowItem();
|
||||||
|
wfi.item = itemRD$;
|
||||||
|
id = 'de11b5e5-064a-4e98-a7ac-a1a6a65ddf80';
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [WorkflowItemSendBackComponent, VarDirective],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) },
|
||||||
|
{ provide: Router, useClass: RouterStub },
|
||||||
|
{ provide: RouteService, useValue: {} },
|
||||||
|
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||||
|
{ provide: WorkflowItemDataService, useValue: wfiService },
|
||||||
|
{ provide: RequestService, useValue: getMockRequestService() },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(WorkflowItemSendBackComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call sendBack on the workflow-item service when sendRequest is called', () => {
|
||||||
|
component.sendRequest(id);
|
||||||
|
expect(wfiService.sendBack).toHaveBeenCalledWith(id);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,44 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { WorkflowItemActionPageComponent } from '../workflow-item-action-page.component';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
|
||||||
|
import { RouteService } from '../../core/services/route.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { RequestService } from '../../core/data/request.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-workflow-item-send-back',
|
||||||
|
templateUrl: '../workflow-item-action-page.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component representing a page to send back a workflow item to the submitter
|
||||||
|
*/
|
||||||
|
export class WorkflowItemSendBackComponent extends WorkflowItemActionPageComponent {
|
||||||
|
constructor(protected route: ActivatedRoute,
|
||||||
|
protected workflowItemService: WorkflowItemDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected routeService: RouteService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translationService: TranslateService,
|
||||||
|
protected requestService: RequestService) {
|
||||||
|
super(route, workflowItemService, router, routeService, notificationsService, translationService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of page
|
||||||
|
*/
|
||||||
|
getType(): string {
|
||||||
|
return 'send-back';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the action of this workflow item action page
|
||||||
|
* @param id The id of the WorkflowItem
|
||||||
|
*/
|
||||||
|
sendRequest(id: string): Observable<boolean> {
|
||||||
|
this.requestService.removeByHrefSubstring('/discover');
|
||||||
|
return this.workflowItemService.sendBack(id);
|
||||||
|
}
|
||||||
|
}
|
@@ -3,21 +3,65 @@ import { RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
import { SubmissionEditComponent } from '../submission/edit/submission-edit.component';
|
import { SubmissionEditComponent } from '../submission/edit/submission-edit.component';
|
||||||
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
|
import { getWorkflowItemModulePath } from '../app-routing.module';
|
||||||
|
import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component';
|
||||||
|
import { WorkflowItemPageResolver } from './workflow-item-page.resolver';
|
||||||
|
import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component';
|
||||||
|
|
||||||
|
export function getWorkflowItemPageRoute(wfiId: string) {
|
||||||
|
return new URLCombiner(getWorkflowItemModulePath(), wfiId).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWorkflowItemEditPath(wfiId: string) {
|
||||||
|
return new URLCombiner(getWorkflowItemModulePath(), wfiId, WORKFLOW_ITEM_EDIT_PATH).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWorkflowItemDeletePath(wfiId: string) {
|
||||||
|
return new URLCombiner(getWorkflowItemModulePath(), wfiId, WORKFLOW_ITEM_DELETE_PATH).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWorkflowItemSendBackPath(wfiId: string) {
|
||||||
|
return new URLCombiner(getWorkflowItemModulePath(), wfiId, WORKFLOW_ITEM_SEND_BACK_PATH).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const WORKFLOW_ITEM_EDIT_PATH = 'edit';
|
||||||
|
const WORKFLOW_ITEM_DELETE_PATH = 'delete';
|
||||||
|
const WORKFLOW_ITEM_SEND_BACK_PATH = 'sendback';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
|
||||||
{
|
{
|
||||||
canActivate: [AuthenticatedGuard],
|
path: ':id',
|
||||||
path: ':id/edit',
|
resolve: { wfi: WorkflowItemPageResolver },
|
||||||
component: SubmissionEditComponent,
|
children: [
|
||||||
data: { title: 'submission.edit.title' }
|
{
|
||||||
}
|
canActivate: [AuthenticatedGuard],
|
||||||
])
|
path: WORKFLOW_ITEM_EDIT_PATH,
|
||||||
]
|
component: SubmissionEditComponent,
|
||||||
|
data: { title: 'submission.edit.title' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
path: WORKFLOW_ITEM_DELETE_PATH,
|
||||||
|
component: WorkflowItemDeleteComponent,
|
||||||
|
data: { title: 'workflow-item.delete.title' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
path: WORKFLOW_ITEM_SEND_BACK_PATH,
|
||||||
|
component: WorkflowItemSendBackComponent,
|
||||||
|
data: { title: 'workflow-item.send-back.title' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
providers: [WorkflowItemPageResolver]
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* This module defines the default component to load when navigating to the workflowitems edit page path.
|
* This module defines the default component to load when navigating to the workflowitems edit page path.
|
||||||
*/
|
*/
|
||||||
export class WorkflowItemsEditPageRoutingModule { }
|
export class WorkflowItemsEditPageRoutingModule {
|
||||||
|
}
|
||||||
|
@@ -3,6 +3,8 @@ import { NgModule } from '@angular/core';
|
|||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { WorkflowItemsEditPageRoutingModule } from './workflowitems-edit-page-routing.module';
|
import { WorkflowItemsEditPageRoutingModule } from './workflowitems-edit-page-routing.module';
|
||||||
import { SubmissionModule } from '../submission/submission.module';
|
import { SubmissionModule } from '../submission/submission.module';
|
||||||
|
import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component';
|
||||||
|
import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -11,7 +13,7 @@ import { SubmissionModule } from '../submission/submission.module';
|
|||||||
SharedModule,
|
SharedModule,
|
||||||
SubmissionModule,
|
SubmissionModule,
|
||||||
],
|
],
|
||||||
declarations: []
|
declarations: [WorkflowItemDeleteComponent, WorkflowItemSendBackComponent]
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* This module handles all modules that need to access the workflowitems edit page.
|
* This module handles all modules that need to access the workflowitems edit page.
|
||||||
|
@@ -33,7 +33,7 @@ export function getBitstreamModulePath() {
|
|||||||
return `/${BITSTREAM_MODULE_PATH}`;
|
return `/${BITSTREAM_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADMIN_MODULE_PATH = 'admin';
|
export const ADMIN_MODULE_PATH = 'admin';
|
||||||
|
|
||||||
export function getAdminModulePath() {
|
export function getAdminModulePath() {
|
||||||
return `/${ADMIN_MODULE_PATH}`;
|
return `/${ADMIN_MODULE_PATH}`;
|
||||||
@@ -45,6 +45,12 @@ export function getProfileModulePath() {
|
|||||||
return `/${PROFILE_MODULE_PATH}`;
|
return `/${PROFILE_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WORKFLOW_ITEM_MODULE_PATH = 'workflowitems';
|
||||||
|
|
||||||
|
export function getWorkflowItemModulePath() {
|
||||||
|
return `/${WORKFLOW_ITEM_MODULE_PATH}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function getDSOPath(dso: DSpaceObject): string {
|
export function getDSOPath(dso: DSpaceObject): string {
|
||||||
switch ((dso as any).type) {
|
switch ((dso as any).type) {
|
||||||
case Community.type.value:
|
case Community.type.value:
|
||||||
@@ -60,6 +66,7 @@ export function getDSOPath(dso: DSpaceObject): string {
|
|||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot([
|
RouterModule.forRoot([
|
||||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||||
|
{ path: 'reload/:rnd', redirectTo: '/home', pathMatch: 'full' },
|
||||||
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false } },
|
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false } },
|
||||||
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
|
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
|
||||||
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||||
@@ -73,7 +80,7 @@ export function getDSOPath(dso: DSpaceObject): string {
|
|||||||
loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule',
|
loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule',
|
||||||
canActivate: [AuthenticatedGuard]
|
canActivate: [AuthenticatedGuard]
|
||||||
},
|
},
|
||||||
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
{ path: 'search', loadChildren: './+search-page/search-page-routing.module#SearchPageRoutingModule' },
|
||||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
|
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
|
||||||
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
||||||
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||||
@@ -84,7 +91,7 @@ export function getDSOPath(dso: DSpaceObject): string {
|
|||||||
loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule'
|
loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'workflowitems',
|
path: WORKFLOW_ITEM_MODULE_PATH,
|
||||||
loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule'
|
loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -1,17 +1,20 @@
|
|||||||
<nav *ngIf="showBreadcrumbs" aria-label="breadcrumb">
|
<ng-container *ngVar="(breadcrumbs$ | async) as breadcrumbs">
|
||||||
<ol class="breadcrumb">
|
<nav *ngIf="showBreadcrumbs" aria-label="breadcrumb">
|
||||||
<ng-container *ngTemplateOutlet="breadcrumbs.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'Home', url: '/'}"></ng-container>
|
<ol class="breadcrumb">
|
||||||
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">
|
<ng-container
|
||||||
<ng-container *ngTemplateOutlet="!last ? breadcrumb : activeBreadcrumb; context: bc"></ng-container>
|
*ngTemplateOutlet="breadcrumbs?.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'Home', url: '/'}"></ng-container>
|
||||||
</ng-container>
|
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">
|
||||||
</ol>
|
<ng-container *ngTemplateOutlet="!last ? breadcrumb : activeBreadcrumb; context: bc"></ng-container>
|
||||||
</nav>
|
</ng-container>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<ng-template #breadcrumb let-text="text" let-url="url">
|
<ng-template #breadcrumb let-text="text" let-url="url">
|
||||||
<li class="breadcrumb-item"><a [routerLink]="url">{{text | translate}}</a></li>
|
<li class="breadcrumb-item"><a [routerLink]="url">{{text | translate}}</a></li>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #activeBreadcrumb let-text="text" >
|
<ng-template #activeBreadcrumb let-text="text">
|
||||||
<li class="breadcrumb-item active" aria-current="page">{{text | translate}}</li>
|
<li class="breadcrumb-item active" aria-current="page">{{text | translate}}</li>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
@@ -9,6 +9,9 @@ import { TranslateLoaderMock } from '../shared/testing/translate-loader.mock';
|
|||||||
import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model';
|
import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model';
|
||||||
import { BreadcrumbsService } from '../core/breadcrumbs/breadcrumbs.service';
|
import { BreadcrumbsService } from '../core/breadcrumbs/breadcrumbs.service';
|
||||||
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { VarDirective } from '../shared/utils/var.directive';
|
||||||
import { getTestScheduler } from 'jasmine-marbles';
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
|
||||||
class TestBreadcrumbsService implements BreadcrumbsService<string> {
|
class TestBreadcrumbsService implements BreadcrumbsService<string> {
|
||||||
@@ -64,17 +67,16 @@ describe('BreadcrumbsComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
init();
|
init();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [BreadcrumbsComponent],
|
declarations: [BreadcrumbsComponent, VarDirective],
|
||||||
imports: [RouterTestingModule.withRoutes([]), TranslateModule.forRoot({
|
imports: [RouterTestingModule.withRoutes([]), TranslateModule.forRoot({
|
||||||
loader: {
|
loader: {
|
||||||
provide: TranslateLoader,
|
provide: TranslateLoader,
|
||||||
useClass: TranslateLoaderMock
|
useClass: TranslateLoaderMock
|
||||||
}
|
}
|
||||||
})],
|
}), NgbModule],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: route }
|
{provide: ActivatedRoute, useValue: route}
|
||||||
|
], schemas: [NO_ERRORS_SCHEMA]
|
||||||
]
|
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
}));
|
}));
|
||||||
@@ -92,14 +94,16 @@ describe('BreadcrumbsComponent', () => {
|
|||||||
|
|
||||||
describe('ngOnInit', () => {
|
describe('ngOnInit', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([]))
|
spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call resolveBreadcrumb on init', () => {
|
it('should call resolveBreadcrumb on init', () => {
|
||||||
router.events = observableOf(new NavigationEnd(0, '', ''));
|
router.events = observableOf(new NavigationEnd(0, '', ''));
|
||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(component.resolveBreadcrumbs).toHaveBeenCalledWith(route.root);
|
expect(component.resolveBreadcrumbs).toHaveBeenCalledWith(route.root);
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resolveBreadcrumbs', () => {
|
describe('resolveBreadcrumbs', () => {
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||||
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||||
import { hasNoValue, hasValue, isNotUndefined, isUndefined } from '../shared/empty.util';
|
import { hasNoValue, hasValue, isUndefined } from '../shared/empty.util';
|
||||||
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { combineLatest, Observable, Subscription, of as observableOf } from 'rxjs';
|
import { combineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component representing the breadcrumbs of a page
|
* Component representing the breadcrumbs of a page
|
||||||
@@ -13,22 +13,17 @@ import { combineLatest, Observable, Subscription, of as observableOf } from 'rxj
|
|||||||
templateUrl: './breadcrumbs.component.html',
|
templateUrl: './breadcrumbs.component.html',
|
||||||
styleUrls: ['./breadcrumbs.component.scss']
|
styleUrls: ['./breadcrumbs.component.scss']
|
||||||
})
|
})
|
||||||
export class BreadcrumbsComponent implements OnInit, OnDestroy {
|
export class BreadcrumbsComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* List of breadcrumbs for this page
|
* Observable of the list of breadcrumbs for this page
|
||||||
*/
|
*/
|
||||||
breadcrumbs: Breadcrumb[];
|
breadcrumbs$: Observable<Breadcrumb[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not to show breadcrumbs on this page
|
* Whether or not to show breadcrumbs on this page
|
||||||
*/
|
*/
|
||||||
showBreadcrumbs: boolean;
|
showBreadcrumbs: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription to unsubscribe from on destroy
|
|
||||||
*/
|
|
||||||
subscription: Subscription;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router
|
private router: Router
|
||||||
@@ -39,14 +34,11 @@ export class BreadcrumbsComponent implements OnInit, OnDestroy {
|
|||||||
* Sets the breadcrumbs on init for this page
|
* Sets the breadcrumbs on init for this page
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.subscription = this.router.events.pipe(
|
this.breadcrumbs$ = this.router.events.pipe(
|
||||||
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
||||||
tap(() => this.reset()),
|
tap(() => this.reset()),
|
||||||
switchMap(() => this.resolveBreadcrumbs(this.route.root))
|
switchMap(() => this.resolveBreadcrumbs(this.route.root)),
|
||||||
).subscribe((breadcrumbs) => {
|
);
|
||||||
this.breadcrumbs = breadcrumbs;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,20 +73,10 @@ export class BreadcrumbsComponent implements OnInit, OnDestroy {
|
|||||||
return !last ? this.resolveBreadcrumbs(route.firstChild) : observableOf([]);
|
return !last ? this.resolveBreadcrumbs(route.firstChild) : observableOf([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe from subscription
|
|
||||||
*/
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
if (hasValue(this.subscription)) {
|
|
||||||
this.subscription.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the state of the breadcrumbs
|
* Resets the state of the breadcrumbs
|
||||||
*/
|
*/
|
||||||
reset() {
|
reset() {
|
||||||
this.breadcrumbs = [];
|
|
||||||
this.showBreadcrumbs = true;
|
this.showBreadcrumbs = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -402,10 +402,10 @@ export class RetrieveAuthenticatedEpersonAction implements Action {
|
|||||||
*/
|
*/
|
||||||
export class RetrieveAuthenticatedEpersonSuccessAction implements Action {
|
export class RetrieveAuthenticatedEpersonSuccessAction implements Action {
|
||||||
public type: string = AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS;
|
public type: string = AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS;
|
||||||
payload: EPerson;
|
payload: string;
|
||||||
|
|
||||||
constructor(user: EPerson) {
|
constructor(userId: string) {
|
||||||
this.payload = user ;
|
this.payload = userId ;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { fakeAsync, flush, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { provideMockActions } from '@ngrx/effects/testing';
|
import { provideMockActions } from '@ngrx/effects/testing';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
|
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||||
import { cold, hot } from 'jasmine-marbles';
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
|
|
||||||
import { Observable, of as observableOf, throwError as observableThrow } from 'rxjs';
|
import { Observable, of as observableOf, throwError as observableThrow } from 'rxjs';
|
||||||
|
|
||||||
import { AuthEffects } from './auth.effects';
|
import { AuthEffects } from './auth.effects';
|
||||||
@@ -29,41 +29,53 @@ import {
|
|||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { authMethodsMock, AuthServiceStub } from '../../shared/testing/auth-service.stub';
|
import { authMethodsMock, AuthServiceStub } from '../../shared/testing/auth-service.stub';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthState } from './auth.reducer';
|
import { authReducer } from './auth.reducer';
|
||||||
|
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { EPersonMock } from '../../shared/testing/eperson.mock';
|
import { EPersonMock } from '../../shared/testing/eperson.mock';
|
||||||
|
import { AppState, storeModuleConfig } from '../../app.reducer';
|
||||||
|
import { StoreActionTypes } from '../../store.actions';
|
||||||
|
import { isAuthenticated, isAuthenticatedLoaded } from './selectors';
|
||||||
|
|
||||||
describe('AuthEffects', () => {
|
describe('AuthEffects', () => {
|
||||||
let authEffects: AuthEffects;
|
let authEffects: AuthEffects;
|
||||||
let actions: Observable<any>;
|
let actions: Observable<any>;
|
||||||
let authServiceStub;
|
let authServiceStub;
|
||||||
const store: Store<AuthState> = jasmine.createSpyObj('store', {
|
let initialState;
|
||||||
/* tslint:disable:no-empty */
|
|
||||||
dispatch: {},
|
|
||||||
/* tslint:enable:no-empty */
|
|
||||||
select: observableOf(true)
|
|
||||||
});
|
|
||||||
let token;
|
let token;
|
||||||
|
let store: MockStore<AppState>;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
authServiceStub = new AuthServiceStub();
|
authServiceStub = new AuthServiceStub();
|
||||||
token = authServiceStub.getToken();
|
token = authServiceStub.getToken();
|
||||||
|
initialState = {
|
||||||
|
core: {
|
||||||
|
auth: {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
authMethods: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
init();
|
init();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
StoreModule.forRoot({ auth: authReducer }, storeModuleConfig)
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AuthEffects,
|
AuthEffects,
|
||||||
|
provideMockStore({ initialState }),
|
||||||
{ provide: AuthService, useValue: authServiceStub },
|
{ provide: AuthService, useValue: authServiceStub },
|
||||||
{ provide: Store, useValue: store },
|
|
||||||
provideMockActions(() => actions),
|
provideMockActions(() => actions),
|
||||||
// other providers
|
// other providers
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
authEffects = TestBed.get(AuthEffects);
|
authEffects = TestBed.get(AuthEffects);
|
||||||
|
store = TestBed.get(Store);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('authenticate$', () => {
|
describe('authenticate$', () => {
|
||||||
@@ -138,7 +150,8 @@ describe('AuthEffects', () => {
|
|||||||
|
|
||||||
describe('authenticatedSuccess$', () => {
|
describe('authenticatedSuccess$', () => {
|
||||||
|
|
||||||
it('should return a RETRIEVE_AUTHENTICATED_EPERSON action in response to a AUTHENTICATED_SUCCESS action', () => {
|
it('should return a RETRIEVE_AUTHENTICATED_EPERSON action in response to a AUTHENTICATED_SUCCESS action', (done) => {
|
||||||
|
spyOn((authEffects as any).authService, 'storeToken');
|
||||||
actions = hot('--a-', {
|
actions = hot('--a-', {
|
||||||
a: {
|
a: {
|
||||||
type: AuthActionTypes.AUTHENTICATED_SUCCESS, payload: {
|
type: AuthActionTypes.AUTHENTICATED_SUCCESS, payload: {
|
||||||
@@ -151,8 +164,14 @@ describe('AuthEffects', () => {
|
|||||||
|
|
||||||
const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonAction(EPersonMock._links.self.href) });
|
const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonAction(EPersonMock._links.self.href) });
|
||||||
|
|
||||||
|
authEffects.authenticatedSuccess$.subscribe(() => {
|
||||||
|
expect(authServiceStub.storeToken).toHaveBeenCalledWith(token);
|
||||||
|
});
|
||||||
|
|
||||||
expect(authEffects.authenticatedSuccess$).toBeObservable(expected);
|
expect(authEffects.authenticatedSuccess$).toBeObservable(expected);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('checkToken$', () => {
|
describe('checkToken$', () => {
|
||||||
@@ -235,7 +254,7 @@ describe('AuthEffects', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock) });
|
const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id) });
|
||||||
|
|
||||||
expect(authEffects.retrieveAuthenticatedEperson$).toBeObservable(expected);
|
expect(authEffects.retrieveAuthenticatedEperson$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -362,4 +381,40 @@ describe('AuthEffects', () => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('clearInvalidTokenOnRehydrate$', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store.overrideSelector(isAuthenticated, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when auth loaded is false', () => {
|
||||||
|
it('should not call removeToken method', (done) => {
|
||||||
|
store.overrideSelector(isAuthenticatedLoaded, false);
|
||||||
|
actions = hot('--a-|', { a: { type: StoreActionTypes.REHYDRATE } });
|
||||||
|
spyOn(authServiceStub, 'removeToken');
|
||||||
|
|
||||||
|
authEffects.clearInvalidTokenOnRehydrate$.subscribe(() => {
|
||||||
|
expect(authServiceStub.removeToken).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when auth loaded is true', () => {
|
||||||
|
it('should call removeToken method', fakeAsync(() => {
|
||||||
|
store.overrideSelector(isAuthenticatedLoaded, true);
|
||||||
|
actions = hot('--a-|', { a: { type: StoreActionTypes.REHYDRATE } });
|
||||||
|
spyOn(authServiceStub, 'removeToken');
|
||||||
|
|
||||||
|
authEffects.clearInvalidTokenOnRehydrate$.subscribe(() => {
|
||||||
|
expect(authServiceStub.removeToken).toHaveBeenCalled();
|
||||||
|
flush();
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,20 +1,18 @@
|
|||||||
import { Observable, of as observableOf } from 'rxjs';
|
|
||||||
|
|
||||||
import { catchError, debounceTime, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { catchError, debounceTime, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
// import @ngrx
|
// import @ngrx
|
||||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||||
import { Action, select, Store } from '@ngrx/store';
|
import { Action, select, Store } from '@ngrx/store';
|
||||||
|
|
||||||
// import services
|
// import services
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
import { EPerson } from '../eperson/models/eperson.model';
|
import { EPerson } from '../eperson/models/eperson.model';
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { isAuthenticated } from './selectors';
|
import { isAuthenticated, isAuthenticatedLoaded } from './selectors';
|
||||||
import { StoreActionTypes } from '../../store.actions';
|
import { StoreActionTypes } from '../../store.actions';
|
||||||
import { AuthMethod } from './models/auth.method';
|
import { AuthMethod } from './models/auth.method';
|
||||||
// import actions
|
// import actions
|
||||||
@@ -43,6 +41,7 @@ import {
|
|||||||
RetrieveAuthMethodsSuccessAction,
|
RetrieveAuthMethodsSuccessAction,
|
||||||
RetrieveTokenAction
|
RetrieveTokenAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthEffects {
|
export class AuthEffects {
|
||||||
@@ -66,7 +65,6 @@ export class AuthEffects {
|
|||||||
@Effect()
|
@Effect()
|
||||||
public authenticateSuccess$: Observable<Action> = this.actions$.pipe(
|
public authenticateSuccess$: Observable<Action> = this.actions$.pipe(
|
||||||
ofType(AuthActionTypes.AUTHENTICATE_SUCCESS),
|
ofType(AuthActionTypes.AUTHENTICATE_SUCCESS),
|
||||||
tap((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)),
|
|
||||||
map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload))
|
map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -83,6 +81,7 @@ export class AuthEffects {
|
|||||||
@Effect()
|
@Effect()
|
||||||
public authenticatedSuccess$: Observable<Action> = 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)),
|
||||||
map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref))
|
map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -97,8 +96,15 @@ export class AuthEffects {
|
|||||||
public retrieveAuthenticatedEperson$: Observable<Action> = 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) => {
|
||||||
return this.authService.retrieveAuthenticatedUserByHref(action.payload).pipe(
|
const impersonatedUserID = this.authService.getImpersonateID();
|
||||||
map((user: EPerson) => new RetrieveAuthenticatedEpersonSuccessAction(user)),
|
let user$: Observable<EPerson>;
|
||||||
|
if (hasValue(impersonatedUserID)) {
|
||||||
|
user$ = this.authService.retrieveAuthenticatedUserById(impersonatedUserID);
|
||||||
|
} else {
|
||||||
|
user$ = this.authService.retrieveAuthenticatedUserByHref(action.payload);
|
||||||
|
}
|
||||||
|
return user$.pipe(
|
||||||
|
map((user: EPerson) => new RetrieveAuthenticatedEpersonSuccessAction(user.id)),
|
||||||
catchError((error) => observableOf(new RetrieveAuthenticatedEpersonErrorAction(error))));
|
catchError((error) => observableOf(new RetrieveAuthenticatedEpersonErrorAction(error))));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -179,10 +185,11 @@ export class AuthEffects {
|
|||||||
public clearInvalidTokenOnRehydrate$: Observable<any> = this.actions$.pipe(
|
public clearInvalidTokenOnRehydrate$: Observable<any> = this.actions$.pipe(
|
||||||
ofType(StoreActionTypes.REHYDRATE),
|
ofType(StoreActionTypes.REHYDRATE),
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
return this.store.pipe(
|
const isLoaded$ = this.store.pipe(select(isAuthenticatedLoaded));
|
||||||
select(isAuthenticated),
|
const authenticated$ = this.store.pipe(select(isAuthenticated));
|
||||||
|
return observableCombineLatest(isLoaded$, authenticated$).pipe(
|
||||||
take(1),
|
take(1),
|
||||||
filter((authenticated) => !authenticated),
|
filter(([loaded, authenticated]) => loaded && !authenticated),
|
||||||
tap(() => this.authService.removeToken()),
|
tap(() => this.authService.removeToken()),
|
||||||
tap(() => this.authService.resetAuthenticationError())
|
tap(() => this.authService.resetAuthenticationError())
|
||||||
);
|
);
|
||||||
@@ -193,6 +200,7 @@ export class AuthEffects {
|
|||||||
.pipe(
|
.pipe(
|
||||||
ofType(AuthActionTypes.LOG_OUT),
|
ofType(AuthActionTypes.LOG_OUT),
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
|
this.authService.stopImpersonating();
|
||||||
return this.authService.logout().pipe(
|
return this.authService.logout().pipe(
|
||||||
map((value) => new LogOutSuccessAction()),
|
map((value) => new LogOutSuccessAction()),
|
||||||
catchError((error) => observableOf(new LogOutErrorAction(error)))
|
catchError((error) => observableOf(new LogOutErrorAction(error)))
|
||||||
|
@@ -48,7 +48,7 @@ describe(`AuthInterceptor`, () => {
|
|||||||
|
|
||||||
describe('when has a valid token', () => {
|
describe('when has a valid token', () => {
|
||||||
|
|
||||||
it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint that is not the logout endpoint', () => {
|
||||||
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/authn/login', 'password=password&user=user').subscribe((response) => {
|
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/authn/login', 'password=password&user=user').subscribe((response) => {
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -58,8 +58,19 @@ describe(`AuthInterceptor`, () => {
|
|||||||
const token = httpRequest.request.headers.get('authorization');
|
const token = httpRequest.request.headers.get('authorization');
|
||||||
expect(token).toBeNull();
|
expect(token).toBeNull();
|
||||||
});
|
});
|
||||||
|
it('should add an Authorization header when we’re sending a HTTP request to the\'authn/logout\' endpoint', () => {
|
||||||
|
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/authn/logout', 'test').subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it('should add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
const httpRequest = httpMock.expectOne(`dspace-spring-rest/api/authn/logout`);
|
||||||
|
|
||||||
|
expect(httpRequest.request.headers.has('authorization'));
|
||||||
|
const token = httpRequest.request.headers.get('authorization');
|
||||||
|
expect(token).toBe('Bearer token_test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add an Authorization header when we’re sending a HTTP request to a non-\'authn\' endpoint', () => {
|
||||||
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'test').subscribe((response) => {
|
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'test').subscribe((response) => {
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
@@ -18,7 +18,7 @@ import { AppState } from '../../app.reducer';
|
|||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
import { isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util';
|
||||||
import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
|
import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -221,7 +221,7 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
// Redirect to the login route
|
// Redirect to the login route
|
||||||
this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired'));
|
this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired'));
|
||||||
return observableOf(null);
|
return observableOf(null);
|
||||||
} else if (!this.isAuthRequest(req) && isNotEmpty(token)) {
|
} else if ((!this.isAuthRequest(req) || this.isLogoutResponse(req)) && isNotEmpty(token)) {
|
||||||
// Intercept a request that is not to the authentication endpoint
|
// Intercept a request that is not to the authentication endpoint
|
||||||
authService.isTokenExpiring().pipe(
|
authService.isTokenExpiring().pipe(
|
||||||
filter((isExpiring) => isExpiring))
|
filter((isExpiring) => isExpiring))
|
||||||
@@ -235,8 +235,16 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
});
|
});
|
||||||
// Get the auth header from the service.
|
// Get the auth header from the service.
|
||||||
authorization = authService.buildAuthHeader(token);
|
authorization = authService.buildAuthHeader(token);
|
||||||
|
let newHeaders = req.headers.set('authorization', authorization);
|
||||||
|
|
||||||
|
// When present, add the ID of the EPerson we're impersonating to the headers
|
||||||
|
const impersonatingID = authService.getImpersonateID();
|
||||||
|
if (hasValue(impersonatingID)) {
|
||||||
|
newHeaders = newHeaders.set('X-On-Behalf-Of', impersonatingID);
|
||||||
|
}
|
||||||
|
|
||||||
// Clone the request to add the new header.
|
// Clone the request to add the new header.
|
||||||
newReq = req.clone({ headers: req.headers.set('authorization', authorization) });
|
newReq = req.clone({ headers: newHeaders });
|
||||||
} else {
|
} else {
|
||||||
newReq = req.clone();
|
newReq = req.clone();
|
||||||
}
|
}
|
||||||
|
@@ -189,7 +189,7 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
user: EPersonMock
|
userId: EPersonMock.id
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new LogOutAction();
|
const action = new LogOutAction();
|
||||||
@@ -206,7 +206,7 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
user: EPersonMock
|
userId: EPersonMock.id
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new LogOutSuccessAction();
|
const action = new LogOutSuccessAction();
|
||||||
@@ -219,7 +219,7 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
user: undefined
|
userId: undefined
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -232,7 +232,7 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
user: EPersonMock
|
userId: EPersonMock.id
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new LogOutErrorAction(mockError);
|
const action = new LogOutErrorAction(mockError);
|
||||||
@@ -244,7 +244,7 @@ describe('authReducer', () => {
|
|||||||
error: 'Test error message',
|
error: 'Test error message',
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
user: EPersonMock
|
userId: EPersonMock.id
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -258,7 +258,7 @@ describe('authReducer', () => {
|
|||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined
|
||||||
};
|
};
|
||||||
const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock);
|
const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
state = {
|
state = {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
@@ -267,7 +267,7 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
user: EPersonMock
|
userId: EPersonMock.id
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -301,7 +301,7 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
user: EPersonMock
|
userId: EPersonMock.id
|
||||||
};
|
};
|
||||||
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||||
const action = new RefreshTokenAction(newTokenInfo);
|
const action = new RefreshTokenAction(newTokenInfo);
|
||||||
@@ -313,7 +313,7 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
user: EPersonMock,
|
userId: EPersonMock.id,
|
||||||
refreshing: true
|
refreshing: true
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
@@ -327,7 +327,7 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
user: EPersonMock,
|
userId: EPersonMock.id,
|
||||||
refreshing: true
|
refreshing: true
|
||||||
};
|
};
|
||||||
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||||
@@ -340,7 +340,7 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
user: EPersonMock,
|
userId: EPersonMock.id,
|
||||||
refreshing: false
|
refreshing: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
@@ -354,7 +354,7 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
user: EPersonMock,
|
userId: EPersonMock.id,
|
||||||
refreshing: true
|
refreshing: true
|
||||||
};
|
};
|
||||||
const action = new RefreshTokenErrorAction();
|
const action = new RefreshTokenErrorAction();
|
||||||
@@ -367,7 +367,7 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
user: undefined
|
userId: undefined
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -380,7 +380,7 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
user: EPersonMock
|
userId: EPersonMock.id
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@@ -390,7 +390,7 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
info: 'Message',
|
info: 'Message',
|
||||||
user: undefined
|
userId: undefined
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -14,7 +14,6 @@ import {
|
|||||||
SetRedirectUrlAction
|
SetRedirectUrlAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
// import models
|
// import models
|
||||||
import { EPerson } from '../eperson/models/eperson.model';
|
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
import { AuthMethod } from './models/auth.method';
|
import { AuthMethod } from './models/auth.method';
|
||||||
import { AuthMethodType } from './models/auth.method-type';
|
import { AuthMethodType } from './models/auth.method-type';
|
||||||
@@ -49,8 +48,8 @@ export interface AuthState {
|
|||||||
// true when refreshing token
|
// true when refreshing token
|
||||||
refreshing?: boolean;
|
refreshing?: boolean;
|
||||||
|
|
||||||
// the authenticated user
|
// the authenticated user's id
|
||||||
user?: EPerson;
|
userId?: string;
|
||||||
|
|
||||||
// all authentication Methods enabled at the backend
|
// all authentication Methods enabled at the backend
|
||||||
authMethods?: AuthMethod[];
|
authMethods?: AuthMethod[];
|
||||||
@@ -112,7 +111,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
user: (action as RetrieveAuthenticatedEpersonSuccessAction).payload
|
userId: (action as RetrieveAuthenticatedEpersonSuccessAction).payload
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.AUTHENTICATE_ERROR:
|
case AuthActionTypes.AUTHENTICATE_ERROR:
|
||||||
@@ -144,7 +143,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
user: undefined
|
userId: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED:
|
case AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED:
|
||||||
@@ -155,7 +154,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: (action as RedirectWhenTokenExpiredAction as RedirectWhenAuthenticationIsRequiredAction).payload,
|
info: (action as RedirectWhenTokenExpiredAction as RedirectWhenAuthenticationIsRequiredAction).payload,
|
||||||
user: undefined
|
userId: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.REGISTRATION:
|
case AuthActionTypes.REGISTRATION:
|
||||||
|
@@ -8,7 +8,7 @@ import { of as observableOf } from 'rxjs';
|
|||||||
|
|
||||||
import { authReducer, AuthState } from './auth.reducer';
|
import { authReducer, AuthState } from './auth.reducer';
|
||||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService, IMPERSONATING_COOKIE } from './auth.service';
|
||||||
import { RouterStub } from '../../shared/testing/router.stub';
|
import { RouterStub } from '../../shared/testing/router.stub';
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
|
|
||||||
@@ -332,5 +332,120 @@ describe('AuthService test', () => {
|
|||||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
||||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/');
|
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('impersonate', () => {
|
||||||
|
const userId = 'testUserId';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(authService, 'refreshAfterLogout');
|
||||||
|
authService.impersonate(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should impersonate user', () => {
|
||||||
|
expect(storage.set).toHaveBeenCalledWith(IMPERSONATING_COOKIE, userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call refreshAfterLogout', () => {
|
||||||
|
expect(authService.refreshAfterLogout).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stopImpersonating', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authService.stopImpersonating();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should impersonate user', () => {
|
||||||
|
expect(storage.remove).toHaveBeenCalledWith(IMPERSONATING_COOKIE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stopImpersonatingAndRefresh', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(authService, 'refreshAfterLogout');
|
||||||
|
authService.stopImpersonatingAndRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should impersonate user', () => {
|
||||||
|
expect(storage.remove).toHaveBeenCalledWith(IMPERSONATING_COOKIE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call refreshAfterLogout', () => {
|
||||||
|
expect(authService.refreshAfterLogout).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getImpersonateID', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authService.getImpersonateID();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should impersonate user', () => {
|
||||||
|
expect(storage.get).toHaveBeenCalledWith(IMPERSONATING_COOKIE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isImpersonating', () => {
|
||||||
|
const userId = 'testUserId';
|
||||||
|
let result: boolean;
|
||||||
|
|
||||||
|
describe('when the cookie doesn\'t contain a value', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
result = authService.isImpersonating();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false', () => {
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the cookie contains a value', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
storage.get = jasmine.createSpy().and.returnValue(userId);
|
||||||
|
result = authService.isImpersonating();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true', () => {
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isImpersonatingUser', () => {
|
||||||
|
const userId = 'testUserId';
|
||||||
|
let result: boolean;
|
||||||
|
|
||||||
|
describe('when the cookie doesn\'t contain a value', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
result = authService.isImpersonatingUser(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false', () => {
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the cookie contains the right value', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
storage.get = jasmine.createSpy().and.returnValue(userId);
|
||||||
|
result = authService.isImpersonatingUser(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true', () => {
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the cookie contains the wrong value', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
storage.get = jasmine.createSpy().and.returnValue('wrongValue');
|
||||||
|
result = authService.isImpersonatingUser(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false', () => {
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -4,7 +4,7 @@ import { HttpHeaders } from '@angular/common/http';
|
|||||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||||
|
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, startWith, take, withLatestFrom } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, startWith, switchMap, take, withLatestFrom } from 'rxjs/operators';
|
||||||
import { RouterReducerState } from '@ngrx/router-store';
|
import { RouterReducerState } from '@ngrx/router-store';
|
||||||
import { select, Store } from '@ngrx/store';
|
import { select, Store } from '@ngrx/store';
|
||||||
import { CookieAttributes } from 'js-cookie';
|
import { CookieAttributes } from 'js-cookie';
|
||||||
@@ -14,9 +14,15 @@ import { AuthRequestService } from './auth-request.service';
|
|||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
|
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
|
||||||
import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
|
import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
|
||||||
import { CookieService } from '../services/cookie.service';
|
import { CookieService } from '../services/cookie.service';
|
||||||
import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors';
|
import {
|
||||||
|
getAuthenticatedUserId,
|
||||||
|
getAuthenticationToken,
|
||||||
|
getRedirectUrl,
|
||||||
|
isAuthenticated,
|
||||||
|
isTokenRefreshing
|
||||||
|
} from './selectors';
|
||||||
import { AppState, routerStateSelector } from '../../app.reducer';
|
import { AppState, routerStateSelector } from '../../app.reducer';
|
||||||
import {
|
import {
|
||||||
CheckAuthenticationTokenAction,
|
CheckAuthenticationTokenAction,
|
||||||
@@ -33,6 +39,7 @@ import { AuthMethod } from './models/auth.method';
|
|||||||
export const LOGIN_ROUTE = '/login';
|
export const LOGIN_ROUTE = '/login';
|
||||||
export const LOGOUT_ROUTE = '/logout';
|
export const LOGOUT_ROUTE = '/logout';
|
||||||
export const REDIRECT_COOKIE = 'dsRedirectUrl';
|
export const REDIRECT_COOKIE = 'dsRedirectUrl';
|
||||||
|
export const IMPERSONATING_COOKIE = 'dsImpersonatingEPerson';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The auth service.
|
* The auth service.
|
||||||
@@ -163,7 +170,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authenticated user
|
* Returns the authenticated user by href
|
||||||
* @returns {User}
|
* @returns {User}
|
||||||
*/
|
*/
|
||||||
public retrieveAuthenticatedUserByHref(userHref: string): Observable<EPerson> {
|
public retrieveAuthenticatedUserByHref(userHref: string): Observable<EPerson> {
|
||||||
@@ -172,6 +179,29 @@ export class AuthService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authenticated user by id
|
||||||
|
* @returns {User}
|
||||||
|
*/
|
||||||
|
public retrieveAuthenticatedUserById(userId: string): Observable<EPerson> {
|
||||||
|
return this.epersonService.findById(userId).pipe(
|
||||||
|
getAllSucceededRemoteDataPayload()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authenticated user from the store
|
||||||
|
* @returns {User}
|
||||||
|
*/
|
||||||
|
public getAuthenticatedUserFromStore(): Observable<EPerson> {
|
||||||
|
return this.store.pipe(
|
||||||
|
select(getAuthenticatedUserId),
|
||||||
|
hasValueOperator(),
|
||||||
|
switchMap((id: string) => this.epersonService.findById(id)),
|
||||||
|
getAllSucceededRemoteDataPayload()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if token is present into browser storage and is valid.
|
* Checks if token is present into browser storage and is valid.
|
||||||
*/
|
*/
|
||||||
@@ -430,9 +460,9 @@ export class AuthService {
|
|||||||
* Refresh route navigated
|
* Refresh route navigated
|
||||||
*/
|
*/
|
||||||
public refreshAfterLogout() {
|
public refreshAfterLogout() {
|
||||||
this.router.navigate(['/home']);
|
// Hard redirect to the reload page with a unique number behind it
|
||||||
// Hard redirect to home page, so that all state is definitely lost
|
// so that all state is definitely lost
|
||||||
this._window.nativeWindow.location.href = '/home';
|
this._window.nativeWindow.location.href = `/reload/${new Date().getTime()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -469,4 +499,51 @@ export class AuthService {
|
|||||||
this.storage.remove(REDIRECT_COOKIE);
|
this.storage.remove(REDIRECT_COOKIE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start impersonating EPerson
|
||||||
|
* @param epersonId ID of the EPerson to impersonate
|
||||||
|
*/
|
||||||
|
impersonate(epersonId: string) {
|
||||||
|
this.storage.set(IMPERSONATING_COOKIE, epersonId);
|
||||||
|
this.refreshAfterLogout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop impersonating EPerson
|
||||||
|
*/
|
||||||
|
stopImpersonating() {
|
||||||
|
this.storage.remove(IMPERSONATING_COOKIE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop impersonating EPerson and refresh the store/ui
|
||||||
|
*/
|
||||||
|
stopImpersonatingAndRefresh() {
|
||||||
|
this.stopImpersonating();
|
||||||
|
this.refreshAfterLogout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of the EPerson we're currently impersonating
|
||||||
|
* Returns undefined if we're not impersonating anyone
|
||||||
|
*/
|
||||||
|
getImpersonateID(): string {
|
||||||
|
return this.storage.get(IMPERSONATING_COOKIE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not we are currently impersonating an EPerson
|
||||||
|
*/
|
||||||
|
isImpersonating(): boolean {
|
||||||
|
return hasValue(this.getImpersonateID());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not we are currently impersonating a specific EPerson
|
||||||
|
* @param epersonId ID of the EPerson to check
|
||||||
|
*/
|
||||||
|
isImpersonatingUser(epersonId: string): boolean {
|
||||||
|
return this.getImpersonateID() === epersonId;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,6 @@ 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 { EPerson } from '../eperson/models/eperson.model';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the user state.
|
* Returns the user state.
|
||||||
@@ -36,12 +35,11 @@ const _isAuthenticatedLoaded = (state: AuthState) => state.loaded;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the users state
|
* Return the users state
|
||||||
* NOTE: when state is REHYDRATED user object lose prototype so return always a new EPerson object
|
* @function _getAuthenticatedUserId
|
||||||
* @function _getAuthenticatedUser
|
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @returns {EPerson}
|
* @returns {string} User ID
|
||||||
*/
|
*/
|
||||||
const _getAuthenticatedUser = (state: AuthState) => Object.assign(new EPerson(), state.user);
|
const _getAuthenticatedUserId = (state: AuthState) => state.userId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authentication error.
|
* Returns the authentication error.
|
||||||
@@ -119,13 +117,13 @@ const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
|
|||||||
export const getAuthenticationMethods = createSelector(getAuthState, _getAuthenticationMethods);
|
export const getAuthenticationMethods = createSelector(getAuthState, _getAuthenticationMethods);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authenticated user
|
* Returns the authenticated user id
|
||||||
* @function getAuthenticatedUser
|
* @function getAuthenticatedUserId
|
||||||
* @param {AuthState} state
|
* @param {AuthState} state
|
||||||
* @param {any} props
|
* @param {any} props
|
||||||
* @return {User}
|
* @return {string} User ID
|
||||||
*/
|
*/
|
||||||
export const getAuthenticatedUser = createSelector(getAuthState, _getAuthenticatedUser);
|
export const getAuthenticatedUserId = createSelector(getAuthState, _getAuthenticatedUserId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authentication error.
|
* Returns the authentication error.
|
||||||
|
@@ -8,7 +8,9 @@ import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-con
|
|||||||
/**
|
/**
|
||||||
* The class that resolves the BreadcrumbConfig object for a Collection
|
* The class that resolves the BreadcrumbConfig object for a Collection
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver<Collection> {
|
export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver<Collection> {
|
||||||
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) {
|
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) {
|
||||||
super(breadcrumbService, dataService);
|
super(breadcrumbService, dataService);
|
||||||
|
@@ -8,7 +8,9 @@ import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-con
|
|||||||
/**
|
/**
|
||||||
* The class that resolves the BreadcrumbConfig object for a Community
|
* The class that resolves the BreadcrumbConfig object for a Community
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver<Community> {
|
export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver<Community> {
|
||||||
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) {
|
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) {
|
||||||
super(breadcrumbService, dataService);
|
super(breadcrumbService, dataService);
|
||||||
|
@@ -13,7 +13,9 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
|||||||
/**
|
/**
|
||||||
* The class that resolves the BreadcrumbConfig object for a DSpaceObject
|
* The class that resolves the BreadcrumbConfig object for a DSpaceObject
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceObject> implements Resolve<BreadcrumbConfig<T>> {
|
export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceObject> implements Resolve<BreadcrumbConfig<T>> {
|
||||||
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DataService<T>) {
|
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DataService<T>) {
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,9 @@ import { Injectable } from '@angular/core';
|
|||||||
/**
|
/**
|
||||||
* Service to calculate DSpaceObject breadcrumbs for a single part of the route
|
* Service to calculate DSpaceObject breadcrumbs for a single part of the route
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
export class DSOBreadcrumbsService implements BreadcrumbsService<ChildHALResource & DSpaceObject> {
|
export class DSOBreadcrumbsService implements BreadcrumbsService<ChildHALResource & DSpaceObject> {
|
||||||
constructor(
|
constructor(
|
||||||
private linkService: LinkService,
|
private linkService: LinkService,
|
||||||
|
@@ -28,7 +28,8 @@ export class DSONameService {
|
|||||||
return dso.firstMetadataValue('organization.legalName');
|
return dso.firstMetadataValue('organization.legalName');
|
||||||
},
|
},
|
||||||
Default: (dso: DSpaceObject): string => {
|
Default: (dso: DSpaceObject): string => {
|
||||||
return dso.firstMetadataValue('dc.title');
|
// If object doesn't have dc.title metadata use name property
|
||||||
|
return dso.firstMetadataValue('dc.title') || dso.name;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -31,8 +31,8 @@ describe('I18nBreadcrumbResolver', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the breadcrumb config', () => {
|
it('should resolve the breadcrumb config', () => {
|
||||||
const resolvedConfig = resolver.resolve(route, {} as any);
|
const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path], pathFromRoot: [{ url: [path] }] } as any, {} as any);
|
||||||
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath };
|
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: path };
|
||||||
expect(resolvedConfig).toEqual(expectedConfig);
|
expect(resolvedConfig).toEqual(expectedConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -8,7 +8,9 @@ import { currentPathFromSnapshot } from '../../shared/utils/route.utils';
|
|||||||
/**
|
/**
|
||||||
* The class that resolves a BreadcrumbConfig object with an i18n key string for a route
|
* The class that resolves a BreadcrumbConfig object with an i18n key string for a route
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>> {
|
export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>> {
|
||||||
constructor(protected breadcrumbService: I18nBreadcrumbsService) {
|
constructor(protected breadcrumbService: I18nBreadcrumbsService) {
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,9 @@ export const BREADCRUMB_MESSAGE_POSTFIX = '.breadcrumbs';
|
|||||||
/**
|
/**
|
||||||
* Service to calculate i18n breadcrumbs for a single part of the route
|
* Service to calculate i18n breadcrumbs for a single part of the route
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
export class I18nBreadcrumbsService implements BreadcrumbsService<string> {
|
export class I18nBreadcrumbsService implements BreadcrumbsService<string> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -8,7 +8,9 @@ import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-con
|
|||||||
/**
|
/**
|
||||||
* The class that resolves the BreadcrumbConfig object for an Item
|
* The class that resolves the BreadcrumbConfig object for an Item
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver<Item> {
|
export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver<Item> {
|
||||||
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) {
|
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) {
|
||||||
super(breadcrumbService, dataService);
|
super(breadcrumbService, dataService);
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* Class representing a query parameter (query?fieldName=fieldValue) used in FindListOptions object
|
* Class representing a query parameter (query?fieldName=fieldValue) used in FindListOptions object
|
||||||
*/
|
*/
|
||||||
export class SearchParam {
|
export class RequestParam {
|
||||||
constructor(public fieldName: string, public fieldValue: any) {
|
constructor(public fieldName: string, public fieldValue: any) {
|
||||||
|
|
||||||
}
|
}
|
45
src/app/core/cache/response.models.ts
vendored
45
src/app/core/cache/response.models.ts
vendored
@@ -6,9 +6,6 @@ import { ConfigObject } from '../config/models/config.model';
|
|||||||
import { FacetValue } from '../../shared/search/facet-value.model';
|
import { FacetValue } from '../../shared/search/facet-value.model';
|
||||||
import { SearchFilterConfig } from '../../shared/search/search-filter-config.model';
|
import { SearchFilterConfig } from '../../shared/search/search-filter-config.model';
|
||||||
import { IntegrationModel } from '../integration/models/integration.model';
|
import { IntegrationModel } from '../integration/models/integration.model';
|
||||||
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
|
|
||||||
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
|
||||||
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { SubmissionObject } from '../submission/models/submission-object.model';
|
import { SubmissionObject } from '../submission/models/submission-object.model';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
@@ -40,48 +37,6 @@ export class DSOSuccessResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A successful response containing a list of MetadataSchemas wrapped in a RegistryMetadataschemasResponse
|
|
||||||
*/
|
|
||||||
export class RegistryMetadataschemasSuccessResponse extends RestResponse {
|
|
||||||
constructor(
|
|
||||||
public metadataschemasResponse: RegistryMetadataschemasResponse,
|
|
||||||
public statusCode: number,
|
|
||||||
public statusText: string,
|
|
||||||
public pageInfo?: PageInfo
|
|
||||||
) {
|
|
||||||
super(true, statusCode, statusText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A successful response containing a list of MetadataFields wrapped in a RegistryMetadatafieldsResponse
|
|
||||||
*/
|
|
||||||
export class RegistryMetadatafieldsSuccessResponse extends RestResponse {
|
|
||||||
constructor(
|
|
||||||
public metadatafieldsResponse: RegistryMetadatafieldsResponse,
|
|
||||||
public statusCode: number,
|
|
||||||
public statusText: string,
|
|
||||||
public pageInfo?: PageInfo
|
|
||||||
) {
|
|
||||||
super(true, statusCode, statusText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A successful response containing a list of BitstreamFormats wrapped in a RegistryBitstreamformatsResponse
|
|
||||||
*/
|
|
||||||
export class RegistryBitstreamformatsSuccessResponse extends RestResponse {
|
|
||||||
constructor(
|
|
||||||
public bitstreamformatsResponse: RegistryBitstreamformatsResponse,
|
|
||||||
public statusCode: number,
|
|
||||||
public statusText: string,
|
|
||||||
public pageInfo?: PageInfo
|
|
||||||
) {
|
|
||||||
super(true, statusCode, statusText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A successful response containing exactly one MetadataSchema
|
* A successful response containing exactly one MetadataSchema
|
||||||
*/
|
*/
|
||||||
|
@@ -68,16 +68,11 @@ import { ItemDataService } from './data/item-data.service';
|
|||||||
import { LicenseDataService } from './data/license-data.service';
|
import { LicenseDataService } from './data/license-data.service';
|
||||||
import { LookupRelationService } from './data/lookup-relation.service';
|
import { LookupRelationService } from './data/lookup-relation.service';
|
||||||
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
||||||
import { MetadatafieldParsingService } from './data/metadatafield-parsing.service';
|
|
||||||
import { MetadataschemaParsingService } from './data/metadataschema-parsing.service';
|
|
||||||
import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service';
|
import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service';
|
||||||
import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
|
||||||
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
|
|
||||||
import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service';
|
|
||||||
import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service';
|
|
||||||
import { RelationshipTypeService } from './data/relationship-type.service';
|
import { RelationshipTypeService } from './data/relationship-type.service';
|
||||||
import { RelationshipService } from './data/relationship.service';
|
import { RelationshipService } from './data/relationship.service';
|
||||||
import { ResourcePolicyService } from './data/resource-policy.service';
|
import { ResourcePolicyService } from './resource-policy/resource-policy.service';
|
||||||
import { SearchResponseParsingService } from './data/search-response-parsing.service';
|
import { SearchResponseParsingService } from './data/search-response-parsing.service';
|
||||||
import { SiteDataService } from './data/site-data.service';
|
import { SiteDataService } from './data/site-data.service';
|
||||||
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
|
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
|
||||||
@@ -115,7 +110,7 @@ import { RelationshipType } from './shared/item-relationships/relationship-type.
|
|||||||
import { Relationship } from './shared/item-relationships/relationship.model';
|
import { Relationship } from './shared/item-relationships/relationship.model';
|
||||||
import { Item } from './shared/item.model';
|
import { Item } from './shared/item.model';
|
||||||
import { License } from './shared/license.model';
|
import { License } from './shared/license.model';
|
||||||
import { ResourcePolicy } from './shared/resource-policy.model';
|
import { ResourcePolicy } from './resource-policy/models/resource-policy.model';
|
||||||
import { SearchConfigurationService } from './shared/search/search-configuration.service';
|
import { SearchConfigurationService } from './shared/search/search-configuration.service';
|
||||||
import { SearchFilterService } from './shared/search/search-filter.service';
|
import { SearchFilterService } from './shared/search/search-filter.service';
|
||||||
import { SearchService } from './shared/search/search.service';
|
import { SearchService } from './shared/search/search.service';
|
||||||
@@ -149,6 +144,8 @@ import { ScriptDataService } from './data/processes/script-data.service';
|
|||||||
import { ProcessFilesResponseParsingService } from './data/process-files-response-parsing.service';
|
import { ProcessFilesResponseParsingService } from './data/process-files-response-parsing.service';
|
||||||
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
||||||
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
||||||
|
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
||||||
|
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -205,9 +202,6 @@ const PROVIDERS = [
|
|||||||
FacetValueResponseParsingService,
|
FacetValueResponseParsingService,
|
||||||
FacetValueMapResponseParsingService,
|
FacetValueMapResponseParsingService,
|
||||||
FacetConfigResponseParsingService,
|
FacetConfigResponseParsingService,
|
||||||
RegistryMetadataschemasResponseParsingService,
|
|
||||||
RegistryMetadatafieldsResponseParsingService,
|
|
||||||
RegistryBitstreamformatsResponseParsingService,
|
|
||||||
MappedCollectionsReponseParsingService,
|
MappedCollectionsReponseParsingService,
|
||||||
DebugResponseParsingService,
|
DebugResponseParsingService,
|
||||||
SearchResponseParsingService,
|
SearchResponseParsingService,
|
||||||
@@ -227,8 +221,6 @@ const PROVIDERS = [
|
|||||||
JsonPatchOperationsBuilder,
|
JsonPatchOperationsBuilder,
|
||||||
AuthorityService,
|
AuthorityService,
|
||||||
IntegrationResponseParsingService,
|
IntegrationResponseParsingService,
|
||||||
MetadataschemaParsingService,
|
|
||||||
MetadatafieldParsingService,
|
|
||||||
UploaderService,
|
UploaderService,
|
||||||
UUIDService,
|
UUIDService,
|
||||||
NotificationsService,
|
NotificationsService,
|
||||||
@@ -271,6 +263,8 @@ const PROVIDERS = [
|
|||||||
ProcessDataService,
|
ProcessDataService,
|
||||||
ScriptDataService,
|
ScriptDataService,
|
||||||
ProcessFilesResponseParsingService,
|
ProcessFilesResponseParsingService,
|
||||||
|
MetadataSchemaDataService,
|
||||||
|
MetadataFieldDataService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
@@ -12,7 +12,7 @@ import { PaginatedSearchOptions } from '../../shared/search/paginated-search-opt
|
|||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { SearchParam } from '../cache/models/search-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models';
|
import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
@@ -94,7 +94,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
|||||||
getAuthorizedCollectionByCommunity(communityId: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
getAuthorizedCollectionByCommunity(communityId: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||||
const searchHref = 'findAuthorizedByCommunity';
|
const searchHref = 'findAuthorizedByCommunity';
|
||||||
options = Object.assign({}, options, {
|
options = Object.assign({}, options, {
|
||||||
searchParams: [new SearchParam('uuid', communityId)]
|
searchParams: [new RequestParam('uuid', communityId)]
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.searchBy(searchHref, options).pipe(
|
return this.searchBy(searchHref, options).pipe(
|
||||||
|
@@ -20,7 +20,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
|||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { getClassForType } from '../cache/builders/build-decorators';
|
import { getClassForType } from '../cache/builders/build-decorators';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { SearchParam } from '../cache/models/search-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
||||||
@@ -45,11 +45,12 @@ import {
|
|||||||
FindListOptions,
|
FindListOptions,
|
||||||
FindListRequest,
|
FindListRequest,
|
||||||
GetRequest,
|
GetRequest,
|
||||||
PatchRequest
|
PatchRequest, PutRequest
|
||||||
} from './request.models';
|
} from './request.models';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RestRequestMethod } from './rest-request-method';
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
|
||||||
export abstract class DataService<T extends CacheableObject> {
|
export abstract class DataService<T extends CacheableObject> {
|
||||||
protected abstract requestService: RequestService;
|
protected abstract requestService: RequestService;
|
||||||
@@ -111,7 +112,7 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
result$ = this.getSearchEndpoint(searchMethod);
|
result$ = this.getSearchEndpoint(searchMethod);
|
||||||
|
|
||||||
if (hasValue(options.searchParams)) {
|
if (hasValue(options.searchParams)) {
|
||||||
options.searchParams.forEach((param: SearchParam) => {
|
options.searchParams.forEach((param: RequestParam) => {
|
||||||
args.push(`${param.fieldName}=${param.fieldValue}`);
|
args.push(`${param.fieldName}=${param.fieldValue}`);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -153,6 +154,33 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn an array of RequestParam into a query string and combine it with the given HREF
|
||||||
|
*
|
||||||
|
* @param href The HREF to which the query string should be appended
|
||||||
|
* @param params Array with additional params to combine with query string
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*
|
||||||
|
* @return {Observable<string>}
|
||||||
|
* Return an observable that emits created HREF
|
||||||
|
*/
|
||||||
|
protected buildHrefWithParams(href: string, params: RequestParam[], ...linksToFollow: Array<FollowLinkConfig<T>>): string {
|
||||||
|
|
||||||
|
let args = [];
|
||||||
|
if (hasValue(params)) {
|
||||||
|
params.forEach((param: RequestParam) => {
|
||||||
|
args.push(`${param.fieldName}=${param.fieldValue}`);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
args = this.addEmbedParams(args, ...linksToFollow);
|
||||||
|
|
||||||
|
if (isNotEmpty(args)) {
|
||||||
|
return new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||||
|
} else {
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Adds the embed options to the link for the request
|
* Adds the embed options to the link for the request
|
||||||
* @param args params for the query string
|
* @param args params for the query string
|
||||||
@@ -293,9 +321,9 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
* @param searchMethod The search method for the object
|
* @param searchMethod The search method for the object
|
||||||
*/
|
*/
|
||||||
protected getSearchEndpoint(searchMethod: string): Observable<string> {
|
protected getSearchEndpoint(searchMethod: string): Observable<string> {
|
||||||
return this.halService.getEndpoint(`${this.linkPath}/search`).pipe(
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
map((href: string) => `${href}/${searchMethod}`));
|
map((href: string) => `${href}/search/${searchMethod}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -316,7 +344,9 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
tap((href: string) => {
|
tap((href: string) => {
|
||||||
this.requestService.removeByHrefSubstring(href);
|
this.requestService.removeByHrefSubstring(href);
|
||||||
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
|
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
|
||||||
request.responseMsToLive = 10 * 1000;
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
|
||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
}
|
}
|
||||||
@@ -354,6 +384,28 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a PUT request for the specified object
|
||||||
|
*
|
||||||
|
* @param object The object to send a put request for.
|
||||||
|
*/
|
||||||
|
put(object: T): Observable<RemoteData<T>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object);
|
||||||
|
const request = new PutRequest(requestId, object._links.self.href, serializedObject);
|
||||||
|
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestService.configure(request);
|
||||||
|
|
||||||
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
find((re: RequestEntry) => hasValue(re) && re.completed),
|
||||||
|
switchMap(() => this.findByHref(object._links.self.href))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new patch to the object cache
|
* Add a new patch to the object cache
|
||||||
* The patch is derived from the differences between the given object and its version in the object cache
|
* The patch is derived from the differences between the given object and its version in the object cache
|
||||||
@@ -380,15 +432,15 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
*
|
*
|
||||||
* @param {DSpaceObject} dso
|
* @param {DSpaceObject} dso
|
||||||
* The object to create
|
* The object to create
|
||||||
* @param {string} parentUUID
|
* @param {RequestParam[]} params
|
||||||
* The UUID of the parent to create the new object under
|
* Array with additional params to combine with query string
|
||||||
*/
|
*/
|
||||||
create(dso: T, parentUUID: string): Observable<RemoteData<T>> {
|
create(dso: T, ...params: RequestParam[]): Observable<RemoteData<T>> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe(
|
const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
isNotEmptyOperator(),
|
isNotEmptyOperator(),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
map((endpoint: string) => parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint)
|
map((endpoint: string) => this.buildHrefWithParams(endpoint, params))
|
||||||
);
|
);
|
||||||
|
|
||||||
const serializedDso = new DSpaceSerializer(getClassForType((dso as any).type)).serialize(dso);
|
const serializedDso = new DSpaceSerializer(getClassForType((dso as any).type)).serialize(dso);
|
||||||
@@ -479,7 +531,7 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata);
|
const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata);
|
||||||
|
|
||||||
return this.requestService.getByUUID(requestId).pipe(
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
find((request: RequestEntry) => request.completed),
|
find((request: RequestEntry) => isNotEmpty(request) && request.completed),
|
||||||
map((request: RequestEntry) => request.response.isSuccessful)
|
map((request: RequestEntry) => request.response.isSuccessful)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
114
src/app/core/data/metadata-field-data.service.spec.ts
Normal file
114
src/app/core/data/metadata-field-data.service.spec.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { CreateRequest, FindListOptions, PutRequest } from './request.models';
|
||||||
|
import { MetadataFieldDataService } from './metadata-field-data.service';
|
||||||
|
import { MetadataField } from '../metadata/metadata-field.model';
|
||||||
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
|
||||||
|
describe('MetadataFieldDataService', () => {
|
||||||
|
let metadataFieldService: MetadataFieldDataService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
let schema: MetadataSchema;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
|
||||||
|
const endpoint = 'api/metadatafield/endpoint';
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
schema = Object.assign(new MetadataSchema(), {
|
||||||
|
prefix: 'dc',
|
||||||
|
namespace: 'namespace',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'selflink' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2',
|
||||||
|
configure: {},
|
||||||
|
getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }),
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
halService = Object.assign(new HALEndpointServiceStub(endpoint));
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||||
|
error: {}
|
||||||
|
});
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: createSuccessfulRemoteDataObject$(undefined)
|
||||||
|
});
|
||||||
|
metadataFieldService = new MetadataFieldDataService(requestService, rdbService, undefined, halService, undefined, undefined, undefined, notificationsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findBySchema', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(metadataFieldService, 'searchBy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call searchBy with the correct arguments', () => {
|
||||||
|
metadataFieldService.findBySchema(schema);
|
||||||
|
const expectedOptions = Object.assign(new FindListOptions(), {
|
||||||
|
searchParams: [new RequestParam('schema', schema.prefix)]
|
||||||
|
});
|
||||||
|
expect(metadataFieldService.searchBy).toHaveBeenCalledWith('bySchema', expectedOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createOrUpdateMetadataField', () => {
|
||||||
|
let field: MetadataField;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
field = Object.assign(new MetadataField(), {
|
||||||
|
element: 'identifier',
|
||||||
|
qualifier: undefined,
|
||||||
|
schema: schema,
|
||||||
|
_links: {
|
||||||
|
self: { href: 'selflink' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('called with a new metadata field', () => {
|
||||||
|
it('should send a CreateRequest', (done) => {
|
||||||
|
metadataFieldService.createOrUpdateMetadataField(field).subscribe(() => {
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(CreateRequest));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('called with an existing metadata field', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
field = Object.assign(field, {
|
||||||
|
id: 'id-of-existing-field'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send a PutRequest', (done) => {
|
||||||
|
metadataFieldService.createOrUpdateMetadataField(field).subscribe(() => {
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearRequests', () => {
|
||||||
|
it('should remove requests on the data service\'s endpoint', (done) => {
|
||||||
|
metadataFieldService.clearRequests().subscribe(() => {
|
||||||
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(`${endpoint}/${(metadataFieldService as any).linkPath}`);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
89
src/app/core/data/metadata-field-data.service.ts
Normal file
89
src/app/core/data/metadata-field-data.service.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
|
import { DataService } from './data.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { METADATA_FIELD } from '../metadata/metadata-field.resource-type';
|
||||||
|
import { MetadataField } from '../metadata/metadata-field.model';
|
||||||
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
|
import { FindListOptions, FindListRequest } from './request.models';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { find, skipWhile, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service responsible for fetching/sending data from/to the REST API on the metadatafields endpoint
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@dataService(METADATA_FIELD)
|
||||||
|
export class MetadataFieldDataService extends DataService<MetadataField> {
|
||||||
|
protected linkPath = 'metadatafields';
|
||||||
|
protected searchBySchemaLinkPath = 'bySchema';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<MetadataField>,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected notificationsService: NotificationsService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find metadata fields belonging to a metadata schema
|
||||||
|
* @param schema The metadata schema to list fields for
|
||||||
|
* @param options The options info used to retrieve the fields
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
findBySchema(schema: MetadataSchema, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>) {
|
||||||
|
const optionsWithSchema = Object.assign(new FindListOptions(), options, {
|
||||||
|
searchParams: [new RequestParam('schema', schema.prefix)]
|
||||||
|
});
|
||||||
|
return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or Update a MetadataField
|
||||||
|
* If the MetadataField contains an id, it is assumed the field already exists and is updated instead
|
||||||
|
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||||
|
* - On creation, a CreateRequest is used
|
||||||
|
* - On update, a PutRequest is used
|
||||||
|
* @param field The MetadataField to create or update
|
||||||
|
*/
|
||||||
|
createOrUpdateMetadataField(field: MetadataField): Observable<RemoteData<MetadataField>> {
|
||||||
|
const isUpdate = hasValue(field.id);
|
||||||
|
|
||||||
|
if (isUpdate) {
|
||||||
|
return this.put(field);
|
||||||
|
} else {
|
||||||
|
return this.create(field, new RequestParam('schemaId', field.schema.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all metadata field requests
|
||||||
|
* Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema
|
||||||
|
*/
|
||||||
|
clearRequests(): Observable<string> {
|
||||||
|
return this.getBrowseEndpoint().pipe(
|
||||||
|
tap((href: string) => {
|
||||||
|
this.requestService.removeByHrefSubstring(href);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
89
src/app/core/data/metadata-schema-data.service.spec.ts
Normal file
89
src/app/core/data/metadata-schema-data.service.spec.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { MetadataSchemaDataService } from './metadata-schema-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
|
import { CreateRequest, PutRequest } from './request.models';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
|
||||||
|
describe('MetadataSchemaDataService', () => {
|
||||||
|
let metadataSchemaService: MetadataSchemaDataService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
|
||||||
|
const endpoint = 'api/metadataschema/endpoint';
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2',
|
||||||
|
configure: {},
|
||||||
|
getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }),
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
halService = Object.assign(new HALEndpointServiceStub(endpoint));
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||||
|
error: {}
|
||||||
|
});
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: createSuccessfulRemoteDataObject$(undefined)
|
||||||
|
});
|
||||||
|
metadataSchemaService = new MetadataSchemaDataService(requestService, rdbService, undefined, halService, undefined, undefined, undefined, notificationsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createOrUpdateMetadataSchema', () => {
|
||||||
|
let schema: MetadataSchema;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
schema = Object.assign(new MetadataSchema(), {
|
||||||
|
prefix: 'dc',
|
||||||
|
namespace: 'namespace',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'selflink' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('called with a new metadata schema', () => {
|
||||||
|
it('should send a CreateRequest', (done) => {
|
||||||
|
metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => {
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(CreateRequest));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('called with an existing metadata schema', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
schema = Object.assign(schema, {
|
||||||
|
id: 'id-of-existing-schema'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send a PutRequest', (done) => {
|
||||||
|
metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => {
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearRequests', () => {
|
||||||
|
it('should remove requests on the data service\'s endpoint', (done) => {
|
||||||
|
metadataSchemaService.clearRequests().subscribe(() => {
|
||||||
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(`${endpoint}/${(metadataSchemaService as any).linkPath}`);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -9,37 +9,21 @@ import { CoreState } from '../core.reducers';
|
|||||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
import { METADATA_SCHEMA } from '../metadata/metadata-schema.resource-type';
|
import { METADATA_SCHEMA } from '../metadata/metadata-schema.resource-type';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { ChangeAnalyzer } from './change-analyzer';
|
|
||||||
|
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
/* tslint:disable:max-classes-per-file */
|
import { hasValue } from '../../shared/empty.util';
|
||||||
class DataServiceImpl extends DataService<MetadataSchema> {
|
import { tap } from 'rxjs/operators';
|
||||||
protected linkPath = 'metadataschemas';
|
import { RemoteData } from './remote-data';
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected requestService: RequestService,
|
|
||||||
protected rdbService: RemoteDataBuildService,
|
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
|
||||||
protected halService: HALEndpointService,
|
|
||||||
protected notificationsService: NotificationsService,
|
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: ChangeAnalyzer<MetadataSchema>) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint
|
* A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(METADATA_SCHEMA)
|
@dataService(METADATA_SCHEMA)
|
||||||
export class MetadataSchemaDataService {
|
export class MetadataSchemaDataService extends DataService<MetadataSchema> {
|
||||||
private dataService: DataServiceImpl;
|
protected linkPath = 'metadataschemas';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -50,6 +34,35 @@ export class MetadataSchemaDataService {
|
|||||||
protected comparator: DefaultChangeAnalyzer<MetadataSchema>,
|
protected comparator: DefaultChangeAnalyzer<MetadataSchema>,
|
||||||
protected http: HttpClient,
|
protected http: HttpClient,
|
||||||
protected notificationsService: NotificationsService) {
|
protected notificationsService: NotificationsService) {
|
||||||
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or Update a MetadataSchema
|
||||||
|
* If the MetadataSchema contains an id, it is assumed the schema already exists and is updated instead
|
||||||
|
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||||
|
* - On creation, a CreateRequest is used
|
||||||
|
* - On update, a PutRequest is used
|
||||||
|
* @param schema The MetadataSchema to create or update
|
||||||
|
*/
|
||||||
|
createOrUpdateMetadataSchema(schema: MetadataSchema): Observable<RemoteData<MetadataSchema>> {
|
||||||
|
const isUpdate = hasValue(schema.id);
|
||||||
|
|
||||||
|
if (isUpdate) {
|
||||||
|
return this.put(schema);
|
||||||
|
} else {
|
||||||
|
return this.create(schema);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all metadata schema requests
|
||||||
|
* Used for refreshing lists after adding/updating/removing a metadata schema in the registry
|
||||||
|
*/
|
||||||
|
clearRequests(): Observable<string> {
|
||||||
|
return this.getBrowseEndpoint().pipe(
|
||||||
|
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,22 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { MetadatafieldSuccessResponse, RestResponse } from '../cache/response.models';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
|
||||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
|
||||||
import { MetadataField } from '../metadata/metadata-field.model';
|
|
||||||
import { ResponseParsingService } from './parsing.service';
|
|
||||||
import { RestRequest } from './request.models';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A service responsible for parsing DSpaceRESTV2Response data related to a single MetadataField to a valid RestResponse
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class MetadatafieldParsingService implements ResponseParsingService {
|
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
|
||||||
const payload = data.payload;
|
|
||||||
|
|
||||||
const deserialized = new DSpaceSerializer(MetadataField).deserialize(payload);
|
|
||||||
return new MetadatafieldSuccessResponse(deserialized, data.statusCode, data.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,19 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
|
||||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
|
||||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
|
||||||
import { ResponseParsingService } from './parsing.service';
|
|
||||||
import { RestRequest } from './request.models';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class MetadataschemaParsingService implements ResponseParsingService {
|
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
|
||||||
const payload = data.payload;
|
|
||||||
|
|
||||||
const deserialized = new DSpaceSerializer(MetadataSchema).deserialize(payload);
|
|
||||||
return new MetadataschemaSuccessResponse(deserialized, data.statusCode, data.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,41 +0,0 @@
|
|||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
|
||||||
import {
|
|
||||||
RegistryBitstreamformatsSuccessResponse
|
|
||||||
} from '../cache/response.models';
|
|
||||||
import { RegistryBitstreamformatsResponseParsingService } from './registry-bitstreamformats-response-parsing.service';
|
|
||||||
|
|
||||||
describe('RegistryBitstreamformatsResponseParsingService', () => {
|
|
||||||
let service: RegistryBitstreamformatsResponseParsingService;
|
|
||||||
|
|
||||||
const mockDSOParser = Object.assign({
|
|
||||||
processPageInfo: () => new PageInfo()
|
|
||||||
}) as DSOResponseParsingService;
|
|
||||||
|
|
||||||
const data = Object.assign({
|
|
||||||
payload: {
|
|
||||||
_embedded: {
|
|
||||||
bitstreamformats: [
|
|
||||||
{
|
|
||||||
uuid: 'uuid-1',
|
|
||||||
description: 'a description'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: 'uuid-2',
|
|
||||||
description: 'another description'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) as DSpaceRESTV2Response;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
service = new RegistryBitstreamformatsResponseParsingService(mockDSOParser);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse the data correctly', () => {
|
|
||||||
const response = service.parse(null, data);
|
|
||||||
expect(response.constructor).toBe(RegistryBitstreamformatsSuccessResponse);
|
|
||||||
});
|
|
||||||
});
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user