mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Fixed conflicts
This commit is contained in:
@@ -32,12 +32,60 @@ cache:
|
|||||||
# NOTE: how long should objects be cached for by default
|
# NOTE: how long should objects be cached for by default
|
||||||
msToLive:
|
msToLive:
|
||||||
default: 900000 # 15 minutes
|
default: 900000 # 15 minutes
|
||||||
control: max-age=60 # revalidate browser
|
# Default 'Cache-Control' HTTP Header to set for all static content (including compiled *.js files)
|
||||||
|
# Defaults to max-age=604,800 seconds (one week). This lets a user's browser know that it can cache these
|
||||||
|
# files for one week, after which they will be "stale" and need to be redownloaded.
|
||||||
|
# NOTE: When updates are made to compiled *.js files, it will automatically bypass this browser cache, because
|
||||||
|
# all compiled *.js files include a unique hash in their name which updates when content is modified.
|
||||||
|
control: max-age=604800 # revalidate browser
|
||||||
autoSync:
|
autoSync:
|
||||||
defaultTime: 0
|
defaultTime: 0
|
||||||
maxBufferSize: 100
|
maxBufferSize: 100
|
||||||
timePerMethod:
|
timePerMethod:
|
||||||
PATCH: 3 # time in seconds
|
PATCH: 3 # time in seconds
|
||||||
|
# In-memory cache(s) of server-side rendered pages. These caches will store the most recently accessed public pages.
|
||||||
|
# Pages are automatically added/dropped from these caches based on how recently they have been used.
|
||||||
|
# Restarting the app clears all page caches.
|
||||||
|
# NOTE: To control the cache size, use the "max" setting. Keep in mind, individual cached pages are usually small (<100KB).
|
||||||
|
# Enabling *both* caches will mean that a page may be cached twice, once in each cache (but may expire at different times via timeToLive).
|
||||||
|
serverSide:
|
||||||
|
# Set to true to see all cache hits/misses/refreshes in your console logs. Useful for debugging SSR caching issues.
|
||||||
|
debug: false
|
||||||
|
# When enabled (i.e. max > 0), known bots will be sent pages from a server side cache specific for bots.
|
||||||
|
# (Keep in mind, bot detection cannot be guarranteed. It is possible some bots will bypass this cache.)
|
||||||
|
botCache:
|
||||||
|
# Maximum number of pages to cache for known bots. Set to zero (0) to disable server side caching for bots.
|
||||||
|
# Default is 1000, which means the 1000 most recently accessed public pages will be cached.
|
||||||
|
# As all pages are cached in server memory, increasing this value will increase memory needs.
|
||||||
|
# Individual cached pages are usually small (<100KB), so max=1000 should only require ~100MB of memory.
|
||||||
|
max: 1000
|
||||||
|
# Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached
|
||||||
|
# copy is automatically refreshed on the next request.
|
||||||
|
# NOTE: For the bot cache, this setting may impact how quickly search engine bots will index new content on your site.
|
||||||
|
# For example, setting this to one week may mean that search engine bots may not find all new content for one week.
|
||||||
|
timeToLive: 86400000 # 1 day
|
||||||
|
# When set to true, after timeToLive expires, the next request will receive the *cached* page & then re-render the page
|
||||||
|
# behind the scenes to update the cache. This ensures users primarily interact with the cache, but may receive stale pages (older than timeToLive).
|
||||||
|
# When set to false, after timeToLive expires, the next request will wait on SSR to complete & receive a fresh page (which is then saved to cache).
|
||||||
|
# This ensures stale pages (older than timeToLive) are never returned from the cache, but some users will wait on SSR.
|
||||||
|
allowStale: true
|
||||||
|
# When enabled (i.e. max > 0), all anonymous users will be sent pages from a server side cache.
|
||||||
|
# This allows anonymous users to interact more quickly with the site, but also means they may see slightly
|
||||||
|
# outdated content (based on timeToLive)
|
||||||
|
anonymousCache:
|
||||||
|
# Maximum number of pages to cache. Default is zero (0) which means anonymous user cache is disabled.
|
||||||
|
# As all pages are cached in server memory, increasing this value will increase memory needs.
|
||||||
|
# Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory.
|
||||||
|
max: 0
|
||||||
|
# Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached
|
||||||
|
# copy is automatically refreshed on the next request.
|
||||||
|
# NOTE: For the anonymous cache, it is recommended to keep this value low to avoid anonymous users seeing outdated content.
|
||||||
|
timeToLive: 10000 # 10 seconds
|
||||||
|
# When set to true, after timeToLive expires, the next request will receive the *cached* page & then re-render the page
|
||||||
|
# behind the scenes to update the cache. This ensures users primarily interact with the cache, but may receive stale pages (older than timeToLive).
|
||||||
|
# When set to false, after timeToLive expires, the next request will wait on SSR to complete & receive a fresh page (which is then saved to cache).
|
||||||
|
# This ensures stale pages (older than timeToLive) are never returned from the cache, but some users will wait on SSR.
|
||||||
|
allowStale: true
|
||||||
|
|
||||||
# Authentication settings
|
# Authentication settings
|
||||||
auth:
|
auth:
|
||||||
@@ -121,6 +169,9 @@ languages:
|
|||||||
- code: en
|
- code: en
|
||||||
label: English
|
label: English
|
||||||
active: true
|
active: true
|
||||||
|
- code: ca
|
||||||
|
label: Català
|
||||||
|
active: true
|
||||||
- code: cs
|
- code: cs
|
||||||
label: Čeština
|
label: Čeština
|
||||||
active: true
|
active: true
|
||||||
@@ -310,3 +361,16 @@ info:
|
|||||||
markdown:
|
markdown:
|
||||||
enabled: false
|
enabled: false
|
||||||
mathjax: false
|
mathjax: false
|
||||||
|
|
||||||
|
# Which vocabularies should be used for which search filters
|
||||||
|
# and whether to show the filter in the search sidebar
|
||||||
|
# Take a look at the filter-vocabulary-config.ts file for documentation on how the options are obtained
|
||||||
|
vocabularies:
|
||||||
|
- filter: 'subject'
|
||||||
|
vocabulary: 'srsc'
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query.
|
||||||
|
comcolSelectionSort:
|
||||||
|
sortField: 'dc.title'
|
||||||
|
sortDirection: 'ASC'
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dspace-angular",
|
"name": "dspace-angular",
|
||||||
"version": "7.5.0-next",
|
"version": "7.6.0-next",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"config:watch": "nodemon",
|
"config:watch": "nodemon",
|
||||||
@@ -93,11 +93,13 @@
|
|||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"date-fns-tz": "^1.3.7",
|
"date-fns-tz": "^1.3.7",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
|
"ejs": "^3.1.8",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-rate-limit": "^5.1.3",
|
"express-rate-limit": "^5.1.3",
|
||||||
"fast-json-patch": "^3.0.0-1",
|
"fast-json-patch": "^3.0.0-1",
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
"http-proxy-middleware": "^1.0.5",
|
"http-proxy-middleware": "^1.0.5",
|
||||||
|
"isbot": "^3.6.5",
|
||||||
"js-cookie": "2.2.1",
|
"js-cookie": "2.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json5": "^2.2.2",
|
"json5": "^2.2.2",
|
||||||
@@ -105,6 +107,7 @@
|
|||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"klaro": "^0.7.18",
|
"klaro": "^0.7.18",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"lru-cache": "^7.14.1",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"markdown-it-mathjax3": "^4.3.1",
|
"markdown-it-mathjax3": "^4.3.1",
|
||||||
"mirador": "^3.3.0",
|
"mirador": "^3.3.0",
|
||||||
@@ -147,6 +150,7 @@
|
|||||||
"@ngtools/webpack": "^13.2.6",
|
"@ngtools/webpack": "^13.2.6",
|
||||||
"@nguniversal/builders": "^13.1.1",
|
"@nguniversal/builders": "^13.1.1",
|
||||||
"@types/deep-freeze": "0.1.2",
|
"@types/deep-freeze": "0.1.2",
|
||||||
|
"@types/ejs": "^3.1.1",
|
||||||
"@types/express": "^4.17.9",
|
"@types/express": "^4.17.9",
|
||||||
"@types/jasmine": "~3.6.0",
|
"@types/jasmine": "~3.6.0",
|
||||||
"@types/js-cookie": "2.2.6",
|
"@types/js-cookie": "2.2.6",
|
||||||
@@ -194,7 +198,7 @@
|
|||||||
"sass-resources-loader": "^2.1.1",
|
"sass-resources-loader": "^2.1.1",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
"typescript": "~4.5.5",
|
"typescript": "~4.5.5",
|
||||||
"webpack": "^5.69.1",
|
"webpack": "^5.76.0",
|
||||||
"webpack-bundle-analyzer": "^4.4.0",
|
"webpack-bundle-analyzer": "^4.4.0",
|
||||||
"webpack-cli": "^4.2.0",
|
"webpack-cli": "^4.2.0",
|
||||||
"webpack-dev-server": "^4.5.0"
|
"webpack-dev-server": "^4.5.0"
|
||||||
|
276
server.ts
276
server.ts
@@ -22,11 +22,14 @@ import 'rxjs';
|
|||||||
/* eslint-disable import/no-namespace */
|
/* eslint-disable import/no-namespace */
|
||||||
import * as morgan from 'morgan';
|
import * as morgan from 'morgan';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
|
import * as ejs from 'ejs';
|
||||||
import * as compression from 'compression';
|
import * as compression from 'compression';
|
||||||
import * as expressStaticGzip from 'express-static-gzip';
|
import * as expressStaticGzip from 'express-static-gzip';
|
||||||
/* eslint-enable import/no-namespace */
|
/* eslint-enable import/no-namespace */
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import LRU from 'lru-cache';
|
||||||
|
import isbot from 'isbot';
|
||||||
import { createCertificate } from 'pem';
|
import { createCertificate } from 'pem';
|
||||||
import { createServer } from 'https';
|
import { createServer } from 'https';
|
||||||
import { json } from 'body-parser';
|
import { json } from 'body-parser';
|
||||||
@@ -34,7 +37,6 @@ import { json } from 'body-parser';
|
|||||||
import { existsSync, readFileSync } from 'fs';
|
import { existsSync, readFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
|
||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from '@angular/core';
|
||||||
|
|
||||||
import { ngExpressEngine } from '@nguniversal/express-engine';
|
import { ngExpressEngine } from '@nguniversal/express-engine';
|
||||||
@@ -52,6 +54,8 @@ import { buildAppConfig } from './src/config/config.server';
|
|||||||
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
|
||||||
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
||||||
import { logStartupMessage } from './startup-message';
|
import { logStartupMessage } from './startup-message';
|
||||||
|
import { TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set path for the browser application's dist folder
|
* Set path for the browser application's dist folder
|
||||||
@@ -60,12 +64,18 @@ const DIST_FOLDER = join(process.cwd(), 'dist/browser');
|
|||||||
// Set path fir IIIF viewer.
|
// Set path fir IIIF viewer.
|
||||||
const IIIF_VIEWER = join(process.cwd(), 'dist/iiif');
|
const IIIF_VIEWER = join(process.cwd(), 'dist/iiif');
|
||||||
|
|
||||||
const indexHtml = existsSync(join(DIST_FOLDER, 'index.html')) ? 'index.html' : 'index';
|
const indexHtml = join(DIST_FOLDER, 'index.html');
|
||||||
|
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
|
|
||||||
const appConfig: AppConfig = buildAppConfig(join(DIST_FOLDER, 'assets/config.json'));
|
const appConfig: AppConfig = buildAppConfig(join(DIST_FOLDER, 'assets/config.json'));
|
||||||
|
|
||||||
|
// cache of SSR pages for known bots, only enabled in production mode
|
||||||
|
let botCache: LRU<string, any>;
|
||||||
|
|
||||||
|
// cache of SSR pages for anonymous users. Disabled by default, and only available in production mode
|
||||||
|
let anonymousCache: LRU<string, any>;
|
||||||
|
|
||||||
// extend environment with app config for server
|
// extend environment with app config for server
|
||||||
extendEnvironmentWithAppConfig(environment, appConfig);
|
extendEnvironmentWithAppConfig(environment, appConfig);
|
||||||
|
|
||||||
@@ -86,10 +96,12 @@ export function app() {
|
|||||||
/*
|
/*
|
||||||
* If production mode is enabled in the environment file:
|
* If production mode is enabled in the environment file:
|
||||||
* - Enable Angular's production mode
|
* - Enable Angular's production mode
|
||||||
|
* - Initialize caching of SSR rendered pages (if enabled in config.yml)
|
||||||
* - Enable compression for SSR reponses. See [compression](https://github.com/expressjs/compression)
|
* - Enable compression for SSR reponses. See [compression](https://github.com/expressjs/compression)
|
||||||
*/
|
*/
|
||||||
if (environment.production) {
|
if (environment.production) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
|
initCache();
|
||||||
server.use(compression({
|
server.use(compression({
|
||||||
// only compress responses we've marked as SSR
|
// only compress responses we've marked as SSR
|
||||||
// otherwise, this middleware may compress files we've chosen not to compress via compression-webpack-plugin
|
// otherwise, this middleware may compress files we've chosen not to compress via compression-webpack-plugin
|
||||||
@@ -105,13 +117,13 @@ export function app() {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Add cookie parser middleware
|
* Add cookie parser middleware
|
||||||
* See [morgan](https://github.com/expressjs/cookie-parser)
|
* See [cookie-parser](https://github.com/expressjs/cookie-parser)
|
||||||
*/
|
*/
|
||||||
server.use(cookieParser());
|
server.use(cookieParser());
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Add parser for request bodies
|
* Add JSON parser for request bodies
|
||||||
* See [morgan](https://github.com/expressjs/body-parser)
|
* See [body-parser](https://github.com/expressjs/body-parser)
|
||||||
*/
|
*/
|
||||||
server.use(json());
|
server.use(json());
|
||||||
|
|
||||||
@@ -136,10 +148,23 @@ export function app() {
|
|||||||
})(_, (options as any), callback)
|
})(_, (options as any), callback)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
server.engine('ejs', ejs.renderFile);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Register the view engines for html and ejs
|
* Register the view engines for html and ejs
|
||||||
*/
|
*/
|
||||||
server.set('view engine', 'html');
|
server.set('view engine', 'html');
|
||||||
|
server.set('view engine', 'ejs');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve the robots.txt ejs template, filling in the origin variable
|
||||||
|
*/
|
||||||
|
server.get('/robots.txt', (req, res) => {
|
||||||
|
res.setHeader('content-type', 'text/plain');
|
||||||
|
res.render('assets/robots.txt.ejs', {
|
||||||
|
'origin': req.protocol + '://' + req.headers.host
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set views folder path to directory where template files are stored
|
* Set views folder path to directory where template files are stored
|
||||||
@@ -172,7 +197,7 @@ export function app() {
|
|||||||
* Serve static resources (images, i18n messages, …)
|
* Serve static resources (images, i18n messages, …)
|
||||||
* Handle pre-compressed files with [express-static-gzip](https://github.com/tkoenig89/express-static-gzip)
|
* Handle pre-compressed files with [express-static-gzip](https://github.com/tkoenig89/express-static-gzip)
|
||||||
*/
|
*/
|
||||||
router.get('*.*', cacheControl, expressStaticGzip(DIST_FOLDER, {
|
router.get('*.*', addCacheControl, expressStaticGzip(DIST_FOLDER, {
|
||||||
index: false,
|
index: false,
|
||||||
enableBrotli: true,
|
enableBrotli: true,
|
||||||
orderPreference: ['br', 'gzip'],
|
orderPreference: ['br', 'gzip'],
|
||||||
@@ -188,8 +213,11 @@ export function app() {
|
|||||||
*/
|
*/
|
||||||
server.get('/app/health', healthCheck);
|
server.get('/app/health', healthCheck);
|
||||||
|
|
||||||
// Register the ngApp callback function to handle incoming requests
|
/**
|
||||||
router.get('*', ngApp);
|
* Default sending all incoming requests to ngApp() function, after first checking for a cached
|
||||||
|
* copy of the page (see cacheCheck())
|
||||||
|
*/
|
||||||
|
router.get('*', cacheCheck, ngApp);
|
||||||
|
|
||||||
server.use(environment.ui.nameSpace, router);
|
server.use(environment.ui.nameSpace, router);
|
||||||
|
|
||||||
@@ -201,6 +229,25 @@ export function app() {
|
|||||||
*/
|
*/
|
||||||
function ngApp(req, res) {
|
function ngApp(req, res) {
|
||||||
if (environment.universal.preboot) {
|
if (environment.universal.preboot) {
|
||||||
|
// Render the page to user via SSR (server side rendering)
|
||||||
|
serverSideRender(req, res);
|
||||||
|
} else {
|
||||||
|
// If preboot is disabled, just serve the client
|
||||||
|
console.log('Universal off, serving for direct client-side rendering (CSR)');
|
||||||
|
clientSideRender(req, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render page content on server side using Angular SSR. By default this page content is
|
||||||
|
* returned to the user.
|
||||||
|
* @param req current request
|
||||||
|
* @param res current response
|
||||||
|
* @param sendToUser if true (default), send the rendered content to the user.
|
||||||
|
* If false, then only save this rendered content to the in-memory cache (to refresh cache).
|
||||||
|
*/
|
||||||
|
function serverSideRender(req, res, sendToUser: boolean = true) {
|
||||||
|
// Render the page via SSR (server side rendering)
|
||||||
res.render(indexHtml, {
|
res.render(indexHtml, {
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@@ -210,51 +257,214 @@ function ngApp(req, res) {
|
|||||||
baseUrl: environment.ui.nameSpace,
|
baseUrl: environment.ui.nameSpace,
|
||||||
originUrl: environment.ui.baseUrl,
|
originUrl: environment.ui.baseUrl,
|
||||||
requestUrl: req.originalUrl,
|
requestUrl: req.originalUrl,
|
||||||
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
|
|
||||||
}, (err, data) => {
|
}, (err, data) => {
|
||||||
if (hasNoValue(err) && hasValue(data)) {
|
if (hasNoValue(err) && hasValue(data)) {
|
||||||
res.locals.ssr = true; // mark response as SSR
|
// save server side rendered page to cache (if any are enabled)
|
||||||
|
saveToCache(req, data);
|
||||||
|
if (sendToUser) {
|
||||||
|
res.locals.ssr = true; // mark response as SSR (enables text compression)
|
||||||
|
// send rendered page to user
|
||||||
res.send(data);
|
res.send(data);
|
||||||
|
}
|
||||||
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
||||||
// When this error occurs we can't fall back to CSR because the response has already been
|
// When this error occurs we can't fall back to CSR because the response has already been
|
||||||
// sent. These errors occur for various reasons in universal, not all of which are in our
|
// sent. These errors occur for various reasons in universal, not all of which are in our
|
||||||
// control to solve.
|
// control to solve.
|
||||||
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
|
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
|
||||||
} else {
|
} else {
|
||||||
console.warn('Error in SSR, serving for direct CSR.');
|
console.warn('Error in server-side rendering (SSR)');
|
||||||
if (hasValue(err)) {
|
if (hasValue(err)) {
|
||||||
console.warn('Error details : ', err);
|
console.warn('Error details : ', err);
|
||||||
}
|
}
|
||||||
res.render(indexHtml, {
|
if (sendToUser) {
|
||||||
req,
|
console.warn('Falling back to serving direct client-side rendering (CSR).');
|
||||||
providers: [{
|
clientSideRender(req, res);
|
||||||
provide: APP_BASE_HREF,
|
}
|
||||||
useValue: req.baseUrl
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
// If preboot is disabled, just serve the client
|
|
||||||
console.log('Universal off, serving for direct CSR');
|
/**
|
||||||
res.render(indexHtml, {
|
* Send back response to user to trigger direct client-side rendering (CSR)
|
||||||
req,
|
* @param req current request
|
||||||
providers: [{
|
* @param res current response
|
||||||
provide: APP_BASE_HREF,
|
*/
|
||||||
useValue: req.baseUrl
|
function clientSideRender(req, res) {
|
||||||
}]
|
res.sendFile(indexHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Adds a Cache-Control HTTP header to the response.
|
||||||
|
* The cache control value can be configured in the config.*.yml file
|
||||||
|
* Defaults to max-age=604,800 seconds (1 week)
|
||||||
|
*/
|
||||||
|
function addCacheControl(req, res, next) {
|
||||||
|
// instruct browser to revalidate
|
||||||
|
res.header('Cache-Control', environment.cache.control || 'max-age=604800');
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initialize server-side caching of pages rendered via SSR.
|
||||||
|
*/
|
||||||
|
function initCache() {
|
||||||
|
if (botCacheEnabled()) {
|
||||||
|
// Initialize a new "least-recently-used" item cache (where least recently used pages are removed first)
|
||||||
|
// See https://www.npmjs.com/package/lru-cache
|
||||||
|
// When enabled, each page defaults to expiring after 1 day
|
||||||
|
botCache = new LRU( {
|
||||||
|
max: environment.cache.serverSide.botCache.max,
|
||||||
|
ttl: environment.cache.serverSide.botCache.timeToLive || 24 * 60 * 60 * 1000, // 1 day
|
||||||
|
allowStale: environment.cache.serverSide.botCache.allowStale ?? true // if object is stale, return stale value before deleting
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anonymousCacheEnabled()) {
|
||||||
|
// NOTE: While caches may share SSR pages, this cache must be kept separately because the timeToLive
|
||||||
|
// may expire pages more frequently.
|
||||||
|
// When enabled, each page defaults to expiring after 10 seconds (to minimize anonymous users seeing out-of-date content)
|
||||||
|
anonymousCache = new LRU( {
|
||||||
|
max: environment.cache.serverSide.anonymousCache.max,
|
||||||
|
ttl: environment.cache.serverSide.anonymousCache.timeToLive || 10 * 1000, // 10 seconds
|
||||||
|
allowStale: environment.cache.serverSide.anonymousCache.allowStale ?? true // if object is stale, return stale value before deleting
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Adds a cache control header to the response
|
* Return whether bot-specific server side caching is enabled in configuration.
|
||||||
* The cache control value can be configured in the environments file and defaults to max-age=60
|
|
||||||
*/
|
*/
|
||||||
function cacheControl(req, res, next) {
|
function botCacheEnabled(): boolean {
|
||||||
// instruct browser to revalidate
|
// Caching is only enabled if SSR is enabled AND
|
||||||
res.header('Cache-Control', environment.cache.control || 'max-age=60');
|
// "max" pages to cache is greater than zero
|
||||||
|
return environment.universal.preboot && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether anonymous user server side caching is enabled in configuration.
|
||||||
|
*/
|
||||||
|
function anonymousCacheEnabled(): boolean {
|
||||||
|
// Caching is only enabled if SSR is enabled AND
|
||||||
|
// "max" pages to cache is greater than zero
|
||||||
|
return environment.universal.preboot && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the currently requested page is in our server-side, in-memory cache.
|
||||||
|
* Caching is ONLY done for SSR requests. Pages are cached base on their path (e.g. /home or /search?query=test)
|
||||||
|
*/
|
||||||
|
function cacheCheck(req, res, next) {
|
||||||
|
// Cached copy of page (if found)
|
||||||
|
let cachedCopy;
|
||||||
|
|
||||||
|
// If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page.
|
||||||
|
if (botCacheEnabled() && isbot(req.get('user-agent'))) {
|
||||||
|
cachedCopy = checkCacheForRequest('bot', botCache, req, res);
|
||||||
|
} else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) {
|
||||||
|
cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If cached copy exists, return it to the user.
|
||||||
|
if (cachedCopy) {
|
||||||
|
res.locals.ssr = true; // mark response as SSR-generated (enables text compression)
|
||||||
|
res.send(cachedCopy);
|
||||||
|
|
||||||
|
// Tell Express to skip all other handlers for this path
|
||||||
|
// This ensures we don't try to re-render the page since we've already returned the cached copy
|
||||||
|
next('router');
|
||||||
|
} else {
|
||||||
|
// If nothing found in cache, just continue with next handler
|
||||||
|
// (This should send the request on to the handler that rerenders the page via SSR
|
||||||
next();
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current request (i.e. page) is found in the given cache. If it is found,
|
||||||
|
* the cached copy is returned. When found, this method also triggers a re-render via
|
||||||
|
* SSR if the cached copy is now expired (i.e. timeToLive has passed for this cached copy).
|
||||||
|
* @param cacheName name of cache (just useful for debug logging)
|
||||||
|
* @param cache LRU cache to check
|
||||||
|
* @param req current request to look for in the cache
|
||||||
|
* @param res current response
|
||||||
|
* @returns cached copy (if found) or undefined (if not found)
|
||||||
|
*/
|
||||||
|
function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, res): any {
|
||||||
|
// Get the cache key for this request
|
||||||
|
const key = getCacheKey(req);
|
||||||
|
|
||||||
|
// Check if this page is in our cache
|
||||||
|
let cachedCopy = cache.get(key);
|
||||||
|
if (cachedCopy) {
|
||||||
|
if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); }
|
||||||
|
|
||||||
|
// Check if cached copy is expired (If expired, the key will now be gone from cache)
|
||||||
|
// NOTE: This will only occur when "allowStale=true", as it means the "get(key)" above returned a stale value.
|
||||||
|
if (!cache.has(key)) {
|
||||||
|
if (environment.cache.serverSide.debug) { console.log(`CACHE EXPIRED FOR ${key} in ${cacheName} cache. Re-rendering...`); }
|
||||||
|
// Update cached copy by rerendering server-side
|
||||||
|
// NOTE: In this scenario the currently cached copy will be returned to the current user.
|
||||||
|
// This re-render is peformed behind the scenes to update cached copy for next user.
|
||||||
|
serverSideRender(req, res, false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// return page from cache
|
||||||
|
return cachedCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a cache key from the current request.
|
||||||
|
* The cache key is the URL path (NOTE: this key will also include any querystring params).
|
||||||
|
* E.g. "/home" or "/search?query=test"
|
||||||
|
* @param req current request
|
||||||
|
* @returns cache key to use for this page
|
||||||
|
*/
|
||||||
|
function getCacheKey(req): string {
|
||||||
|
// NOTE: this will return the URL path *without* any baseUrl
|
||||||
|
return req.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save page to server side cache(s), if enabled. If caching is not enabled or a user is authenticated, this is a noop
|
||||||
|
* If multiple caches are enabled, the page will be saved to any caches where it does not yet exist (or is expired).
|
||||||
|
* (This minimizes the number of times we need to run SSR on the same page.)
|
||||||
|
* @param req current page request
|
||||||
|
* @param page page data to save to cache
|
||||||
|
*/
|
||||||
|
function saveToCache(req, page: any) {
|
||||||
|
// Only cache if no one is currently authenticated. This means ONLY public pages can be cached.
|
||||||
|
// NOTE: It's not safe to save page data to the cache when a user is authenticated. In that situation,
|
||||||
|
// the page may include sensitive or user-specific materials. As the cache is shared across all users, it can only contain public info.
|
||||||
|
if (!isUserAuthenticated(req)) {
|
||||||
|
const key = getCacheKey(req);
|
||||||
|
// Avoid caching "/reload/[random]" paths (these are hard refreshes after logout)
|
||||||
|
if (key.startsWith('/reload')) { return; }
|
||||||
|
|
||||||
|
// If bot cache is enabled, save it to that cache if it doesn't exist or is expired
|
||||||
|
// (NOTE: has() will return false if page is expired in cache)
|
||||||
|
if (botCacheEnabled() && !botCache.has(key)) {
|
||||||
|
botCache.set(key, page);
|
||||||
|
if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in bot cache.`); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// If anonymous cache is enabled, save it to that cache if it doesn't exist or is expired
|
||||||
|
if (anonymousCacheEnabled() && !anonymousCache.has(key)) {
|
||||||
|
anonymousCache.set(key, page);
|
||||||
|
if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in anonymous cache.`); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a user is authenticated or not
|
||||||
|
*/
|
||||||
|
function isUserAuthenticated(req): boolean {
|
||||||
|
// Check whether our DSpace authentication Cookie exists or not
|
||||||
|
return req.cookies[TOKENITEM];
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@@ -27,7 +27,10 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
|||||||
SharedModule,
|
SharedModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
AccessControlRoutingModule,
|
AccessControlRoutingModule,
|
||||||
FormModule
|
FormModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
MembersListComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
EPeopleRegistryComponent,
|
EPeopleRegistryComponent,
|
||||||
@@ -35,7 +38,7 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
|||||||
GroupsRegistryComponent,
|
GroupsRegistryComponent,
|
||||||
GroupFormComponent,
|
GroupFormComponent,
|
||||||
SubgroupsListComponent,
|
SubgroupsListComponent,
|
||||||
MembersListComponent
|
MembersListComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
<button class="mr-auto btn btn-success addEPerson-button"
|
<button class="mr-auto btn btn-success addEPerson-button"
|
||||||
(click)="isEPersonFormShown = true">
|
(click)="isEPersonFormShown = true">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline">{{labelPrefix + 'button.add' | translate}}</span>
|
<span class="d-none d-sm-inline ml-1">{{labelPrefix + 'button.add' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<div class="flex-grow-1 mr-3 ml-3">
|
<div class="flex-grow-1 mr-3 ml-3">
|
||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" name="query" id="query" formControlName="query"
|
<input type="text" name="query" id="query" formControlName="query"
|
||||||
class="form-control" attr.aria-label="{{labelPrefix + 'search.placeholder' | translate}}"
|
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
|
||||||
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
|
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
<button type="submit" class="search-button btn btn-primary">
|
<button type="submit" class="search-button btn btn-primary">
|
||||||
@@ -72,13 +72,13 @@
|
|||||||
<td>{{epersonDto.eperson.email}}</td>
|
<td>{{epersonDto.eperson.email}}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button class="delete-button" (click)="toggleEditEPerson(epersonDto.eperson)"
|
<button (click)="toggleEditEPerson(epersonDto.eperson)"
|
||||||
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
||||||
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: {name: epersonDto.eperson.name} }}">
|
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: {name: epersonDto.eperson.name} }}">
|
||||||
<i class="fas fa-edit fa-fw"></i>
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
|
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
|
||||||
class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
||||||
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: {name: epersonDto.eperson.name} }}">
|
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: {name: epersonDto.eperson.name} }}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
@@ -42,6 +42,7 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
jasmine.getEnv().allowRespy(true);
|
||||||
mockEPeople = [EPersonMock, EPersonMock2];
|
mockEPeople = [EPersonMock, EPersonMock2];
|
||||||
ePersonDataServiceStub = {
|
ePersonDataServiceStub = {
|
||||||
activeEPerson: null,
|
activeEPerson: null,
|
||||||
@@ -260,17 +261,16 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
describe('delete EPerson button when the isAuthorized returns false', () => {
|
describe('delete EPerson button when the isAuthorized returns false', () => {
|
||||||
let ePeopleDeleteButton;
|
let ePeopleDeleteButton;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false));
|
||||||
isAuthorized: observableOf(false)
|
component.initialisePage();
|
||||||
});
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be disabled', () => {
|
it('should be disabled', () => {
|
||||||
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
|
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
|
||||||
ePeopleDeleteButton.forEach((deleteButton) => {
|
ePeopleDeleteButton.forEach((deleteButton: DebugElement) => {
|
||||||
expect(deleteButton.nativeElement.disabled).toBe(true);
|
expect(deleteButton.nativeElement.disabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -13,12 +13,13 @@
|
|||||||
[formGroup]="formGroup"
|
[formGroup]="formGroup"
|
||||||
[formLayout]="formLayout"
|
[formLayout]="formLayout"
|
||||||
[displayCancel]="false"
|
[displayCancel]="false"
|
||||||
|
[submitLabel]="submitLabel"
|
||||||
(submitForm)="onSubmit()">
|
(submitForm)="onSubmit()">
|
||||||
<div before class="btn-group">
|
<div before class="btn-group">
|
||||||
<button (click)="onCancel()"
|
<button (click)="onCancel()"
|
||||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div between class="btn-group">
|
<div *ngIf="displayResetPassword" between class="btn-group">
|
||||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" (click)="resetPassword()">
|
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" (click)="resetPassword()">
|
||||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -537,7 +537,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call epersonRegistrationService.registerEmail', () => {
|
it('should call epersonRegistrationService.registerEmail', () => {
|
||||||
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail);
|
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail, null, 'forgot');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -36,6 +36,7 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
|
|||||||
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||||
import { Registration } from '../../../core/shared/registration.model';
|
import { Registration } from '../../../core/shared/registration.model';
|
||||||
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
||||||
|
import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-eperson-form',
|
selector: 'ds-eperson-form',
|
||||||
@@ -164,6 +165,15 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
isImpersonated = false;
|
isImpersonated = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean that indicate if to display EPersonForm's Rest password button
|
||||||
|
*/
|
||||||
|
displayResetPassword = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string that indicate the label of Submit button
|
||||||
|
*/
|
||||||
|
submitLabel = 'form.create';
|
||||||
/**
|
/**
|
||||||
* Subscription to email field value change
|
* Subscription to email field value change
|
||||||
*/
|
*/
|
||||||
@@ -187,6 +197,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
this.epersonInitial = eperson;
|
this.epersonInitial = eperson;
|
||||||
if (hasValue(eperson)) {
|
if (hasValue(eperson)) {
|
||||||
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
||||||
|
this.displayResetPassword = true;
|
||||||
|
this.submitLabel = 'form.submit';
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -491,7 +503,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
resetPassword() {
|
resetPassword() {
|
||||||
if (hasValue(this.epersonInitial.email)) {
|
if (hasValue(this.epersonInitial.email)) {
|
||||||
this.epersonRegistrationService.registerEmail(this.epersonInitial.email).pipe(getFirstCompletedRemoteData())
|
this.epersonRegistrationService.registerEmail(this.epersonInitial.email, null, TYPE_REQUEST_FORGOT).pipe(getFirstCompletedRemoteData())
|
||||||
.subscribe((response: RemoteData<Registration>) => {
|
.subscribe((response: RemoteData<Registration>) => {
|
||||||
if (response.hasSucceeded) {
|
if (response.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'),
|
this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'),
|
||||||
|
@@ -9,7 +9,18 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #editheader>
|
<ng-template #editheader>
|
||||||
<h2 class="border-bottom pb-2">{{messagePrefix + '.head.edit' | translate}}</h2>
|
<h2 class="border-bottom pb-2">
|
||||||
|
<span
|
||||||
|
*dsContextHelp="{
|
||||||
|
content: 'admin.access-control.groups.form.tooltip.editGroupPage',
|
||||||
|
id: 'edit-group-page',
|
||||||
|
iconPlacement: 'right',
|
||||||
|
tooltipPlacement: ['right', 'bottom']
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{messagePrefix + '.head.edit' | translate}}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
|
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
|
||||||
|
@@ -266,6 +266,43 @@ describe('GroupFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should edit with name and description operations', () => {
|
||||||
|
const operations = [{
|
||||||
|
op: 'add',
|
||||||
|
path: '/metadata/dc.description',
|
||||||
|
value: 'testDescription'
|
||||||
|
}, {
|
||||||
|
op: 'replace',
|
||||||
|
path: '/name',
|
||||||
|
value: 'newGroupName'
|
||||||
|
}];
|
||||||
|
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should edit with description operations', () => {
|
||||||
|
component.groupName.value = null;
|
||||||
|
component.onSubmit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const operations = [{
|
||||||
|
op: 'add',
|
||||||
|
path: '/metadata/dc.description',
|
||||||
|
value: 'testDescription'
|
||||||
|
}];
|
||||||
|
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should edit with name operations', () => {
|
||||||
|
component.groupDescription.value = null;
|
||||||
|
component.onSubmit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const operations = [{
|
||||||
|
op: 'replace',
|
||||||
|
path: '/name',
|
||||||
|
value: 'newGroupName'
|
||||||
|
}];
|
||||||
|
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||||
|
});
|
||||||
|
|
||||||
it('should emit the existing group using the correct new values', waitForAsync(() => {
|
it('should emit the existing group using the correct new values', waitForAsync(() => {
|
||||||
fixture.whenStable().then(() => {
|
fixture.whenStable().then(() => {
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
||||||
|
@@ -346,8 +346,8 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (hasValue(this.groupDescription.value)) {
|
if (hasValue(this.groupDescription.value)) {
|
||||||
operations = [...operations, {
|
operations = [...operations, {
|
||||||
op: 'replace',
|
op: 'add',
|
||||||
path: '/metadata/dc.description/0/value',
|
path: '/metadata/dc.description',
|
||||||
value: this.groupDescription.value
|
value: this.groupDescription.value
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,19 @@
|
|||||||
<ng-container>
|
<ng-container>
|
||||||
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
||||||
|
|
||||||
<h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}
|
<h4 id="search" class="border-bottom pb-2">
|
||||||
|
<span
|
||||||
|
*dsContextHelp="{
|
||||||
|
content: 'admin.access-control.groups.form.tooltip.editGroup.addEpeople',
|
||||||
|
id: 'edit-group-add-epeople',
|
||||||
|
iconPlacement: 'right',
|
||||||
|
tooltipPlacement: ['top', 'right', 'bottom']
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{messagePrefix + '.search.head' | translate}}
|
||||||
|
</span>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
||||||
@@ -55,18 +65,20 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button *ngIf="(ePerson.memberOfGroup)"
|
<button *ngIf="ePerson.memberOfGroup"
|
||||||
(click)="deleteMemberFromGroup(ePerson)"
|
(click)="deleteMemberFromGroup(ePerson)"
|
||||||
class="btn btn-outline-danger btn-sm"
|
[disabled]="actionConfig.remove.disabled"
|
||||||
|
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}">
|
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button *ngIf="!(ePerson.memberOfGroup)"
|
<button *ngIf="!ePerson.memberOfGroup"
|
||||||
(click)="addMemberToGroup(ePerson)"
|
(click)="addMemberToGroup(ePerson)"
|
||||||
class="btn btn-outline-primary btn-sm"
|
[disabled]="actionConfig.add.disabled"
|
||||||
|
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.eperson.name} }}">
|
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.eperson.name} }}">
|
||||||
<i class="fas fa-plus fa-fw"></i>
|
<i [ngClass]="actionConfig.add.icon"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -113,10 +125,19 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button (click)="deleteMemberFromGroup(ePerson)"
|
<button *ngIf="ePerson.memberOfGroup"
|
||||||
class="btn btn-outline-danger btn-sm"
|
(click)="deleteMemberFromGroup(ePerson)"
|
||||||
|
[disabled]="actionConfig.remove.disabled"
|
||||||
|
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}">
|
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||||
|
</button>
|
||||||
|
<button *ngIf="!ePerson.memberOfGroup"
|
||||||
|
(click)="addMemberToGroup(ePerson)"
|
||||||
|
[disabled]="actionConfig.add.disabled"
|
||||||
|
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||||
|
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.eperson.name} }}">
|
||||||
|
<i [ngClass]="actionConfig.add.icon"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
@@ -37,10 +37,10 @@ describe('MembersListComponent', () => {
|
|||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let groupsDataServiceStub: any;
|
let groupsDataServiceStub: any;
|
||||||
let activeGroup;
|
let activeGroup;
|
||||||
let allEPersons;
|
let allEPersons: EPerson[];
|
||||||
let allGroups;
|
let allGroups: Group[];
|
||||||
let epersonMembers;
|
let epersonMembers: EPerson[];
|
||||||
let subgroupMembers;
|
let subgroupMembers: Group[];
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -53,7 +53,7 @@ describe('MembersListComponent', () => {
|
|||||||
activeGroup: activeGroup,
|
activeGroup: activeGroup,
|
||||||
epersonMembers: epersonMembers,
|
epersonMembers: epersonMembers,
|
||||||
subgroupMembers: subgroupMembers,
|
subgroupMembers: subgroupMembers,
|
||||||
findListByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
findListByHref(_href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
||||||
},
|
},
|
||||||
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
@@ -147,8 +147,10 @@ describe('MembersListComponent', () => {
|
|||||||
});
|
});
|
||||||
afterEach(fakeAsync(() => {
|
afterEach(fakeAsync(() => {
|
||||||
fixture.destroy();
|
fixture.destroy();
|
||||||
|
fixture.debugElement.nativeElement.remove();
|
||||||
flush();
|
flush();
|
||||||
component = null;
|
component = null;
|
||||||
|
fixture.debugElement.nativeElement.remove();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should create MembersListComponent', inject([MembersListComponent], (comp: MembersListComponent) => {
|
it('should create MembersListComponent', inject([MembersListComponent], (comp: MembersListComponent) => {
|
||||||
@@ -167,12 +169,19 @@ describe('MembersListComponent', () => {
|
|||||||
|
|
||||||
describe('search', () => {
|
describe('search', () => {
|
||||||
describe('when searching without query', () => {
|
describe('when searching without query', () => {
|
||||||
let epersonsFound;
|
let epersonsFound: DebugElement[];
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
|
spyOn(component, 'isMemberOfGroup').and.callFake((ePerson: EPerson) => {
|
||||||
|
return observableOf(activeGroup.epersons.includes(ePerson));
|
||||||
|
});
|
||||||
component.search({ scope: 'metadata', query: '' });
|
component.search({ scope: 'metadata', query: '' });
|
||||||
tick();
|
tick();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||||
|
// Stop using the fake spy function (because otherwise the clicking on the buttons will not change anything
|
||||||
|
// because they don't change the value of activeGroup.epersons)
|
||||||
|
jasmine.getEnv().allowRespy(true);
|
||||||
|
spyOn(component, 'isMemberOfGroup').and.callThrough();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should display all epersons', () => {
|
it('should display all epersons', () => {
|
||||||
@@ -181,62 +190,56 @@ describe('MembersListComponent', () => {
|
|||||||
|
|
||||||
describe('if eperson is already a eperson', () => {
|
describe('if eperson is already a eperson', () => {
|
||||||
it('should have delete button, else it should have add button', () => {
|
it('should have delete button, else it should have add button', () => {
|
||||||
activeGroup.epersons.map((eperson: EPerson) => {
|
const memberIds: string[] = activeGroup.epersons.map((ePerson: EPerson) => ePerson.id);
|
||||||
epersonsFound.map((foundEPersonRowElement) => {
|
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||||
if (foundEPersonRowElement.debugElement !== undefined) {
|
const epersonId: DebugElement = foundEPersonRowElement.query(By.css('td:first-child'));
|
||||||
const epersonId = foundEPersonRowElement.debugElement.query(By.css('td:first-child'));
|
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
if (memberIds.includes(epersonId.nativeElement.textContent)) {
|
||||||
if (epersonId.nativeElement.textContent === eperson.id) {
|
expect(addButton).toBeNull();
|
||||||
expect(addButton).toBeUndefined();
|
expect(deleteButton).not.toBeNull();
|
||||||
expect(deleteButton).toBeDefined();
|
|
||||||
} else {
|
} else {
|
||||||
expect(deleteButton).toBeUndefined();
|
expect(deleteButton).toBeNull();
|
||||||
expect(addButton).toBeDefined();
|
expect(addButton).not.toBeNull();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if first add button is pressed', () => {
|
describe('if first add button is pressed', () => {
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
|
const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
|
||||||
addButton.nativeElement.click();
|
addButton.nativeElement.click();
|
||||||
tick();
|
tick();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
it('all groups in search member of selected group', () => {
|
it('then all the ePersons are member of the active group', () => {
|
||||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||||
expect(epersonsFound.length).toEqual(2);
|
expect(epersonsFound.length).toEqual(2);
|
||||||
epersonsFound.map((foundEPersonRowElement) => {
|
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||||
if (foundEPersonRowElement.debugElement !== undefined) {
|
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
expect(addButton).toBeNull();
|
||||||
expect(addButton).toBeUndefined();
|
expect(deleteButton).not.toBeNull();
|
||||||
expect(deleteButton).toBeDefined();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if first delete button is pressed', () => {
|
describe('if first delete button is pressed', () => {
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt'));
|
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt'));
|
||||||
addButton.nativeElement.click();
|
deleteButton.nativeElement.click();
|
||||||
tick();
|
tick();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
it('first eperson in search delete button, because now member', () => {
|
it('then no ePerson is member of the active group', () => {
|
||||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||||
epersonsFound.map((foundEPersonRowElement) => {
|
expect(epersonsFound.length).toEqual(2);
|
||||||
if (foundEPersonRowElement.debugElement !== undefined) {
|
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||||
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
expect(deleteButton).toBeUndefined();
|
expect(deleteButton).toBeNull();
|
||||||
expect(addButton).toBeDefined();
|
expect(addButton).not.toBeNull();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -11,7 +11,7 @@ import {
|
|||||||
ObservedValueOf,
|
ObservedValueOf,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { defaultIfEmpty, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
import { defaultIfEmpty, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||||
import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||||
@@ -19,11 +19,13 @@ import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
|||||||
import { Group } from '../../../../core/eperson/models/group.model';
|
import { Group } from '../../../../core/eperson/models/group.model';
|
||||||
import {
|
import {
|
||||||
getFirstSucceededRemoteData,
|
getFirstSucceededRemoteData,
|
||||||
getFirstCompletedRemoteData, getAllCompletedRemoteData, getRemoteDataPayload
|
getFirstCompletedRemoteData,
|
||||||
|
getAllCompletedRemoteData,
|
||||||
|
getRemoteDataPayload
|
||||||
} from '../../../../core/shared/operators';
|
} from '../../../../core/shared/operators';
|
||||||
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 {EpersonDtoModel} from '../../../../core/eperson/models/eperson-dto.model';
|
import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model';
|
||||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +37,35 @@ enum SubKey {
|
|||||||
SearchResultsDTO,
|
SearchResultsDTO,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The layout config of the buttons in the last column
|
||||||
|
*/
|
||||||
|
export interface EPersonActionConfig {
|
||||||
|
/**
|
||||||
|
* The css classes that should be added to the button
|
||||||
|
*/
|
||||||
|
css?: string;
|
||||||
|
/**
|
||||||
|
* Whether the button should be disabled
|
||||||
|
*/
|
||||||
|
disabled: boolean;
|
||||||
|
/**
|
||||||
|
* The Font Awesome icon that should be used
|
||||||
|
*/
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link EPersonActionConfig} that should be used to display the button. The remove config will be used when the
|
||||||
|
* {@link EPerson} is already a member of the {@link Group} and the remove config will be used otherwise.
|
||||||
|
*
|
||||||
|
* *See {@link actionConfig} for an example*
|
||||||
|
*/
|
||||||
|
export interface EPersonListActionConfig {
|
||||||
|
add: EPersonActionConfig;
|
||||||
|
remove: EPersonActionConfig;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-members-list',
|
selector: 'ds-members-list',
|
||||||
templateUrl: './members-list.component.html'
|
templateUrl: './members-list.component.html'
|
||||||
@@ -47,6 +78,20 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
@Input()
|
@Input()
|
||||||
messagePrefix: string;
|
messagePrefix: string;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
actionConfig: EPersonListActionConfig = {
|
||||||
|
add: {
|
||||||
|
css: 'btn-outline-primary',
|
||||||
|
disabled: false,
|
||||||
|
icon: 'fas fa-plus fa-fw',
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
css: 'btn-outline-danger',
|
||||||
|
disabled: false,
|
||||||
|
icon: 'fas fa-trash-alt fa-fw'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EPeople being displayed in search result, initially all members, after search result of search
|
* EPeople being displayed in search result, initially all members, after search result of search
|
||||||
*/
|
*/
|
||||||
@@ -91,21 +136,20 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
// current active group being edited
|
// current active group being edited
|
||||||
groupBeingEdited: Group;
|
groupBeingEdited: Group;
|
||||||
|
|
||||||
paginationSub: Subscription;
|
constructor(
|
||||||
|
protected groupDataService: GroupDataService,
|
||||||
|
|
||||||
constructor(private groupDataService: GroupDataService,
|
|
||||||
public ePersonDataService: EPersonDataService,
|
public ePersonDataService: EPersonDataService,
|
||||||
private translateService: TranslateService,
|
protected translateService: TranslateService,
|
||||||
private notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
private formBuilder: FormBuilder,
|
protected formBuilder: FormBuilder,
|
||||||
private paginationService: PaginationService,
|
protected paginationService: PaginationService,
|
||||||
private router: Router) {
|
private router: Router
|
||||||
|
) {
|
||||||
this.currentSearchQuery = '';
|
this.currentSearchQuery = '';
|
||||||
this.currentSearchScope = 'metadata';
|
this.currentSearchScope = 'metadata';
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
this.searchForm = this.formBuilder.group(({
|
this.searchForm = this.formBuilder.group(({
|
||||||
scope: 'metadata',
|
scope: 'metadata',
|
||||||
query: '',
|
query: '',
|
||||||
@@ -124,7 +168,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
* @param page the number of the page to retrieve
|
* @param page the number of the page to retrieve
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private retrieveMembers(page: number) {
|
retrieveMembers(page: number): void {
|
||||||
this.unsubFrom(SubKey.MembersDTO);
|
this.unsubFrom(SubKey.MembersDTO);
|
||||||
this.subs.set(SubKey.MembersDTO,
|
this.subs.set(SubKey.MembersDTO,
|
||||||
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||||
@@ -138,7 +182,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
getAllCompletedRemoteData(),
|
getAllCompletedRemoteData(),
|
||||||
map((rd: RemoteData<any>) => {
|
map((rd: RemoteData<any>) => {
|
||||||
if (rd.hasFailed) {
|
if (rd.hasFailed) {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', {cause: rd.errorMessage}));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage }));
|
||||||
} else {
|
} else {
|
||||||
return rd;
|
return rd;
|
||||||
}
|
}
|
||||||
@@ -164,7 +208,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the given ePerson is a member of the group currently being edited
|
* Whether the given ePerson is a member of the group currently being edited
|
||||||
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
|
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
|
||||||
*/
|
*/
|
||||||
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
|
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
|
||||||
@@ -193,7 +237,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
* @param key The key of the subscription to unsubscribe from
|
* @param key The key of the subscription to unsubscribe from
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private unsubFrom(key: SubKey) {
|
protected unsubFrom(key: SubKey) {
|
||||||
if (this.subs.has(key)) {
|
if (this.subs.has(key)) {
|
||||||
this.subs.get(key).unsubscribe();
|
this.subs.get(key).unsubscribe();
|
||||||
this.subs.delete(key);
|
this.subs.delete(key);
|
||||||
@@ -205,6 +249,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
* @param ePerson EPerson we want to delete as member from group that is currently being edited
|
* @param ePerson EPerson we want to delete as member from group that is currently being edited
|
||||||
*/
|
*/
|
||||||
deleteMemberFromGroup(ePerson: EpersonDtoModel) {
|
deleteMemberFromGroup(ePerson: EpersonDtoModel) {
|
||||||
|
ePerson.memberOfGroup = false;
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
||||||
@@ -267,7 +312,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
getAllCompletedRemoteData(),
|
getAllCompletedRemoteData(),
|
||||||
map((rd: RemoteData<any>) => {
|
map((rd: RemoteData<any>) => {
|
||||||
if (rd.hasFailed) {
|
if (rd.hasFailed) {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', {cause: rd.errorMessage}));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage }));
|
||||||
} else {
|
} else {
|
||||||
return rd;
|
return rd;
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,16 @@
|
|||||||
<ng-container>
|
<ng-container>
|
||||||
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
||||||
|
|
||||||
<h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}
|
<h4 id="search" class="border-bottom pb-2">
|
||||||
|
<span *dsContextHelp="{
|
||||||
|
content: 'admin.access-control.groups.form.tooltip.editGroup.addSubgroups',
|
||||||
|
id: 'edit-group-add-subgroups',
|
||||||
|
iconPlacement: 'right',
|
||||||
|
tooltipPlacement: ['top', 'right', 'bottom']
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{messagePrefix + '.search.head' | translate}}
|
||||||
|
</span>
|
||||||
|
|
||||||
</h4>
|
</h4>
|
||||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||||
@@ -56,7 +65,7 @@
|
|||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p *ngIf="(isActiveGroup(group) | async)">{{ messagePrefix + '.table.edit.currentGroup' | translate }}</p>
|
<span *ngIf="(isActiveGroup(group) | async)">{{ messagePrefix + '.table.edit.currentGroup' | translate }}</span>
|
||||||
|
|
||||||
<button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
<button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
||||||
(click)="addSubgroupToGroup(group)"
|
(click)="addSubgroupToGroup(group)"
|
||||||
|
@@ -1,14 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
|
||||||
import {
|
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
ComponentFixture,
|
|
||||||
fakeAsync,
|
|
||||||
flush,
|
|
||||||
inject,
|
|
||||||
TestBed,
|
|
||||||
tick,
|
|
||||||
waitForAsync
|
|
||||||
} from '@angular/core/testing';
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -46,8 +38,8 @@ describe('SubgroupsListComponent', () => {
|
|||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let groupsDataServiceStub: any;
|
let groupsDataServiceStub: any;
|
||||||
let activeGroup;
|
let activeGroup;
|
||||||
let subgroups;
|
let subgroups: Group[];
|
||||||
let allGroups;
|
let allGroups: Group[];
|
||||||
let routerStub;
|
let routerStub;
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
@@ -65,7 +57,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
getSubgroups(): Group {
|
getSubgroups(): Group {
|
||||||
return this.activeGroup;
|
return this.activeGroup;
|
||||||
},
|
},
|
||||||
findListByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
findListByHref(_href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
return this.subgroups$.pipe(
|
return this.subgroups$.pipe(
|
||||||
map((currentGroups: Group[]) => {
|
map((currentGroups: Group[]) => {
|
||||||
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));
|
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));
|
||||||
@@ -133,6 +125,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
});
|
});
|
||||||
afterEach(fakeAsync(() => {
|
afterEach(fakeAsync(() => {
|
||||||
fixture.destroy();
|
fixture.destroy();
|
||||||
|
fixture.debugElement.nativeElement.remove();
|
||||||
flush();
|
flush();
|
||||||
component = null;
|
component = null;
|
||||||
}));
|
}));
|
||||||
@@ -152,7 +145,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('if first group delete button is pressed', () => {
|
describe('if first group delete button is pressed', () => {
|
||||||
let groupsFound;
|
let groupsFound: DebugElement[];
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
|
const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
|
||||||
addButton.triggerEventHandler('click', {
|
addButton.triggerEventHandler('click', {
|
||||||
@@ -170,7 +163,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
|
|
||||||
describe('search', () => {
|
describe('search', () => {
|
||||||
describe('when searching with empty query', () => {
|
describe('when searching with empty query', () => {
|
||||||
let groupsFound;
|
let groupsFound: DebugElement[];
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
component.search({ query: '' });
|
component.search({ query: '' });
|
||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
@@ -181,9 +174,9 @@ describe('SubgroupsListComponent', () => {
|
|||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
expect(groupsFound.length).toEqual(2);
|
expect(groupsFound.length).toEqual(2);
|
||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
const groupIdsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
|
const groupIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
|
||||||
allGroups.map((group: Group) => {
|
allGroups.map((group: Group) => {
|
||||||
expect(groupIdsFound.find((foundEl) => {
|
expect(groupIdsFound.find((foundEl: DebugElement) => {
|
||||||
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
||||||
})).toBeTruthy();
|
})).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -195,30 +188,30 @@ describe('SubgroupsListComponent', () => {
|
|||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups;
|
const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups;
|
||||||
if (getSubgroups !== undefined && getSubgroups.length > 0) {
|
if (getSubgroups !== undefined && getSubgroups.length > 0) {
|
||||||
groupsFound.map((foundGroupRowElement) => {
|
groupsFound.map((foundGroupRowElement: DebugElement) => {
|
||||||
if (foundGroupRowElement.debugElement !== undefined) {
|
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
|
||||||
const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
expect(addButton).toBeUndefined();
|
expect(addButton).toBeNull();
|
||||||
expect(deleteButton).toBeDefined();
|
if (activeGroup.id === groupId.nativeElement.textContent) {
|
||||||
|
expect(deleteButton).toBeNull();
|
||||||
|
} else {
|
||||||
|
expect(deleteButton).not.toBeNull();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
getSubgroups.map((group: Group) => {
|
const subgroupIds: string[] = activeGroup.subgroups.map((group: Group) => group.id);
|
||||||
groupsFound.map((foundGroupRowElement) => {
|
groupsFound.map((foundGroupRowElement: DebugElement) => {
|
||||||
if (foundGroupRowElement.debugElement !== undefined) {
|
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
|
||||||
const groupId = foundGroupRowElement.debugElement.query(By.css('td:first-child'));
|
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
if (subgroupIds.includes(groupId.nativeElement.textContent)) {
|
||||||
if (groupId.nativeElement.textContent === group.id) {
|
expect(addButton).toBeNull();
|
||||||
expect(addButton).toBeUndefined();
|
expect(deleteButton).not.toBeNull();
|
||||||
expect(deleteButton).toBeDefined();
|
|
||||||
} else {
|
} else {
|
||||||
expect(deleteButton).toBeUndefined();
|
expect(deleteButton).toBeNull();
|
||||||
expect(addButton).toBeDefined();
|
expect(addButton).not.toBeNull();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<button class="mr-auto btn btn-success"
|
<button class="mr-auto btn btn-success"
|
||||||
[routerLink]="['newGroup']">
|
[routerLink]="['newGroup']">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline">{{messagePrefix + 'button.add' | translate}}</span>
|
<span class="d-none d-sm-inline ml-1">{{messagePrefix + 'button.add' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="flex-grow-1 mr-3">
|
<div class="flex-grow-1 mr-3">
|
||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" name="query" id="query" formControlName="query"
|
<input type="text" name="query" id="query" formControlName="query"
|
||||||
class="form-control" attr.aria-label="{{messagePrefix + 'search.placeholder' | translate}}"
|
class="form-control" [attr.aria-label]="messagePrefix + 'search.placeholder' | translate"
|
||||||
[placeholder]="(messagePrefix + 'search.placeholder' | translate)" >
|
[placeholder]="(messagePrefix + 'search.placeholder' | translate)" >
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
<button type="submit" class="search-button btn btn-primary">
|
<button type="submit" class="search-button btn btn-primary">
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
||||||
<td>
|
<td>
|
||||||
<label>
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[checked]="isSelected(bitstreamFormat) | async"
|
[checked]="isSelected(bitstreamFormat) | async"
|
||||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(schema) | async}">
|
[ngClass]="{'table-primary' : isActive(schema) | async}">
|
||||||
<td>
|
<td>
|
||||||
<label>
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[checked]="isSelected(schema) | async"
|
[checked]="isSelected(schema) | async"
|
||||||
(change)="selectMetadataSchema(schema, $event)"
|
(change)="selectMetadataSchema(schema, $event)"
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
.selectable-row:hover {
|
.selectable-row:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep #metadatadataschemagroup {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
@@ -34,14 +34,14 @@
|
|||||||
<tr *ngFor="let field of fields?.page"
|
<tr *ngFor="let field of fields?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
[ngClass]="{'table-primary' : isActive(field) | async}">
|
||||||
<td>
|
<td>
|
||||||
<label>
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[checked]="isSelected(field) | async"
|
[checked]="isSelected(field) | async"
|
||||||
(change)="selectMetadataField(field, $event)">
|
(change)="selectMetadataField(field, $event)">
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier" class="mb-0">.</label>{{field.qualifier}}</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@@ -1,3 +1,8 @@
|
|||||||
.selectable-row:hover {
|
.selectable-row:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep #metadatadatafieldgroup {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
@@ -52,6 +52,12 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import
|
|||||||
component: BatchImportPageComponent,
|
component: BatchImportPageComponent,
|
||||||
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }
|
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'system-wide-alert',
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule),
|
||||||
|
data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'}
|
||||||
|
},
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<a class="nav-item nav-link d-flex flex-row flex-nowrap"
|
<a class="nav-item nav-link d-flex flex-row flex-nowrap"
|
||||||
[ngClass]="{ disabled: !hasLink }"
|
[ngClass]="{ disabled: isDisabled }"
|
||||||
[attr.aria-disabled]="!hasLink"
|
[attr.aria-disabled]="isDisabled"
|
||||||
[attr.aria-labelledby]="'sidebarName-' + section.id"
|
[attr.aria-labelledby]="'sidebarName-' + section.id"
|
||||||
[title]="('menu.section.icon.' + section.id) | translate"
|
[title]="('menu.section.icon.' + section.id) | translate"
|
||||||
[routerLink]="itemModel.link"
|
[routerLink]="itemModel.link"
|
||||||
|
@@ -17,14 +17,16 @@ describe('AdminSidebarSectionComponent', () => {
|
|||||||
const menuService = new MenuServiceStub();
|
const menuService = new MenuServiceStub();
|
||||||
const iconString = 'test';
|
const iconString = 'test';
|
||||||
|
|
||||||
|
describe('when not disabled', () => {
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
|
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
|
||||||
declarations: [AdminSidebarSectionComponent, TestComponent],
|
declarations: [AdminSidebarSectionComponent, TestComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: 'sectionDataProvider', useValue: { model: { link: 'google.com' }, icon: iconString } },
|
{provide: 'sectionDataProvider', useValue: {model: {link: 'google.com'}, icon: iconString}},
|
||||||
{ provide: MenuService, useValue: menuService },
|
{provide: MenuService, useValue: menuService},
|
||||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
{provide: CSSVariableService, useClass: CSSVariableServiceStub},
|
||||||
]
|
]
|
||||||
}).overrideComponent(AdminSidebarSectionComponent, {
|
}).overrideComponent(AdminSidebarSectionComponent, {
|
||||||
set: {
|
set: {
|
||||||
@@ -49,6 +51,52 @@ describe('AdminSidebarSectionComponent', () => {
|
|||||||
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
|
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
|
||||||
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
||||||
});
|
});
|
||||||
|
it('should not contain the disabled class', () => {
|
||||||
|
const disabled = fixture.debugElement.query(By.css('.disabled'));
|
||||||
|
expect(disabled).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
describe('when disabled', () => {
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
|
||||||
|
declarations: [AdminSidebarSectionComponent, TestComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: 'sectionDataProvider', useValue: {model: {link: 'google.com', disabled: true}, icon: iconString}},
|
||||||
|
{provide: MenuService, useValue: menuService},
|
||||||
|
{provide: CSSVariableService, useClass: CSSVariableServiceStub},
|
||||||
|
]
|
||||||
|
}).overrideComponent(AdminSidebarSectionComponent, {
|
||||||
|
set: {
|
||||||
|
entryComponents: [TestComponent]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AdminSidebarSectionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the right icon', () => {
|
||||||
|
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
|
||||||
|
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
||||||
|
});
|
||||||
|
it('should contain the disabled class', () => {
|
||||||
|
const disabled = fixture.debugElement.query(By.css('.disabled'));
|
||||||
|
expect(disabled).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// declare a test component
|
// declare a test component
|
||||||
|
@@ -5,7 +5,7 @@ import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorat
|
|||||||
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
|
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
|
||||||
import { MenuSection } from '../../../shared/menu/menu-section.model';
|
import { MenuSection } from '../../../shared/menu/menu-section.model';
|
||||||
import { MenuID } from '../../../shared/menu/menu-id.model';
|
import { MenuID } from '../../../shared/menu/menu-id.model';
|
||||||
import { isNotEmpty } from '../../../shared/empty.util';
|
import { isEmpty } from '../../../shared/empty.util';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +26,12 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
|
|||||||
*/
|
*/
|
||||||
menuID: MenuID = MenuID.ADMIN;
|
menuID: MenuID = MenuID.ADMIN;
|
||||||
itemModel;
|
itemModel;
|
||||||
hasLink: boolean;
|
|
||||||
|
/**
|
||||||
|
* Boolean to indicate whether this section is disabled
|
||||||
|
*/
|
||||||
|
isDisabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('sectionDataProvider') menuSection: MenuSection,
|
@Inject('sectionDataProvider') menuSection: MenuSection,
|
||||||
protected menuService: MenuService,
|
protected menuService: MenuService,
|
||||||
@@ -38,13 +43,13 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.hasLink = isNotEmpty(this.itemModel?.link);
|
this.isDisabled = this.itemModel?.disabled || isEmpty(this.itemModel?.link);
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(event: any): void {
|
navigate(event: any): void {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this.hasLink) {
|
if (!this.isDisabled) {
|
||||||
this.router.navigate(this.itemModel.link);
|
this.router.navigate(this.itemModel.link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -100,6 +100,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.menuVisible = this.menuService.isMenuVisibleWithVisibleSections(this.menuID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('focusin')
|
@HostListener('focusin')
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
[attr.aria-labelledby]="'sidebarName-' + section.id"
|
[attr.aria-labelledby]="'sidebarName-' + section.id"
|
||||||
[attr.aria-expanded]="expanded | async"
|
[attr.aria-expanded]="expanded | async"
|
||||||
[title]="('menu.section.icon.' + section.id) | translate"
|
[title]="('menu.section.icon.' + section.id) | translate"
|
||||||
|
[class.disabled]="section.model?.disabled"
|
||||||
(click)="toggleSection($event)"
|
(click)="toggleSection($event)"
|
||||||
(keyup.space)="toggleSection($event)"
|
(keyup.space)="toggleSection($event)"
|
||||||
(keyup.enter)="toggleSection($event)"
|
(keyup.enter)="toggleSection($event)"
|
||||||
|
@@ -23,7 +23,7 @@ describe('ExpandableAdminSidebarSectionComponent', () => {
|
|||||||
imports: [NoopAnimationsModule, TranslateModule.forRoot()],
|
imports: [NoopAnimationsModule, TranslateModule.forRoot()],
|
||||||
declarations: [ExpandableAdminSidebarSectionComponent, TestComponent],
|
declarations: [ExpandableAdminSidebarSectionComponent, TestComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: 'sectionDataProvider', useValue: { icon: iconString } },
|
{ provide: 'sectionDataProvider', useValue: { icon: iconString, model: {} } },
|
||||||
{ provide: MenuService, useValue: menuService },
|
{ provide: MenuService, useValue: menuService },
|
||||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||||
{ provide: Router, useValue: new RouterStub() },
|
{ provide: Router, useValue: new RouterStub() },
|
||||||
|
@@ -1 +1 @@
|
|||||||
<ds-configuration-search-page configuration="workflowAdmin" [context]="context"></ds-configuration-search-page>
|
<ds-configuration-search-page configuration="supervision" [context]="context"></ds-configuration-search-page>
|
||||||
|
@@ -4,24 +4,32 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../../../../core/url-combiner/url-combiner';
|
||||||
import { WorkflowItemAdminWorkflowActionsComponent } from './workflow-item-admin-workflow-actions.component';
|
import { WorkflowItemAdminWorkflowActionsComponent } from './workflow-item-admin-workflow-actions.component';
|
||||||
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
import {
|
import {
|
||||||
getWorkflowItemDeleteRoute,
|
getWorkflowItemDeleteRoute,
|
||||||
getWorkflowItemSendBackRoute
|
getWorkflowItemSendBackRoute
|
||||||
} from '../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
|
import { RequestEntryState } from '../../../../../core/data/request-entry-state.model';
|
||||||
|
|
||||||
describe('WorkflowItemAdminWorkflowActionsComponent', () => {
|
describe('WorkflowItemAdminWorkflowActionsComponent', () => {
|
||||||
let component: WorkflowItemAdminWorkflowActionsComponent;
|
let component: WorkflowItemAdminWorkflowActionsComponent;
|
||||||
let fixture: ComponentFixture<WorkflowItemAdminWorkflowActionsComponent>;
|
let fixture: ComponentFixture<WorkflowItemAdminWorkflowActionsComponent>;
|
||||||
let id;
|
let id;
|
||||||
let wfi;
|
let wfi;
|
||||||
|
let item = new Item();
|
||||||
|
item.uuid = 'itemUUID1111';
|
||||||
|
const rd = new RemoteData(undefined, undefined, undefined, RequestEntryState.Success, undefined, item, 200);
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
wfi = new WorkflowItem();
|
wfi = new WorkflowItem();
|
||||||
wfi.id = id;
|
wfi.id = id;
|
||||||
|
wfi.item = of(rd);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -59,4 +67,5 @@ describe('WorkflowItemAdminWorkflowActionsComponent', () => {
|
|||||||
const link = a.nativeElement.href;
|
const link = a.nativeElement.href;
|
||||||
expect(link).toContain(new URLCombiner(getWorkflowItemSendBackRoute(wfi.id)).toString());
|
expect(link).toContain(new URLCombiner(getWorkflowItemSendBackRoute(wfi.id)).toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
@@ -1,9 +1,10 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
|
|
||||||
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
import {
|
import {
|
||||||
getWorkflowItemSendBackRoute,
|
getWorkflowItemDeleteRoute,
|
||||||
getWorkflowItemDeleteRoute
|
getWorkflowItemSendBackRoute
|
||||||
} from '../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-workflow-item-admin-workflow-actions-element',
|
selector: 'ds-workflow-item-admin-workflow-actions-element',
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
templateUrl: './workflow-item-admin-workflow-actions.component.html'
|
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
|
* The component for displaying the actions for a list element for a workflow-item on the admin workflow search page
|
||||||
*/
|
*/
|
||||||
export class WorkflowItemAdminWorkflowActionsComponent {
|
export class WorkflowItemAdminWorkflowActionsComponent {
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ export class WorkflowItemAdminWorkflowActionsComponent {
|
|||||||
@Input() public wfi: WorkflowItem;
|
@Input() public wfi: WorkflowItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not to use small buttons
|
* Whether to use small buttons or not
|
||||||
*/
|
*/
|
||||||
@Input() public small: boolean;
|
@Input() public small: boolean;
|
||||||
|
|
||||||
@@ -29,7 +30,6 @@ export class WorkflowItemAdminWorkflowActionsComponent {
|
|||||||
* Returns the path to the delete page of this workflow item
|
* Returns the path to the delete page of this workflow item
|
||||||
*/
|
*/
|
||||||
getDeleteRoute(): string {
|
getDeleteRoute(): string {
|
||||||
|
|
||||||
return getWorkflowItemDeleteRoute(this.wfi.id);
|
return getWorkflowItemDeleteRoute(this.wfi.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,4 +39,5 @@ export class WorkflowItemAdminWorkflowActionsComponent {
|
|||||||
getSendBackRoute(): string {
|
getSendBackRoute(): string {
|
||||||
return getWorkflowItemSendBackRoute(this.wfi.id);
|
return getWorkflowItemSendBackRoute(this.wfi.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -0,0 +1,44 @@
|
|||||||
|
<div>
|
||||||
|
<div class="modal-header">{{'supervision-group-selector.header' | translate}}
|
||||||
|
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="control-group col-sm-12">
|
||||||
|
<label for="supervisionOrder">{{'supervision-group-selector.select.type-of-order.label' | translate}}</label>
|
||||||
|
<select name="supervisionOrder" id="supervisionOrder" class="form-control"
|
||||||
|
[(ngModel)]="selectedOrderType"
|
||||||
|
attr.aria-label="{{'supervision-group-selector.select.type-of-order.label' | translate}}">
|
||||||
|
<option value="EDITOR">{{'supervision-group-selector.select.type-of-order.option.editor' | translate}}</option>
|
||||||
|
<option value="OBSERVER">{{'supervision-group-selector.select.type-of-order.option.observer' | translate}}</option>
|
||||||
|
</select>
|
||||||
|
<ds-error *ngIf="isSubmitted && (!selectedOrderType || selectedOrderType === '')"
|
||||||
|
message="{{'supervision-group-selector.select.type-of-order.error' | translate}}"></ds-error>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="control-group col-sm-12">
|
||||||
|
<label for="supervisionGroup">{{'supervision-group-selector.select.group.label' | translate}}</label>
|
||||||
|
<ng-container class="mb-3">
|
||||||
|
<input id="supervisionGroup" class="form-control" type="text" [value]="selectedGroup ? dsoNameService.getName(selectedGroup) : ''" disabled>
|
||||||
|
<ds-error *ngIf="isSubmitted && !selectedGroup" message="{{'supervision-group-selector.select.group.error' | translate}}"></ds-error>
|
||||||
|
</ng-container>
|
||||||
|
<ds-eperson-group-list [isListOfEPerson]="false"
|
||||||
|
(select)="updateGroupObjectSelected($event)"></ds-eperson-group-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="d-flex flex-row-reverse m-2"> -->
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-outline-secondary"
|
||||||
|
(click)="close()">
|
||||||
|
<i class="fas fa-times"></i> {{"supervision-group-selector.button.cancel" | translate}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary save"
|
||||||
|
(click)="save()">
|
||||||
|
<i class="fas fa-save"></i> {{"supervision-group-selector.button.save" | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,70 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { SupervisionOrderGroupSelectorComponent } from './supervision-order-group-selector.component';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import { NotificationsService } from '../../../../../../shared/notifications/notifications.service';
|
||||||
|
import { Group } from '../../../../../../core/eperson/models/group.model';
|
||||||
|
import { SupervisionOrder } from '../../../../../../core/supervision-order/models/supervision-order.model';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
describe('SupervisionOrderGroupSelectorComponent', () => {
|
||||||
|
let component: SupervisionOrderGroupSelectorComponent;
|
||||||
|
let fixture: ComponentFixture<SupervisionOrderGroupSelectorComponent>;
|
||||||
|
let debugElement: DebugElement;
|
||||||
|
|
||||||
|
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
|
||||||
|
|
||||||
|
const supervisionOrderDataService: any = jasmine.createSpyObj('supervisionOrderDataService', {
|
||||||
|
create: of(new SupervisionOrder())
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedOrderType = 'NONE';
|
||||||
|
const itemUUID = 'itemUUID1234';
|
||||||
|
|
||||||
|
const selectedGroup = new Group();
|
||||||
|
selectedGroup.uuid = 'GroupUUID1234';
|
||||||
|
|
||||||
|
const supervisionDataObject = new SupervisionOrder();
|
||||||
|
supervisionDataObject.ordertype = selectedOrderType;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [SupervisionOrderGroupSelectorComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: NgbActiveModal, useValue: modalStub },
|
||||||
|
{ provide: SupervisionOrderDataService, useValue: supervisionOrderDataService },
|
||||||
|
{ provide: NotificationsService, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
fixture = TestBed.createComponent(SupervisionOrderGroupSelectorComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component.itemUUID = itemUUID;
|
||||||
|
component.selectedGroup = selectedGroup;
|
||||||
|
component.selectedOrderType = selectedOrderType;
|
||||||
|
debugElement = fixture.debugElement;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create component', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call create for supervision order', () => {
|
||||||
|
component.save();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(supervisionOrderDataService.create).toHaveBeenCalledWith(supervisionDataObject, itemUUID, selectedGroup.uuid, selectedOrderType);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,97 @@
|
|||||||
|
import { Component, EventEmitter, Output } from '@angular/core';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { getFirstCompletedRemoteData } from 'src/app/core/shared/operators';
|
||||||
|
import { NotificationsService } from 'src/app/shared/notifications/notifications.service';
|
||||||
|
import { DSONameService } from '../../../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { Group } from '../../../../../../core/eperson/models/group.model';
|
||||||
|
import { SupervisionOrder } from '../../../../../../core/supervision-order/models/supervision-order.model';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import { RemoteData } from '../../../../../../core/data/remote-data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to wrap a dropdown - for type of order -
|
||||||
|
* and a list of groups
|
||||||
|
* inside a modal
|
||||||
|
* Used to create a new supervision order
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-supervision-group-selector',
|
||||||
|
styleUrls: ['./supervision-order-group-selector.component.scss'],
|
||||||
|
templateUrl: './supervision-order-group-selector.component.html',
|
||||||
|
})
|
||||||
|
export class SupervisionOrderGroupSelectorComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to perform the actions on
|
||||||
|
*/
|
||||||
|
itemUUID: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected supervision order type
|
||||||
|
*/
|
||||||
|
selectedOrderType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* selected group for supervision
|
||||||
|
*/
|
||||||
|
selectedGroup: Group;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* boolean flag for the validations
|
||||||
|
*/
|
||||||
|
isSubmitted = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when a new SupervisionOrder has been created
|
||||||
|
*/
|
||||||
|
@Output() create: EventEmitter<SupervisionOrder> = new EventEmitter<SupervisionOrder>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dsoNameService: DSONameService,
|
||||||
|
private activeModal: NgbActiveModal,
|
||||||
|
private supervisionOrderDataService: SupervisionOrderDataService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
this.activeModal.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign the value of group on select
|
||||||
|
*/
|
||||||
|
updateGroupObjectSelected(object) {
|
||||||
|
this.selectedGroup = object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the supervision order
|
||||||
|
*/
|
||||||
|
save() {
|
||||||
|
this.isSubmitted = true;
|
||||||
|
if (this.selectedOrderType && this.selectedGroup) {
|
||||||
|
let supervisionDataObject = new SupervisionOrder();
|
||||||
|
supervisionDataObject.ordertype = this.selectedOrderType;
|
||||||
|
this.supervisionOrderDataService.create(supervisionDataObject, this.itemUUID, this.selectedGroup.uuid, this.selectedOrderType).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
).subscribe((rd: RemoteData<SupervisionOrder>) => {
|
||||||
|
if (rd.state === 'Success') {
|
||||||
|
this.notificationsService.success(this.translateService.get('supervision-group-selector.notification.create.success.title', { name: this.selectedGroup.name }));
|
||||||
|
this.create.emit(rd.payload);
|
||||||
|
this.close();
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(
|
||||||
|
this.translateService.get('supervision-group-selector.notification.create.failure.title'),
|
||||||
|
rd.statusCode === 422 ? this.translateService.get('supervision-group-selector.notification.create.already-existing') : rd.errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
<ng-container *ngVar="(supervisionOrderEntries$ | async) as supervisionOrders">
|
||||||
|
<div class="item-list-supervision" *ngIf="supervisionOrders?.length > 0">
|
||||||
|
<div>
|
||||||
|
<span>{{'workflow-item.search.result.list.element.supervised-by' | translate}} </span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a class="badge badge-primary mr-1 mb-1 text-capitalize mw-100 text-truncate" *ngFor="let supervisionOrder of supervisionOrders" data-test="soBadge"
|
||||||
|
[ngbTooltip]="'workflow-item.search.result.list.element.supervised.remove-tooltip' | translate"
|
||||||
|
(click)="$event.preventDefault(); $event.stopImmediatePropagation(); deleteSupervisionOrder(supervisionOrder)" aria-label="Close">
|
||||||
|
{{supervisionOrder.group.name}}
|
||||||
|
<span aria-hidden="true"> ×</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
@@ -0,0 +1,3 @@
|
|||||||
|
.badge {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@@ -0,0 +1,61 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { SupervisionOrderStatusComponent } from './supervision-order-status.component';
|
||||||
|
import { VarDirective } from '../../../../../../shared/utils/var.directive';
|
||||||
|
import { TranslateLoaderMock } from '../../../../../../shared/mocks/translate-loader.mock';
|
||||||
|
import { supervisionOrderListMock } from '../../../../../../shared/testing/supervision-order.mock';
|
||||||
|
|
||||||
|
describe('SupervisionOrderStatusComponent', () => {
|
||||||
|
let component: SupervisionOrderStatusComponent;
|
||||||
|
let fixture: ComponentFixture<SupervisionOrderStatusComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NgbTooltipModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
declarations: [ SupervisionOrderStatusComponent, VarDirective ],
|
||||||
|
schemas: [
|
||||||
|
NO_ERRORS_SCHEMA
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SupervisionOrderStatusComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.supervisionOrderList = supervisionOrderListMock;
|
||||||
|
component.ngOnChanges( {
|
||||||
|
supervisionOrderList: new SimpleChange(null, supervisionOrderListMock, true)
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render badges properly', () => {
|
||||||
|
const badges = fixture.debugElement.queryAll(By.css('[data-test="soBadge"]'));
|
||||||
|
expect(badges.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit delete event on click', () => {
|
||||||
|
spyOn(component.delete, 'emit');
|
||||||
|
const badges = fixture.debugElement.queryAll(By.css('[data-test="soBadge"]'));
|
||||||
|
badges[0].nativeElement.click();
|
||||||
|
expect(component.delete.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,82 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, from, Observable } from 'rxjs';
|
||||||
|
import { map, mergeMap, reduce } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { SupervisionOrder } from '../../../../../../core/supervision-order/models/supervision-order.model';
|
||||||
|
import { Group } from '../../../../../../core/eperson/models/group.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../../../../../core/shared/operators';
|
||||||
|
import { isNotEmpty } from '../../../../../../shared/empty.util';
|
||||||
|
import { RemoteData } from '../../../../../../core/data/remote-data';
|
||||||
|
|
||||||
|
export interface SupervisionOrderListEntry {
|
||||||
|
supervisionOrder: SupervisionOrder;
|
||||||
|
group: Group
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-supervision-order-status',
|
||||||
|
templateUrl: './supervision-order-status.component.html',
|
||||||
|
styleUrls: ['./supervision-order-status.component.scss']
|
||||||
|
})
|
||||||
|
export class SupervisionOrderStatusComponent implements OnChanges {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of supervision order object to show
|
||||||
|
*/
|
||||||
|
@Input() supervisionOrderList: SupervisionOrder[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of the supervision orders combined with the group
|
||||||
|
*/
|
||||||
|
supervisionOrderEntries$: BehaviorSubject<SupervisionOrderListEntry[]> = new BehaviorSubject([]);
|
||||||
|
|
||||||
|
@Output() delete: EventEmitter<SupervisionOrderListEntry> = new EventEmitter<SupervisionOrderListEntry>();
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes && changes.supervisionOrderList) {
|
||||||
|
this.getSupervisionOrderEntries(changes.supervisionOrderList.currentValue)
|
||||||
|
.subscribe((supervisionOrderEntries: SupervisionOrderListEntry[]) => {
|
||||||
|
this.supervisionOrderEntries$.next(supervisionOrderEntries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a list of SupervisionOrderListEntry by the given SupervisionOrder list
|
||||||
|
*
|
||||||
|
* @param supervisionOrderList
|
||||||
|
*/
|
||||||
|
private getSupervisionOrderEntries(supervisionOrderList: SupervisionOrder[]): Observable<SupervisionOrderListEntry[]> {
|
||||||
|
return from(supervisionOrderList).pipe(
|
||||||
|
mergeMap((so: SupervisionOrder) => so.group.pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((sogRD: RemoteData<Group>) => {
|
||||||
|
if (sogRD.hasSucceeded) {
|
||||||
|
const entry: SupervisionOrderListEntry = {
|
||||||
|
supervisionOrder: so,
|
||||||
|
group: sogRD.payload
|
||||||
|
};
|
||||||
|
return entry;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)),
|
||||||
|
reduce((acc: SupervisionOrderListEntry[], value: any) => {
|
||||||
|
if (isNotEmpty(value)) {
|
||||||
|
return [...acc, value];
|
||||||
|
} else {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a delete event with the given SupervisionOrderListEntry.
|
||||||
|
*/
|
||||||
|
deleteSupervisionOrder(supervisionOrder: SupervisionOrderListEntry) {
|
||||||
|
this.delete.emit(supervisionOrder);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
<div class="my-1">
|
||||||
|
<ds-supervision-order-status [supervisionOrderList]="supervisionOrderList"
|
||||||
|
(delete)="deleteSupervisionOrder($event)"></ds-supervision-order-status>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-children-mr">
|
||||||
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.workflow.item.delete' | translate">
|
||||||
|
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.workflow.item.delete" | translate}}</span>
|
||||||
|
</a>
|
||||||
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 policies-link" [routerLink]="resourcePoliciesPageRoute" [title]="'admin.workflow.item.policies' | translate">
|
||||||
|
<i class="fas fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{'admin.workflow.item.policies' | translate}}</span>
|
||||||
|
</a>
|
||||||
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 supervision-group-selector" [title]="'admin.workflow.item.supervision' | translate" (click)="openSupervisionModal()">
|
||||||
|
<i class="fas fa-users-cog"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{'admin.workflow.item.supervision' | translate}}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
@@ -0,0 +1,156 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { URLCombiner } from '../../../../../core/url-combiner/url-combiner';
|
||||||
|
import { WorkspaceItemAdminWorkflowActionsComponent } from './workspace-item-admin-workflow-actions.component';
|
||||||
|
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||||
|
import {
|
||||||
|
getWorkflowItemDeleteRoute,
|
||||||
|
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
|
import { RequestEntryState } from '../../../../../core/data/request-entry-state.model';
|
||||||
|
import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service.stub';
|
||||||
|
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
|
||||||
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import { ConfirmationModalComponent } from '../../../../../shared/confirmation-modal/confirmation-modal.component';
|
||||||
|
import { supervisionOrderEntryMock } from '../../../../../shared/testing/supervision-order.mock';
|
||||||
|
import {
|
||||||
|
SupervisionOrderGroupSelectorComponent
|
||||||
|
} from './supervision-order-group-selector/supervision-order-group-selector.component';
|
||||||
|
|
||||||
|
describe('WorkspaceItemAdminWorkflowActionsComponent', () => {
|
||||||
|
let component: WorkspaceItemAdminWorkflowActionsComponent;
|
||||||
|
let fixture: ComponentFixture<WorkspaceItemAdminWorkflowActionsComponent>;
|
||||||
|
let id;
|
||||||
|
let wsi;
|
||||||
|
let item = new Item();
|
||||||
|
item.uuid = 'itemUUID1111';
|
||||||
|
const rd = new RemoteData(undefined, undefined, undefined, RequestEntryState.Success, undefined, item, 200);
|
||||||
|
let supervisionOrderDataService;
|
||||||
|
let notificationService: NotificationsServiceStub;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
notificationService = new NotificationsServiceStub();
|
||||||
|
supervisionOrderDataService = jasmine.createSpyObj('supervisionOrderDataService', {
|
||||||
|
searchByItem: jasmine.createSpy('searchByItem'),
|
||||||
|
delete: jasmine.createSpy('delete'),
|
||||||
|
});
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
wsi = new WorkspaceItem();
|
||||||
|
wsi.id = id;
|
||||||
|
wsi.item = of(rd);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NgbModalModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [WorkspaceItemAdminWorkflowActionsComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: NotificationsService, useValue: notificationService },
|
||||||
|
{ provide: SupervisionOrderDataService, useValue: supervisionOrderDataService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(WorkspaceItemAdminWorkflowActionsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.wsi = wsi;
|
||||||
|
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(getWorkflowItemDeleteRoute(wsi.id)).toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a policies button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.policies-link'));
|
||||||
|
const link = a.nativeElement.href;
|
||||||
|
expect(link).toContain(new URLCombiner('/items/itemUUID1111/edit/authorizations').toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteSupervisionOrder', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(component.delete, 'emit');
|
||||||
|
spyOn((component as any).modalService, 'open').and.returnValue({
|
||||||
|
componentInstance: { response: of(true) }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when delete succeeded', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
supervisionOrderDataService.delete.and.returnValue(of(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should notify success', () => {
|
||||||
|
component.deleteSupervisionOrder(supervisionOrderEntryMock);
|
||||||
|
expect((component as any).modalService.open).toHaveBeenCalledWith(ConfirmationModalComponent);
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
expect(component.delete.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when delete failed', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
supervisionOrderDataService.delete.and.returnValue(of(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should notify success', () => {
|
||||||
|
component.deleteSupervisionOrder(supervisionOrderEntryMock);
|
||||||
|
expect((component as any).modalService.open).toHaveBeenCalledWith(ConfirmationModalComponent);
|
||||||
|
expect(notificationService.error).toHaveBeenCalled();
|
||||||
|
expect(component.delete.emit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('openSupervisionModal', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(component.create, 'emit');
|
||||||
|
spyOn((component as any).modalService, 'open').and.returnValue({
|
||||||
|
componentInstance: { create: of(true) }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit create event properly', () => {
|
||||||
|
component.openSupervisionModal();
|
||||||
|
expect((component as any).modalService.open).toHaveBeenCalledWith(SupervisionOrderGroupSelectorComponent, {
|
||||||
|
size: 'lg',
|
||||||
|
backdrop: 'static'
|
||||||
|
});
|
||||||
|
expect(component.create.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,192 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
import { switchMap, take, tap } from 'rxjs/operators';
|
||||||
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { getFirstSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||||
|
import {
|
||||||
|
SupervisionOrderGroupSelectorComponent
|
||||||
|
} from './supervision-order-group-selector/supervision-order-group-selector.component';
|
||||||
|
import {
|
||||||
|
getWorkflowItemDeleteRoute
|
||||||
|
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||||
|
import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../../../item-page/edit-item-page/edit-item-page.routing-paths';
|
||||||
|
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||||
|
import { SupervisionOrder } from '../../../../../core/supervision-order/models/supervision-order.model';
|
||||||
|
import { SupervisionOrderListEntry } from './supervision-order-status/supervision-order-status.component';
|
||||||
|
import { ConfirmationModalComponent } from '../../../../../shared/confirmation-modal/confirmation-modal.component';
|
||||||
|
import { hasValue } from '../../../../../shared/empty.util';
|
||||||
|
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
|
||||||
|
import { getSearchResultFor } from '../../../../../shared/search/search-result-element-decorator';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-workspace-item-admin-workflow-actions-element',
|
||||||
|
styleUrls: ['./workspace-item-admin-workflow-actions.component.scss'],
|
||||||
|
templateUrl: './workspace-item-admin-workflow-actions.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying the actions for a list element for a workspace-item on the admin workflow search page
|
||||||
|
*/
|
||||||
|
export class WorkspaceItemAdminWorkflowActionsComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The workspace item to perform the actions on
|
||||||
|
*/
|
||||||
|
@Input() public wsi: WorkspaceItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to use small buttons or not
|
||||||
|
*/
|
||||||
|
@Input() public small: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of supervision order object to show
|
||||||
|
*/
|
||||||
|
@Input() supervisionOrderList: SupervisionOrder[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item related to the workspace item
|
||||||
|
*/
|
||||||
|
item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array containing the route to the resource policies page
|
||||||
|
*/
|
||||||
|
resourcePoliciesPageRoute: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The i18n keys prefix
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private messagePrefix = 'workflow-item.search.result';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when a new SupervisionOrder has been created
|
||||||
|
*/
|
||||||
|
@Output() create: EventEmitter<DSpaceObject> = new EventEmitter<DSpaceObject>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when new SupervisionOrder has been deleted
|
||||||
|
*/
|
||||||
|
@Output() delete: EventEmitter<DSpaceObject> = new EventEmitter<DSpaceObject>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when a new SupervisionOrder has been created
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
protected dsoNameService: DSONameService,
|
||||||
|
protected modalService: NgbModal,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected supervisionOrderDataService: SupervisionOrderDataService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const item$: Observable<Item> = this.wsi.item.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
|
||||||
|
item$.pipe(
|
||||||
|
map((item: Item) => this.getPoliciesRoute(item))
|
||||||
|
).subscribe((route: string[]) => {
|
||||||
|
this.resourcePoliciesPageRoute = route;
|
||||||
|
});
|
||||||
|
|
||||||
|
item$.subscribe((item: Item) => {
|
||||||
|
this.item = item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the delete page of this workflow item
|
||||||
|
*/
|
||||||
|
getDeleteRoute(): string {
|
||||||
|
return getWorkflowItemDeleteRoute(this.wsi.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the administrative edit page policies tab
|
||||||
|
*/
|
||||||
|
getPoliciesRoute(item: Item): string[] {
|
||||||
|
return ['/items', item.uuid, 'edit', ITEM_EDIT_AUTHORIZATIONS_PATH];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the Group from the Repository. The Group will be the only that this form is showing.
|
||||||
|
* It'll either show a success or error message depending on whether delete was successful or not.
|
||||||
|
*/
|
||||||
|
deleteSupervisionOrder(supervisionOrderEntry: SupervisionOrderListEntry) {
|
||||||
|
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||||
|
modalRef.componentInstance.dso = supervisionOrderEntry.group;
|
||||||
|
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-supervision.modal.header';
|
||||||
|
modalRef.componentInstance.infoLabel = this.messagePrefix + '.delete-supervision.modal.info';
|
||||||
|
modalRef.componentInstance.cancelLabel = this.messagePrefix + '.delete-supervision.modal.cancel';
|
||||||
|
modalRef.componentInstance.confirmLabel = this.messagePrefix + '.delete-supervision.modal.confirm';
|
||||||
|
modalRef.componentInstance.brandColor = 'danger';
|
||||||
|
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
|
||||||
|
modalRef.componentInstance.response.pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((confirm: boolean) => {
|
||||||
|
if (confirm && hasValue(supervisionOrderEntry.supervisionOrder.id)) {
|
||||||
|
return this.supervisionOrderDataService.delete(supervisionOrderEntry.supervisionOrder.id).pipe(
|
||||||
|
take(1),
|
||||||
|
tap((result: boolean) => {
|
||||||
|
if (result) {
|
||||||
|
this.notificationsService.success(
|
||||||
|
null,
|
||||||
|
this.translateService.get(
|
||||||
|
this.messagePrefix + '.notification.deleted.success',
|
||||||
|
{ name: this.dsoNameService.getName(supervisionOrderEntry.group) }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(
|
||||||
|
null,
|
||||||
|
this.translateService.get(
|
||||||
|
this.messagePrefix + '.notification.deleted.failure',
|
||||||
|
{ name: this.dsoNameService.getName(supervisionOrderEntry.group) }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).subscribe((result: boolean) => {
|
||||||
|
if (result) {
|
||||||
|
this.delete.emit(this.convertReloadedObject());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the Supervision Modal to create a supervision order
|
||||||
|
*/
|
||||||
|
openSupervisionModal() {
|
||||||
|
const supervisionModal: NgbModalRef = this.modalService.open(SupervisionOrderGroupSelectorComponent, {
|
||||||
|
size: 'lg',
|
||||||
|
backdrop: 'static'
|
||||||
|
});
|
||||||
|
supervisionModal.componentInstance.itemUUID = this.item.uuid;
|
||||||
|
supervisionModal.componentInstance.create.subscribe(() => {
|
||||||
|
this.create.emit(this.convertReloadedObject());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the reloadedObject to the Type required by this dso.
|
||||||
|
*/
|
||||||
|
private convertReloadedObject(): DSpaceObject {
|
||||||
|
const constructor = getSearchResultFor((this.wsi as any).constructor);
|
||||||
|
return Object.assign(new constructor(), this.wsi, {
|
||||||
|
indexableObject: this.wsi
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -7,14 +7,22 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl
|
|||||||
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './workflow-item-search-result-admin-workflow-grid-element.component';
|
import {
|
||||||
|
WorkflowItemSearchResultAdminWorkflowGridElementComponent
|
||||||
|
} from './workflow-item-search-result-admin-workflow-grid-element.component';
|
||||||
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { ItemGridElementComponent } from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-grid-element.component';
|
import {
|
||||||
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
ItemGridElementComponent
|
||||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
} from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-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 { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
||||||
@@ -22,7 +30,7 @@ import { of as observableOf } from 'rxjs';
|
|||||||
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||||
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
|
||||||
describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
|
describe('WorkflowItemSearchResultAdminWorkflowGridElementComponent', () => {
|
||||||
let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent;
|
let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent;
|
||||||
let fixture: ComponentFixture<WorkflowItemSearchResultAdminWorkflowGridElementComponent>;
|
let fixture: ComponentFixture<WorkflowItemSearchResultAdminWorkflowGridElementComponent>;
|
||||||
let id;
|
let id;
|
||||||
|
@@ -0,0 +1,16 @@
|
|||||||
|
<ng-template dsListableObject>
|
||||||
|
</ng-template>
|
||||||
|
<div #badges class="position-absolute ml-1">
|
||||||
|
<div class="workflow-badge">
|
||||||
|
<span class="badge badge-info">{{ "admin.workflow.item.workspace" | translate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul #buttons class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<ds-workspace-item-admin-workflow-actions-element [small]="true"
|
||||||
|
[supervisionOrderList]="supervisionOrder$ | async"
|
||||||
|
[wsi]="dso"
|
||||||
|
(create)="reloadObject($event)"
|
||||||
|
(delete)="reloadObject($event)"></ds-workspace-item-admin-workflow-actions-element>
|
||||||
|
</li>
|
||||||
|
</ul>
|
@@ -0,0 +1,127 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
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 {
|
||||||
|
WorkspaceItemSearchResultAdminWorkflowGridElementComponent
|
||||||
|
} from './workspace-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 {
|
||||||
|
ItemGridElementComponent
|
||||||
|
} from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-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';
|
||||||
|
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||||
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
import {
|
||||||
|
supervisionOrderPaginatedListRD,
|
||||||
|
supervisionOrderPaginatedListRD$
|
||||||
|
} from '../../../../../shared/testing/supervision-order.mock';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
describe('WorkspaceItemSearchResultAdminWorkflowGridElementComponent', () => {
|
||||||
|
let component: WorkspaceItemSearchResultAdminWorkflowGridElementComponent;
|
||||||
|
let fixture: ComponentFixture<WorkspaceItemSearchResultAdminWorkflowGridElementComponent>;
|
||||||
|
let id;
|
||||||
|
let wfi;
|
||||||
|
let itemRD$;
|
||||||
|
let linkService;
|
||||||
|
let object;
|
||||||
|
let themeService;
|
||||||
|
let supervisionOrderDataService;
|
||||||
|
|
||||||
|
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();
|
||||||
|
themeService = getMockThemeService();
|
||||||
|
supervisionOrderDataService = jasmine.createSpyObj('supervisionOrderDataService', {
|
||||||
|
searchByItem: jasmine.createSpy('searchByItem'),
|
||||||
|
delete: jasmine.createSpy('delete'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule(
|
||||||
|
{
|
||||||
|
declarations: [WorkspaceItemSearchResultAdminWorkflowGridElementComponent, ItemGridElementComponent, ListableObjectDirective],
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: ThemeService, useValue: themeService },
|
||||||
|
{
|
||||||
|
provide: TruncatableService, useValue: {
|
||||||
|
isCollapsed: () => observableOf(true),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ provide: BitstreamDataService, useValue: {} },
|
||||||
|
{ provide: SupervisionOrderDataService, useValue: supervisionOrderDataService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.overrideComponent(WorkspaceItemSearchResultAdminWorkflowGridElementComponent, {
|
||||||
|
set: {
|
||||||
|
entryComponents: [ItemGridElementComponent]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
linkService.resolveLink.and.callFake((a) => a);
|
||||||
|
fixture = TestBed.createComponent(WorkspaceItemSearchResultAdminWorkflowGridElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = object;
|
||||||
|
component.linkTypes = CollectionElementLinkType;
|
||||||
|
component.index = 0;
|
||||||
|
component.viewModes = ViewMode;
|
||||||
|
supervisionOrderDataService.searchByItem.and.returnValue(supervisionOrderPaginatedListRD$);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve the item using the link service', () => {
|
||||||
|
expect(linkService.resolveLink).toHaveBeenCalledWith(wfi, followLink('item'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve supervision order objects properly', () => {
|
||||||
|
expect(component.supervisionOrder$.value).toEqual(supervisionOrderPaginatedListRD.payload.page);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit reloadedObject properly ', () => {
|
||||||
|
spyOn(component.reloadedObject, 'emit');
|
||||||
|
const dso = new DSpaceObject();
|
||||||
|
component.reloadObject(dso);
|
||||||
|
expect(component.reloadedObject.emit).toHaveBeenCalledWith(dso);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,158 @@
|
|||||||
|
import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
import { map, mergeMap, take, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
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 { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||||
|
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,
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getRemoteDataPayload
|
||||||
|
} from '../../../../../core/shared/operators';
|
||||||
|
import {
|
||||||
|
WorkspaceItemSearchResult
|
||||||
|
} from '../../../../../shared/object-collection/shared/workspace-item-search-result.model';
|
||||||
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
|
||||||
|
import { SupervisionOrder } from '../../../../../core/supervision-order/models/supervision-order.model';
|
||||||
|
import { PaginatedList } from '../../../../../core/data/paginated-list.model';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
|
||||||
|
@listableObjectComponent(WorkspaceItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-workflow-item-search-result-admin-workflow-grid-element',
|
||||||
|
styleUrls: ['./workspace-item-search-result-admin-workflow-grid-element.component.scss'],
|
||||||
|
templateUrl: './workspace-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 WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent<WorkspaceItemSearchResult, WorkspaceItem> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item linked to the workspace item
|
||||||
|
*/
|
||||||
|
public item$: Observable<Item>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the item linked to the workflow item
|
||||||
|
*/
|
||||||
|
public itemId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The supervision orders linked to the workflow item
|
||||||
|
*/
|
||||||
|
public supervisionOrder$: BehaviorSubject<SupervisionOrder[]> = new BehaviorSubject<SupervisionOrder[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private componentFactoryResolver: ComponentFactoryResolver,
|
||||||
|
private linkService: LinkService,
|
||||||
|
protected truncatableService: TruncatableService,
|
||||||
|
private themeService: ThemeService,
|
||||||
|
protected bitstreamDataService: BitstreamDataService,
|
||||||
|
protected supervisionOrderDataService: SupervisionOrderDataService,
|
||||||
|
) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.item$.pipe(
|
||||||
|
take(1),
|
||||||
|
tap((item: Item) => this.itemId = item.id),
|
||||||
|
mergeMap((item: Item) => this.retrieveSupervisorOrders(item.id))
|
||||||
|
).subscribe((supervisionOrderList: SupervisionOrder[]) => {
|
||||||
|
this.supervisionOrder$.next(supervisionOrderList);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the component depending on the item's entity type, view mode and context
|
||||||
|
* @returns {GenericConstructor<Component>}
|
||||||
|
*/
|
||||||
|
private getComponent(item: Item): GenericConstructor<Component> {
|
||||||
|
return getListableObjectComponent(item.getRenderTypes(), ViewMode.GridElement, undefined, this.themeService.getThemeName());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the list of SupervisionOrder object related to the given item
|
||||||
|
*
|
||||||
|
* @param itemId
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private retrieveSupervisorOrders(itemId): Observable<SupervisionOrder[]> {
|
||||||
|
return this.supervisionOrderDataService.searchByItem(
|
||||||
|
itemId, false, true, followLink('group')
|
||||||
|
).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((soRD: RemoteData<PaginatedList<SupervisionOrder>>) => soRD.hasSucceeded && !soRD.hasNoContent ? soRD.payload.page : [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadObject(dso: DSpaceObject) {
|
||||||
|
this.reloadedObject.emit(dso);
|
||||||
|
}
|
||||||
|
}
|
@@ -9,11 +9,15 @@ import { CollectionElementLinkType } from '../../../../../shared/object-collecti
|
|||||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './workflow-item-search-result-admin-workflow-list-element.component';
|
import {
|
||||||
|
WorkflowItemSearchResultAdminWorkflowListElementComponent
|
||||||
|
} from './workflow-item-search-result-admin-workflow-list-element.component';
|
||||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
import {
|
||||||
|
WorkflowItemSearchResult
|
||||||
|
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
@@ -21,7 +25,7 @@ import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service
|
|||||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
import { environment } from '../../../../../../environments/environment';
|
import { environment } from '../../../../../../environments/environment';
|
||||||
|
|
||||||
describe('WorkflowItemAdminWorkflowListElementComponent', () => {
|
describe('WorkflowItemSearchResultAdminWorkflowListElementComponent', () => {
|
||||||
let component: WorkflowItemSearchResultAdminWorkflowListElementComponent;
|
let component: WorkflowItemSearchResultAdminWorkflowListElementComponent;
|
||||||
let fixture: ComponentFixture<WorkflowItemSearchResultAdminWorkflowListElementComponent>;
|
let fixture: ComponentFixture<WorkflowItemSearchResultAdminWorkflowListElementComponent>;
|
||||||
let id;
|
let id;
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { Component, Inject, OnInit } from '@angular/core';
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
import {
|
||||||
|
listableObjectComponent
|
||||||
|
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
import { Context } from '../../../../../core/shared/context.model';
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@@ -9,9 +11,13 @@ import { followLink } from '../../../../../shared/utils/follow-link-config.model
|
|||||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators';
|
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
import {
|
||||||
|
SearchResultListElementComponent
|
||||||
|
} from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
import {
|
||||||
|
WorkflowItemSearchResult
|
||||||
|
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
|
||||||
|
|
||||||
@@ -22,7 +28,7 @@ import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.inter
|
|||||||
templateUrl: './workflow-item-search-result-admin-workflow-list-element.component.html'
|
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
|
* The component for displaying a list element for a workflow item on the admin workflow search page
|
||||||
*/
|
*/
|
||||||
export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends SearchResultListElementComponent<WorkflowItemSearchResult, WorkflowItem> implements OnInit {
|
export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends SearchResultListElementComponent<WorkflowItemSearchResult, WorkflowItem> implements OnInit {
|
||||||
|
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
<span class="badge badge-info">{{ "admin.workflow.item.workspace" | translate }}</span>
|
||||||
|
<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-workspace-item-admin-workflow-actions-element [small]="false"
|
||||||
|
[supervisionOrderList]="supervisionOrder$ | async"
|
||||||
|
[wsi]="dso"
|
||||||
|
(create)="reloadObject($event)"
|
||||||
|
(delete)="reloadObject($event)"></ds-workspace-item-admin-workflow-actions-element>
|
@@ -0,0 +1,111 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/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 { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
|
import {
|
||||||
|
WorkspaceItemSearchResultAdminWorkflowListElementComponent
|
||||||
|
} from './workspace-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';
|
||||||
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../../../environments/environment';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import {
|
||||||
|
supervisionOrderPaginatedListRD,
|
||||||
|
supervisionOrderPaginatedListRD$
|
||||||
|
} from '../../../../../shared/testing/supervision-order.mock';
|
||||||
|
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
describe('WorkspaceItemSearchResultAdminWorkflowListElementComponent', () => {
|
||||||
|
let component: WorkspaceItemSearchResultAdminWorkflowListElementComponent;
|
||||||
|
let fixture: ComponentFixture<WorkspaceItemSearchResultAdminWorkflowListElementComponent>;
|
||||||
|
let id;
|
||||||
|
let wfi;
|
||||||
|
let itemRD$;
|
||||||
|
let linkService;
|
||||||
|
let object;
|
||||||
|
let supervisionOrderDataService;
|
||||||
|
|
||||||
|
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();
|
||||||
|
supervisionOrderDataService = jasmine.createSpyObj('supervisionOrderDataService', {
|
||||||
|
searchByItem: jasmine.createSpy('searchByItem'),
|
||||||
|
delete: jasmine.createSpy('delete'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule(
|
||||||
|
{
|
||||||
|
declarations: [WorkspaceItemSearchResultAdminWorkflowListElementComponent],
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: SupervisionOrderDataService, useValue: supervisionOrderDataService },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
linkService.resolveLink.and.callFake((a) => a);
|
||||||
|
fixture = TestBed.createComponent(WorkspaceItemSearchResultAdminWorkflowListElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = object;
|
||||||
|
component.linkTypes = CollectionElementLinkType;
|
||||||
|
component.index = 0;
|
||||||
|
component.viewModes = ViewMode;
|
||||||
|
supervisionOrderDataService.searchByItem.and.returnValue(supervisionOrderPaginatedListRD$);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve the item using the link service', () => {
|
||||||
|
expect(linkService.resolveLink).toHaveBeenCalledWith(wfi, followLink('item'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve supervision order objects properly', () => {
|
||||||
|
expect(component.supervisionOrder$.value).toEqual(supervisionOrderPaginatedListRD.payload.page);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit reloadedObject properly ', () => {
|
||||||
|
spyOn(component.reloadedObject, 'emit');
|
||||||
|
const dso = new DSpaceObject();
|
||||||
|
component.reloadObject(dso);
|
||||||
|
expect(component.reloadedObject.emit).toHaveBeenCalledWith(dso);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,109 @@
|
|||||||
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
import { map, mergeMap, take, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
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 { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||||
|
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,
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
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 { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
|
||||||
|
import {
|
||||||
|
WorkspaceItemSearchResult
|
||||||
|
} from '../../../../../shared/object-collection/shared/workspace-item-search-result.model';
|
||||||
|
import { SupervisionOrder } from '../../../../../core/supervision-order/models/supervision-order.model';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import { PaginatedList } from '../../../../../core/data/paginated-list.model';
|
||||||
|
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
@listableObjectComponent(WorkspaceItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-workflow-item-search-result-admin-workflow-list-element',
|
||||||
|
styleUrls: ['./workspace-item-search-result-admin-workflow-list-element.component.scss'],
|
||||||
|
templateUrl: './workspace-item-search-result-admin-workflow-list-element.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a list element for a workflow item on the admin workflow search page
|
||||||
|
*/
|
||||||
|
export class WorkspaceItemSearchResultAdminWorkflowListElementComponent extends SearchResultListElementComponent<WorkspaceItemSearchResult, WorkspaceItem> implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item linked to the workflow item
|
||||||
|
*/
|
||||||
|
public item$: Observable<Item>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the item linked to the workflow item
|
||||||
|
*/
|
||||||
|
public itemId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The supervision orders linked to the workflow item
|
||||||
|
*/
|
||||||
|
public supervisionOrder$: BehaviorSubject<SupervisionOrder[]> = new BehaviorSubject<SupervisionOrder[]>([]);
|
||||||
|
|
||||||
|
constructor(private linkService: LinkService,
|
||||||
|
protected dsoNameService: DSONameService,
|
||||||
|
protected supervisionOrderDataService: SupervisionOrderDataService,
|
||||||
|
protected truncatableService: TruncatableService,
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig
|
||||||
|
) {
|
||||||
|
super(truncatableService, dsoNameService, appConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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),
|
||||||
|
tap((item: Item) => this.itemId = item.id),
|
||||||
|
mergeMap((item: Item) => this.retrieveSupervisorOrders(item.id))
|
||||||
|
).subscribe((supervisionOrderList: SupervisionOrder[]) => {
|
||||||
|
this.supervisionOrder$.next(supervisionOrderList);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the list of SupervisionOrder object related to the given item
|
||||||
|
*
|
||||||
|
* @param itemId
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private retrieveSupervisorOrders(itemId): Observable<SupervisionOrder[]> {
|
||||||
|
return this.supervisionOrderDataService.searchByItem(
|
||||||
|
itemId, false, true, followLink('group')
|
||||||
|
).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((soRD: RemoteData<PaginatedList<SupervisionOrder>>) => soRD.hasSucceeded && !soRD.hasNoContent ? soRD.payload.page : [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload list element after supervision order change.
|
||||||
|
*/
|
||||||
|
reloadObject(dso: DSpaceObject) {
|
||||||
|
this.reloadedObject.emit(dso);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,16 +1,39 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
|
||||||
|
|
||||||
import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-search-results/workflow-item-admin-workflow-actions.component';
|
import {
|
||||||
import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component';
|
WorkflowItemSearchResultAdminWorkflowGridElementComponent
|
||||||
|
} from './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-search-results/actions/workflow-item/workflow-item-admin-workflow-actions.component';
|
||||||
|
import {
|
||||||
|
WorkflowItemSearchResultAdminWorkflowListElementComponent
|
||||||
|
} from './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.component';
|
import { AdminWorkflowPageComponent } from './admin-workflow-page.component';
|
||||||
import { SearchModule } from '../../shared/search/search.module';
|
import { SearchModule } from '../../shared/search/search.module';
|
||||||
|
import {
|
||||||
|
WorkspaceItemAdminWorkflowActionsComponent
|
||||||
|
} from './admin-workflow-search-results/actions/workspace-item/workspace-item-admin-workflow-actions.component';
|
||||||
|
import {
|
||||||
|
WorkspaceItemSearchResultAdminWorkflowListElementComponent
|
||||||
|
} from './admin-workflow-search-results/admin-workflow-search-result-list-element/workspace-item/workspace-item-search-result-admin-workflow-list-element.component';
|
||||||
|
import {
|
||||||
|
WorkspaceItemSearchResultAdminWorkflowGridElementComponent
|
||||||
|
} from './admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component';
|
||||||
|
import {
|
||||||
|
SupervisionOrderGroupSelectorComponent
|
||||||
|
} from './admin-workflow-search-results/actions/workspace-item/supervision-order-group-selector/supervision-order-group-selector.component';
|
||||||
|
import {
|
||||||
|
SupervisionOrderStatusComponent
|
||||||
|
} from './admin-workflow-search-results/actions/workspace-item/supervision-order-status/supervision-order-status.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
WorkflowItemSearchResultAdminWorkflowListElementComponent,
|
WorkflowItemSearchResultAdminWorkflowListElementComponent,
|
||||||
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
|
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
|
||||||
|
WorkspaceItemSearchResultAdminWorkflowListElementComponent,
|
||||||
|
WorkspaceItemSearchResultAdminWorkflowGridElementComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -20,7 +43,10 @@ const ENTRY_COMPONENTS = [
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AdminWorkflowPageComponent,
|
AdminWorkflowPageComponent,
|
||||||
|
SupervisionOrderGroupSelectorComponent,
|
||||||
|
SupervisionOrderStatusComponent,
|
||||||
WorkflowItemAdminWorkflowActionsComponent,
|
WorkflowItemAdminWorkflowActionsComponent,
|
||||||
|
WorkspaceItemAdminWorkflowActionsComponent,
|
||||||
...ENTRY_COMPONENTS
|
...ENTRY_COMPONENTS
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
@@ -126,3 +126,9 @@ export function getRequestCopyModulePath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const HEALTH_PAGE_PATH = 'health';
|
export const HEALTH_PAGE_PATH = 'health';
|
||||||
|
|
||||||
|
export const SUBSCRIPTIONS_MODULE_PATH = 'subscriptions';
|
||||||
|
|
||||||
|
export function getSubscriptionsModuleRoute() {
|
||||||
|
return `/${SUBSCRIPTIONS_MODULE_PATH}`;
|
||||||
|
}
|
||||||
|
@@ -218,7 +218,8 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
|
|||||||
{
|
{
|
||||||
path: 'statistics',
|
path: 'statistics',
|
||||||
loadChildren: () => import('./statistics-page/statistics-page-routing.module')
|
loadChildren: () => import('./statistics-page/statistics-page-routing.module')
|
||||||
.then((m) => m.StatisticsPageRoutingModule)
|
.then((m) => m.StatisticsPageRoutingModule),
|
||||||
|
canActivate: [EndUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: HEALTH_PAGE_PATH,
|
path: HEALTH_PAGE_PATH,
|
||||||
@@ -228,7 +229,13 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
|
|||||||
{
|
{
|
||||||
path: ACCESS_CONTROL_MODULE_PATH,
|
path: ACCESS_CONTROL_MODULE_PATH,
|
||||||
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
|
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
|
||||||
canActivate: [GroupAdministratorGuard],
|
canActivate: [GroupAdministratorGuard, EndUserAgreementCurrentUserGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subscriptions',
|
||||||
|
loadChildren: () => import('./subscriptions-page/subscriptions-page-routing.module')
|
||||||
|
.then((m) => m.SubscriptionsPageRoutingModule),
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
},
|
},
|
||||||
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
|
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
|
||||||
]
|
]
|
||||||
|
@@ -9,6 +9,8 @@ import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router
|
|||||||
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||||
|
import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core';
|
||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { appEffects } from './app.effects';
|
import { appEffects } from './app.effects';
|
||||||
@@ -101,6 +103,8 @@ const PROVIDERS = [
|
|||||||
useClass: LogInterceptor,
|
useClass: LogInterceptor,
|
||||||
multi: true
|
multi: true
|
||||||
},
|
},
|
||||||
|
// register the dynamic matcher used by form. MUST be provided by the app module
|
||||||
|
...DYNAMIC_MATCHER_PROVIDERS,
|
||||||
];
|
];
|
||||||
|
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
|
@@ -47,6 +47,7 @@ import { truncatableReducer, TruncatablesState } from './shared/truncatable/trun
|
|||||||
import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer';
|
import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer';
|
||||||
import { MenusState } from './shared/menu/menus-state.model';
|
import { MenusState } from './shared/menu/menus-state.model';
|
||||||
import { correlationIdReducer } from './correlation-id/correlation-id.reducer';
|
import { correlationIdReducer } from './correlation-id/correlation-id.reducer';
|
||||||
|
import { contextHelpReducer, ContextHelpState } from './shared/context-help.reducer';
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
router: RouterReducerState;
|
router: RouterReducerState;
|
||||||
@@ -67,6 +68,7 @@ export interface AppState {
|
|||||||
epeopleRegistry: EPeopleRegistryState;
|
epeopleRegistry: EPeopleRegistryState;
|
||||||
groupRegistry: GroupRegistryState;
|
groupRegistry: GroupRegistryState;
|
||||||
correlationId: string;
|
correlationId: string;
|
||||||
|
contextHelp: ContextHelpState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appReducers: ActionReducerMap<AppState> = {
|
export const appReducers: ActionReducerMap<AppState> = {
|
||||||
@@ -87,7 +89,8 @@ export const appReducers: ActionReducerMap<AppState> = {
|
|||||||
communityList: CommunityListReducer,
|
communityList: CommunityListReducer,
|
||||||
epeopleRegistry: ePeopleRegistryReducer,
|
epeopleRegistry: ePeopleRegistryReducer,
|
||||||
groupRegistry: groupRegistryReducer,
|
groupRegistry: groupRegistryReducer,
|
||||||
correlationId: correlationIdReducer
|
correlationId: correlationIdReducer,
|
||||||
|
contextHelp: contextHelpReducer,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const routerStateSelector = (state: AppState) => state.router;
|
export const routerStateSelector = (state: AppState) => state.router;
|
||||||
|
@@ -618,7 +618,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
// TODO: Set bitstream to primary when supported
|
// TODO: Set bitstream to primary when supported
|
||||||
const primary = rawForm.fileNamePrimaryContainer.primaryBitstream;
|
const primary = rawForm.fileNamePrimaryContainer.primaryBitstream;
|
||||||
Metadata.setFirstValue(newMetadata, 'dc.title', rawForm.fileNamePrimaryContainer.fileName);
|
Metadata.setFirstValue(newMetadata, 'dc.title', rawForm.fileNamePrimaryContainer.fileName);
|
||||||
|
if (isEmpty(rawForm.descriptionContainer.description)) {
|
||||||
|
delete newMetadata['dc.description'];
|
||||||
|
} else {
|
||||||
Metadata.setFirstValue(newMetadata, 'dc.description', rawForm.descriptionContainer.description);
|
Metadata.setFirstValue(newMetadata, 'dc.description', rawForm.descriptionContainer.description);
|
||||||
|
}
|
||||||
if (this.isIIIF) {
|
if (this.isIIIF) {
|
||||||
// It's helpful to remove these metadata elements entirely when the form value is empty.
|
// It's helpful to remove these metadata elements entirely when the form value is empty.
|
||||||
// This avoids potential issues on the REST side and makes it possible to do things like
|
// This avoids potential issues on the REST side and makes it possible to do things like
|
||||||
|
@@ -65,6 +65,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails);
|
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails);
|
||||||
this.updatePageWithItems(searchOptions, this.value, undefined);
|
this.updatePageWithItems(searchOptions, this.value, undefined);
|
||||||
this.updateParent(params.scope);
|
this.updateParent(params.scope);
|
||||||
|
this.updateLogo();
|
||||||
this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope);
|
this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,17 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<ng-container *ngVar="(parent$ | async) as parent">
|
<ng-container *ngVar="(parent$ | async) as parent">
|
||||||
<ng-container *ngIf="parent?.payload as parentContext">
|
<ng-container *ngIf="parent?.payload as parentContext">
|
||||||
<header class="comcol-header border-bottom mb-4 pb-4">
|
<div class="d-flex flex-row border-bottom mb-4 pb-4">
|
||||||
|
|
||||||
|
<header class="comcol-header mr-auto">
|
||||||
<!-- Parent Name -->
|
<!-- Parent Name -->
|
||||||
<ds-comcol-page-header [name]="parentContext.name">
|
<ds-comcol-page-header [name]="parentContext.name">
|
||||||
</ds-comcol-page-header>
|
</ds-comcol-page-header>
|
||||||
|
<!-- Collection logo -->
|
||||||
|
<ds-comcol-page-logo *ngIf="logo$"
|
||||||
|
[logo]="(logo$ | async)?.payload"
|
||||||
|
[alternateText]="'Community or Collection Logo'">
|
||||||
|
</ds-comcol-page-logo>
|
||||||
<!-- Handle -->
|
<!-- Handle -->
|
||||||
<ds-themed-comcol-page-handle
|
<ds-themed-comcol-page-handle
|
||||||
[content]="parentContext.handle"
|
[content]="parentContext.handle"
|
||||||
@@ -17,6 +24,8 @@
|
|||||||
<ds-comcol-page-content [content]="parentContext.sidebarText" [hasInnerHtml]="true" [title]="'community.page.news'">
|
<ds-comcol-page-content [content]="parentContext.sidebarText" [hasInnerHtml]="true" [title]="'community.page.news'">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
</header>
|
</header>
|
||||||
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
|
</div>
|
||||||
<!-- Browse-By Links -->
|
<!-- Browse-By Links -->
|
||||||
<ds-themed-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-themed-comcol-page-browse-by>
|
<ds-themed-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-themed-comcol-page-browse-by>
|
||||||
</ng-container></ng-container>
|
</ng-container></ng-container>
|
||||||
|
@@ -144,6 +144,9 @@ describe('BrowseByMetadataPageComponent', () => {
|
|||||||
|
|
||||||
route.params = observableOf(paramsWithValue);
|
route.params = observableOf(paramsWithValue);
|
||||||
comp.ngOnInit();
|
comp.ngOnInit();
|
||||||
|
comp.updateParent('fake-scope');
|
||||||
|
comp.updateLogo();
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch items', () => {
|
it('should fetch items', () => {
|
||||||
@@ -151,6 +154,10 @@ describe('BrowseByMetadataPageComponent', () => {
|
|||||||
expect(result.payload.page).toEqual(mockItems);
|
expect(result.payload.page).toEqual(mockItems);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fetch the logo', () => {
|
||||||
|
expect(comp.logo$).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when calling browseParamsToOptions', () => {
|
describe('when calling browseParamsToOptions', () => {
|
||||||
|
@@ -15,7 +15,11 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv
|
|||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { map } from 'rxjs/operators';
|
import { filter, map, mergeMap } from 'rxjs/operators';
|
||||||
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
||||||
|
|
||||||
export const BBM_PAGINATION_ID = 'bbm';
|
export const BBM_PAGINATION_ID = 'bbm';
|
||||||
@@ -48,6 +52,11 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
parent$: Observable<RemoteData<DSpaceObject>>;
|
parent$: Observable<RemoteData<DSpaceObject>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logo of the current Community or Collection
|
||||||
|
*/
|
||||||
|
logo$: Observable<RemoteData<Bitstream>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The pagination config used to display the values
|
* The pagination config used to display the values
|
||||||
*/
|
*/
|
||||||
@@ -142,8 +151,15 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
|||||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||||
this.browseId = params.id || this.defaultBrowseId;
|
this.browseId = params.id || this.defaultBrowseId;
|
||||||
this.authority = params.authority;
|
this.authority = params.authority;
|
||||||
this.value = +params.value || params.value || '';
|
|
||||||
this.startsWith = +params.startsWith || params.startsWith;
|
if (typeof params.value === 'string'){
|
||||||
|
this.value = params.value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof params.startsWith === 'string'){
|
||||||
|
this.startsWith = params.startsWith.trim();
|
||||||
|
}
|
||||||
|
|
||||||
if (isNotEmpty(this.value)) {
|
if (isNotEmpty(this.value)) {
|
||||||
this.updatePageWithItems(
|
this.updatePageWithItems(
|
||||||
browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), this.value, this.authority);
|
browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), this.value, this.authority);
|
||||||
@@ -151,6 +167,7 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
|||||||
this.updatePage(browseParamsToOptions(params, currentPage, currentSort, this.browseId, false));
|
this.updatePage(browseParamsToOptions(params, currentPage, currentSort, this.browseId, false));
|
||||||
}
|
}
|
||||||
this.updateParent(params.scope);
|
this.updateParent(params.scope);
|
||||||
|
this.updateLogo();
|
||||||
}));
|
}));
|
||||||
this.updateStartsWithTextOptions();
|
this.updateStartsWithTextOptions();
|
||||||
|
|
||||||
@@ -196,12 +213,31 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
updateParent(scope: string) {
|
updateParent(scope: string) {
|
||||||
if (hasValue(scope)) {
|
if (hasValue(scope)) {
|
||||||
this.parent$ = this.dsoService.findById(scope).pipe(
|
const linksToFollow = () => {
|
||||||
|
return [followLink('logo')];
|
||||||
|
};
|
||||||
|
this.parent$ = this.dsoService.findById(scope,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
...linksToFollow() as FollowLinkConfig<DSpaceObject>[]).pipe(
|
||||||
getFirstSucceededRemoteData()
|
getFirstSucceededRemoteData()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the parent Community or Collection logo
|
||||||
|
*/
|
||||||
|
updateLogo() {
|
||||||
|
if (hasValue(this.parent$)) {
|
||||||
|
this.logo$ = this.parent$.pipe(
|
||||||
|
map((rd: RemoteData<Collection | Community>) => rd.payload),
|
||||||
|
filter((collectionOrCommunity: Collection | Community) => hasValue(collectionOrCommunity.logo)),
|
||||||
|
mergeMap((collectionOrCommunity: Collection | Community) => collectionOrCommunity.logo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to the previous page
|
* Navigate to the previous page
|
||||||
*/
|
*/
|
||||||
@@ -276,7 +312,7 @@ export function browseParamsToOptions(params: any,
|
|||||||
metadata,
|
metadata,
|
||||||
paginationConfig,
|
paginationConfig,
|
||||||
sortConfig,
|
sortConfig,
|
||||||
+params.startsWith || params.startsWith,
|
params.startsWith,
|
||||||
params.scope,
|
params.scope,
|
||||||
fetchThumbnail
|
fetchThumbnail
|
||||||
);
|
);
|
||||||
|
@@ -4,16 +4,21 @@ import { BrowseByModule } from './browse-by.module';
|
|||||||
import { ItemDataService } from '../core/data/item-data.service';
|
import { ItemDataService } from '../core/data/item-data.service';
|
||||||
import { BrowseService } from '../core/browse/browse.service';
|
import { BrowseService } from '../core/browse/browse.service';
|
||||||
import { BrowseByGuard } from './browse-by-guard';
|
import { BrowseByGuard } from './browse-by-guard';
|
||||||
|
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
SharedBrowseByModule,
|
||||||
BrowseByRoutingModule,
|
BrowseByRoutingModule,
|
||||||
BrowseByModule.withEntryComponents()
|
BrowseByModule.withEntryComponents(),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ItemDataService,
|
ItemDataService,
|
||||||
BrowseService,
|
BrowseService,
|
||||||
BrowseByGuard
|
BrowseByGuard,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class BrowseByPageModule {
|
export class BrowseByPageModule {
|
||||||
|
@@ -4,13 +4,17 @@ import { BrowseByGuard } from './browse-by-guard';
|
|||||||
import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
|
import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
|
||||||
import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
|
import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
|
||||||
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
|
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
|
||||||
|
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
resolve: { breadcrumb: BrowseByDSOBreadcrumbResolver },
|
resolve: {
|
||||||
|
breadcrumb: BrowseByDSOBreadcrumbResolver,
|
||||||
|
menu: DSOEditMenuResolver
|
||||||
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
|
@@ -49,6 +49,7 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
this.browseId = params.id || this.defaultBrowseId;
|
this.browseId = params.id || this.defaultBrowseId;
|
||||||
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined);
|
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined);
|
||||||
this.updateParent(params.scope);
|
this.updateParent(params.scope);
|
||||||
|
this.updateLogo();
|
||||||
}));
|
}));
|
||||||
this.updateStartsWithTextOptions();
|
this.updateStartsWithTextOptions();
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { BrowseByTitlePageComponent } from './browse-by-title-page/browse-by-title-page.component';
|
import { BrowseByTitlePageComponent } from './browse-by-title-page/browse-by-title-page.component';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
|
||||||
import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-by-metadata-page.component';
|
import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component';
|
import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component';
|
||||||
import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component';
|
import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component';
|
||||||
@@ -10,6 +9,8 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
|
|||||||
import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/themed-browse-by-metadata-page.component';
|
import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/themed-browse-by-metadata-page.component';
|
||||||
import { ThemedBrowseByDatePageComponent } from './browse-by-date-page/themed-browse-by-date-page.component';
|
import { ThemedBrowseByDatePageComponent } from './browse-by-date-page/themed-browse-by-date-page.component';
|
||||||
import { ThemedBrowseByTitlePageComponent } from './browse-by-title-page/themed-browse-by-title-page.component';
|
import { ThemedBrowseByTitlePageComponent } from './browse-by-title-page/themed-browse-by-title-page.component';
|
||||||
|
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
|
||||||
|
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
@@ -25,9 +26,10 @@ const ENTRY_COMPONENTS = [
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
SharedBrowseByModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
ComcolModule,
|
ComcolModule,
|
||||||
SharedModule
|
DsoPageModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
BrowseBySwitcherComponent,
|
BrowseBySwitcherComponent,
|
||||||
@@ -45,7 +47,7 @@ export class BrowseByModule {
|
|||||||
*/
|
*/
|
||||||
static withEntryComponents() {
|
static withEntryComponents() {
|
||||||
return {
|
return {
|
||||||
ngModule: SharedModule,
|
ngModule: SharedBrowseByModule,
|
||||||
providers: ENTRY_COMPONENTS.map((component) => ({provide: component}))
|
providers: ENTRY_COMPONENTS.map((component) => ({provide: component}))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -28,14 +28,14 @@
|
|||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
<ds-search-form id="search-form"
|
<ds-themed-search-form id="search-form"
|
||||||
[query]="(searchOptions$ | async)?.query"
|
[query]="(searchOptions$ | async)?.query"
|
||||||
[scope]="(searchOptions$ | async)?.scope"
|
[scope]="(searchOptions$ | async)?.scope"
|
||||||
[currentUrl]="'./'"
|
[currentUrl]="'./'"
|
||||||
[inPlaceSearch]="true"
|
[inPlaceSearch]="true"
|
||||||
[searchPlaceholder]="'collection.edit.item-mapper.search-form.placeholder' | translate"
|
[searchPlaceholder]="'collection.edit.item-mapper.search-form.placeholder' | translate"
|
||||||
(submitSearch)="performedSearch = true">
|
(submitSearch)="performedSearch = true">
|
||||||
</ds-search-form>
|
</ds-themed-search-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -21,6 +21,7 @@ import { CollectionPageAdministratorGuard } from './collection-page-administrato
|
|||||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||||
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
||||||
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||||
|
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -34,7 +35,8 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
|||||||
path: ':id',
|
path: ':id',
|
||||||
resolve: {
|
resolve: {
|
||||||
dso: CollectionPageResolver,
|
dso: CollectionPageResolver,
|
||||||
breadcrumb: CollectionBreadcrumbResolver
|
breadcrumb: CollectionBreadcrumbResolver,
|
||||||
|
menu: DSOEditMenuResolver
|
||||||
},
|
},
|
||||||
runGuardsAndResolvers: 'always',
|
runGuardsAndResolvers: 'always',
|
||||||
children: [
|
children: [
|
||||||
@@ -91,7 +93,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
|||||||
DSOBreadcrumbsService,
|
DSOBreadcrumbsService,
|
||||||
LinkService,
|
LinkService,
|
||||||
CreateCollectionPageGuard,
|
CreateCollectionPageGuard,
|
||||||
CollectionPageAdministratorGuard
|
CollectionPageAdministratorGuard,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CollectionPageRoutingModule {
|
export class CollectionPageRoutingModule {
|
||||||
|
@@ -33,8 +33,9 @@
|
|||||||
[title]="'collection.page.news'">
|
[title]="'collection.page.news'">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
</header>
|
</header>
|
||||||
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
<div class="pl-2 space-children-mr">
|
<div class="pl-2 space-children-mr">
|
||||||
<ds-dso-page-edit-button *ngIf="isCollectionAdmin$ | async" [pageRoute]="collectionPageRoute$ | async" [dso]="collection" [tooltipMsg]="'collection.page.edit'"></ds-dso-page-edit-button>
|
<ds-dso-page-subscription-button [dso]="collection"></ds-dso-page-subscription-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section">
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit, Inject } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs';
|
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs';
|
||||||
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
|
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
@@ -29,6 +29,7 @@ import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
|||||||
import { getCollectionPageRoute } from './collection-page-routing-paths';
|
import { getCollectionPageRoute } from './collection-page-routing-paths';
|
||||||
import { redirectOn4xx } from '../core/shared/authorized.operators';
|
import { redirectOn4xx } from '../core/shared/authorized.operators';
|
||||||
import { BROWSE_LINKS_TO_FOLLOW } from '../core/browse/browse.service';
|
import { BROWSE_LINKS_TO_FOLLOW } from '../core/browse/browse.service';
|
||||||
|
import { APP_CONFIG, AppConfig } from '../../../src/config/app-config.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-collection-page',
|
selector: 'ds-collection-page',
|
||||||
@@ -69,13 +70,15 @@ export class CollectionPageComponent implements OnInit {
|
|||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private paginationService: PaginationService,
|
private paginationService: PaginationService,
|
||||||
private authorizationDataService: AuthorizationDataService,
|
private authorizationDataService: AuthorizationDataService,
|
||||||
|
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||||
) {
|
) {
|
||||||
this.paginationConfig = new PaginationComponentOptions();
|
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
||||||
this.paginationConfig.id = 'cp';
|
id: 'cp',
|
||||||
this.paginationConfig.pageSize = 5;
|
currentPage: 1,
|
||||||
this.paginationConfig.currentPage = 1;
|
pageSize: this.appConfig.browseBy.pageSize,
|
||||||
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
|
});
|
||||||
|
|
||||||
|
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@@ -16,6 +16,8 @@ import { StatisticsModule } from '../statistics/statistics.module';
|
|||||||
import { CollectionFormModule } from './collection-form/collection-form.module';
|
import { CollectionFormModule } from './collection-form/collection-form.module';
|
||||||
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
||||||
import { ComcolModule } from '../shared/comcol/comcol.module';
|
import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||||
|
import { DsoSharedModule } from '../dso-shared/dso-shared.module';
|
||||||
|
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -26,6 +28,8 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
|
|||||||
EditItemPageModule,
|
EditItemPageModule,
|
||||||
CollectionFormModule,
|
CollectionFormModule,
|
||||||
ComcolModule,
|
ComcolModule,
|
||||||
|
DsoSharedModule,
|
||||||
|
DsoPageModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CollectionPageComponent,
|
CollectionPageComponent,
|
||||||
|
@@ -6,6 +6,7 @@ import { RemoteData } from '../../../core/data/remote-data';
|
|||||||
import { Collection } from '../../../core/shared/collection.model';
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators';
|
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
import { HALLink } from '../../../core/shared/hal-link.model';
|
import { HALLink } from '../../../core/shared/hal-link.model';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for managing a collection's roles
|
* Component for managing a collection's roles
|
||||||
@@ -45,7 +46,12 @@ export class CollectionRolesComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.comcolRoles$ = this.collection$.pipe(
|
this.comcolRoles$ = this.collection$.pipe(
|
||||||
map((collection) => [
|
map((collection) => {
|
||||||
|
let workflowGroups: HALLink[] | HALLink = hasValue(collection._links.workflowGroups) ? collection._links.workflowGroups : [];
|
||||||
|
if (!Array.isArray(workflowGroups)) {
|
||||||
|
workflowGroups = [workflowGroups];
|
||||||
|
}
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
name: 'collection-admin',
|
name: 'collection-admin',
|
||||||
href: collection._links.adminGroup.href,
|
href: collection._links.adminGroup.href,
|
||||||
@@ -62,8 +68,9 @@ export class CollectionRolesComponent implements OnInit {
|
|||||||
name: 'bitstream_read',
|
name: 'bitstream_read',
|
||||||
href: collection._links.bitstreamReadGroup.href,
|
href: collection._links.bitstreamReadGroup.href,
|
||||||
},
|
},
|
||||||
...collection._links.workflowGroups,
|
...workflowGroups,
|
||||||
]),
|
];
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
|
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
|
||||||
<ng-container *ngIf="itemRD?.hasSucceeded">
|
<ng-container *ngIf="itemRD?.hasSucceeded">
|
||||||
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
|
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
|
||||||
<ds-themed-item-metadata [updateService]="itemTemplateService" [item]="itemRD?.payload"></ds-themed-item-metadata>
|
<ds-themed-dso-edit-metadata [updateDataService]="itemTemplateService" [dso]="itemRD?.payload"></ds-themed-dso-edit-metadata>
|
||||||
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ds-themed-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-themed-loading>
|
<ds-themed-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-themed-loading>
|
||||||
|
@@ -14,6 +14,7 @@ import { CommunityPageAdministratorGuard } from './community-page-administrator.
|
|||||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||||
import { ThemedCommunityPageComponent } from './themed-community-page.component';
|
import { ThemedCommunityPageComponent } from './themed-community-page.component';
|
||||||
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||||
|
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -27,7 +28,8 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
|||||||
path: ':id',
|
path: ':id',
|
||||||
resolve: {
|
resolve: {
|
||||||
dso: CommunityPageResolver,
|
dso: CommunityPageResolver,
|
||||||
breadcrumb: CommunityBreadcrumbResolver
|
breadcrumb: CommunityBreadcrumbResolver,
|
||||||
|
menu: DSOEditMenuResolver
|
||||||
},
|
},
|
||||||
runGuardsAndResolvers: 'always',
|
runGuardsAndResolvers: 'always',
|
||||||
children: [
|
children: [
|
||||||
@@ -73,7 +75,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
|||||||
DSOBreadcrumbsService,
|
DSOBreadcrumbsService,
|
||||||
LinkService,
|
LinkService,
|
||||||
CreateCommunityPageGuard,
|
CreateCommunityPageGuard,
|
||||||
CommunityPageAdministratorGuard
|
CommunityPageAdministratorGuard,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CommunityPageRoutingModule {
|
export class CommunityPageRoutingModule {
|
||||||
|
@@ -20,8 +20,9 @@
|
|||||||
[title]="'community.page.news'">
|
[title]="'community.page.news'">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
</header>
|
</header>
|
||||||
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
<div class="pl-2 space-children-mr">
|
<div class="pl-2 space-children-mr">
|
||||||
<ds-dso-page-edit-button *ngIf="isCommunityAdmin$ | async" [pageRoute]="communityPageRoute$ | async" [dso]="communityPayload" [tooltipMsg]="'community.page.edit'"></ds-dso-page-edit-button>
|
<ds-dso-page-subscription-button [dso]="communityPayload"></ds-dso-page-subscription-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section">
|
||||||
|
@@ -19,6 +19,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ThemedCollectionPageSubCollectionListComponent
|
ThemedCollectionPageSubCollectionListComponent
|
||||||
} from './sub-collection-list/themed-community-page-sub-collection-list.component';
|
} from './sub-collection-list/themed-community-page-sub-collection-list.component';
|
||||||
|
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
||||||
|
|
||||||
const DECLARATIONS = [CommunityPageComponent,
|
const DECLARATIONS = [CommunityPageComponent,
|
||||||
ThemedCommunityPageComponent,
|
ThemedCommunityPageComponent,
|
||||||
@@ -37,6 +38,7 @@ const DECLARATIONS = [CommunityPageComponent,
|
|||||||
StatisticsModule.forRoot(),
|
StatisticsModule.forRoot(),
|
||||||
CommunityFormModule,
|
CommunityFormModule,
|
||||||
ComcolModule,
|
ComcolModule,
|
||||||
|
DsoPageModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
...DECLARATIONS
|
...DECLARATIONS
|
||||||
|
@@ -11,6 +11,7 @@ import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
|||||||
import objectContaining = jasmine.objectContaining;
|
import objectContaining = jasmine.objectContaining;
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
describe(`AuthRequestService`, () => {
|
describe(`AuthRequestService`, () => {
|
||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
@@ -34,8 +35,8 @@ describe(`AuthRequestService`, () => {
|
|||||||
super(hes, rs, rdbs);
|
super(hes, rs, rdbs);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createShortLivedTokenRequest(href: string): PostRequest {
|
protected createShortLivedTokenRequest(href: string): Observable<PostRequest> {
|
||||||
return new PostRequest(this.requestService.generateRequestId(), href);
|
return observableOf(new PostRequest(this.requestService.generateRequestId(), href));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -100,14 +100,12 @@ export abstract class AuthRequestService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Factory function to create the request object to send. This needs to be a POST client side and
|
* Factory function to create the request object to send.
|
||||||
* a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow
|
|
||||||
* only the server IP to send a GET to this endpoint.
|
|
||||||
*
|
*
|
||||||
* @param href The href to send the request to
|
* @param href The href to send the request to
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
protected abstract createShortLivedTokenRequest(href: string): GetRequest | PostRequest;
|
protected abstract createShortLivedTokenRequest(href: string): Observable<PostRequest>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a request to retrieve a short-lived token which provides download access of restricted files
|
* Send a request to retrieve a short-lived token which provides download access of restricted files
|
||||||
@@ -117,7 +115,7 @@ export abstract class AuthRequestService {
|
|||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
map((href: string) => new URLCombiner(href, this.shortlivedtokensEndpoint).toString()),
|
map((href: string) => new URLCombiner(href, this.shortlivedtokensEndpoint).toString()),
|
||||||
map((endpointURL: string) => this.createShortLivedTokenRequest(endpointURL)),
|
switchMap((endpointURL: string) => this.createShortLivedTokenRequest(endpointURL)),
|
||||||
tap((request: RestRequest) => this.requestService.send(request)),
|
tap((request: RestRequest) => this.requestService.send(request)),
|
||||||
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<ShortLivedToken>(request.uuid)),
|
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<ShortLivedToken>(request.uuid)),
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { AuthRequestService } from './auth-request.service';
|
import { AuthRequestService } from './auth-request.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { BrowserAuthRequestService } from './browser-auth-request.service';
|
import { BrowserAuthRequestService } from './browser-auth-request.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { PostRequest } from '../data/request.models';
|
||||||
|
|
||||||
describe(`BrowserAuthRequestService`, () => {
|
describe(`BrowserAuthRequestService`, () => {
|
||||||
let href: string;
|
let href: string;
|
||||||
@@ -16,14 +18,20 @@ describe(`BrowserAuthRequestService`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe(`createShortLivedTokenRequest`, () => {
|
describe(`createShortLivedTokenRequest`, () => {
|
||||||
it(`should return a PostRequest`, () => {
|
it(`should return a PostRequest`, (done) => {
|
||||||
const result = (service as any).createShortLivedTokenRequest(href);
|
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||||
|
obs.subscribe((result: PostRequest) => {
|
||||||
expect(result.constructor.name).toBe('PostRequest');
|
expect(result.constructor.name).toBe('PostRequest');
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should return a request with the given href`, () => {
|
it(`should return a request with the given href`, (done) => {
|
||||||
const result = (service as any).createShortLivedTokenRequest(href);
|
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||||
expect(result.href).toBe(href) ;
|
obs.subscribe((result: PostRequest) => {
|
||||||
|
expect(result.href).toBe(href);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -4,6 +4,7 @@ import { PostRequest } from '../data/request.models';
|
|||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client side version of the service to send authentication requests
|
* Client side version of the service to send authentication requests
|
||||||
@@ -20,15 +21,13 @@ export class BrowserAuthRequestService extends AuthRequestService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory function to create the request object to send. This needs to be a POST client side and
|
* Factory function to create the request object to send.
|
||||||
* a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow
|
|
||||||
* only the server IP to send a GET to this endpoint.
|
|
||||||
*
|
*
|
||||||
* @param href The href to send the request to
|
* @param href The href to send the request to
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
protected createShortLivedTokenRequest(href: string): PostRequest {
|
protected createShortLivedTokenRequest(href: string): Observable<PostRequest> {
|
||||||
return new PostRequest(this.requestService.generateRequestId(), href);
|
return observableOf(new PostRequest(this.requestService.generateRequestId(), href));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,34 +1,68 @@
|
|||||||
import { AuthRequestService } from './auth-request.service';
|
import { AuthRequestService } from './auth-request.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { ServerAuthRequestService } from './server-auth-request.service';
|
import { ServerAuthRequestService } from './server-auth-request.service';
|
||||||
|
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { PostRequest } from '../data/request.models';
|
||||||
|
import {
|
||||||
|
XSRF_REQUEST_HEADER,
|
||||||
|
XSRF_RESPONSE_HEADER
|
||||||
|
} from '../xsrf/xsrf.interceptor';
|
||||||
|
|
||||||
describe(`ServerAuthRequestService`, () => {
|
describe(`ServerAuthRequestService`, () => {
|
||||||
let href: string;
|
let href: string;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let service: AuthRequestService;
|
let service: AuthRequestService;
|
||||||
|
let httpClient: HttpClient;
|
||||||
|
let httpResponse: HttpResponse<any>;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
const mockToken = 'mock-token';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
href = 'https://rest.api/auth/shortlivedtokens';
|
href = 'https://rest.api/auth/shortlivedtokens';
|
||||||
requestService = jasmine.createSpyObj('requestService', {
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2'
|
'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2'
|
||||||
});
|
});
|
||||||
service = new ServerAuthRequestService(null, requestService, null);
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.set(XSRF_RESPONSE_HEADER, mockToken);
|
||||||
|
httpResponse = {
|
||||||
|
body: { bar: false },
|
||||||
|
headers: headers,
|
||||||
|
statusText: '200'
|
||||||
|
} as HttpResponse<any>;
|
||||||
|
httpClient = jasmine.createSpyObj('httpClient', {
|
||||||
|
get: observableOf(httpResponse),
|
||||||
|
});
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
'getRootHref': '/api'
|
||||||
|
});
|
||||||
|
service = new ServerAuthRequestService(halService, requestService, null, httpClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`createShortLivedTokenRequest`, () => {
|
describe(`createShortLivedTokenRequest`, () => {
|
||||||
it(`should return a GetRequest`, () => {
|
it(`should return a PostRequest`, (done) => {
|
||||||
const result = (service as any).createShortLivedTokenRequest(href);
|
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||||
expect(result.constructor.name).toBe('GetRequest');
|
obs.subscribe((result: PostRequest) => {
|
||||||
|
expect(result.constructor.name).toBe('PostRequest');
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should return a request with the given href`, () => {
|
it(`should return a request with the given href`, (done) => {
|
||||||
const result = (service as any).createShortLivedTokenRequest(href);
|
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||||
expect(result.href).toBe(href) ;
|
obs.subscribe((result: PostRequest) => {
|
||||||
|
expect(result.href).toBe(href);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should have a responseMsToLive of 2 seconds`, () => {
|
it(`should return a request with a xsrf header`, (done) => {
|
||||||
const result = (service as any).createShortLivedTokenRequest(href);
|
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||||
expect(result.responseMsToLive).toBe(2 * 1000) ;
|
obs.subscribe((result: PostRequest) => {
|
||||||
|
expect(result.options.headers.get(XSRF_REQUEST_HEADER)).toBe(mockToken);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,9 +1,21 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { AuthRequestService } from './auth-request.service';
|
import { AuthRequestService } from './auth-request.service';
|
||||||
import { GetRequest } from '../data/request.models';
|
import { PostRequest } from '../data/request.models';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import {
|
||||||
|
HttpHeaders,
|
||||||
|
HttpClient,
|
||||||
|
HttpResponse
|
||||||
|
} from '@angular/common/http';
|
||||||
|
import {
|
||||||
|
XSRF_REQUEST_HEADER,
|
||||||
|
XSRF_RESPONSE_HEADER,
|
||||||
|
DSPACE_XSRF_COOKIE
|
||||||
|
} from '../xsrf/xsrf.interceptor';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server side version of the service to send authentication requests
|
* Server side version of the service to send authentication requests
|
||||||
@@ -14,23 +26,42 @@ export class ServerAuthRequestService extends AuthRequestService {
|
|||||||
constructor(
|
constructor(
|
||||||
halService: HALEndpointService,
|
halService: HALEndpointService,
|
||||||
requestService: RequestService,
|
requestService: RequestService,
|
||||||
rdbService: RemoteDataBuildService
|
rdbService: RemoteDataBuildService,
|
||||||
|
protected httpClient: HttpClient,
|
||||||
) {
|
) {
|
||||||
super(halService, requestService, rdbService);
|
super(halService, requestService, rdbService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory function to create the request object to send. This needs to be a POST client side and
|
* Factory function to create the request object to send.
|
||||||
* a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow
|
|
||||||
* only the server IP to send a GET to this endpoint.
|
|
||||||
*
|
*
|
||||||
* @param href The href to send the request to
|
* @param href The href to send the request to
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
protected createShortLivedTokenRequest(href: string): GetRequest {
|
protected createShortLivedTokenRequest(href: string): Observable<PostRequest> {
|
||||||
return Object.assign(new GetRequest(this.requestService.generateRequestId(), href), {
|
// First do a call to the root endpoint in order to get an XSRF token
|
||||||
responseMsToLive: 2 * 1000 // A short lived token is only valid for 2 seconds.
|
return this.httpClient.get(this.halService.getRootHref(), { observe: 'response' }).pipe(
|
||||||
});
|
// retrieve the XSRF token from the response header
|
||||||
|
map((response: HttpResponse<any>) => response.headers.get(XSRF_RESPONSE_HEADER)),
|
||||||
|
// Use that token to create an HttpHeaders object
|
||||||
|
map((xsrfToken: string) => new HttpHeaders()
|
||||||
|
.set('Content-Type', 'application/json; charset=utf-8')
|
||||||
|
// set the token as the XSRF header
|
||||||
|
.set(XSRF_REQUEST_HEADER, xsrfToken)
|
||||||
|
// and as the DSPACE-XSRF-COOKIE
|
||||||
|
.set('Cookie', `${DSPACE_XSRF_COOKIE}=${xsrfToken}`)),
|
||||||
|
map((headers: HttpHeaders) =>
|
||||||
|
// Create a new PostRequest using those headers and the given href
|
||||||
|
new PostRequest(
|
||||||
|
this.requestService.generateRequestId(),
|
||||||
|
href,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: headers,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -39,7 +39,7 @@ export class DSOBreadcrumbsService implements BreadcrumbsProviderService<ChildHA
|
|||||||
return this.linkService.resolveLink(key, followLink(propertyName))[propertyName].pipe(
|
return this.linkService.resolveLink(key, followLink(propertyName))[propertyName].pipe(
|
||||||
find((parentRD: RemoteData<ChildHALResource & DSpaceObject>) => parentRD.hasSucceeded || parentRD.statusCode === 204),
|
find((parentRD: RemoteData<ChildHALResource & DSpaceObject>) => parentRD.hasSucceeded || parentRD.statusCode === 204),
|
||||||
switchMap((parentRD: RemoteData<ChildHALResource & DSpaceObject>) => {
|
switchMap((parentRD: RemoteData<ChildHALResource & DSpaceObject>) => {
|
||||||
if (hasValue(parentRD.payload)) {
|
if (hasValue(parentRD) && hasValue(parentRD.payload)) {
|
||||||
const parent = parentRD.payload;
|
const parent = parentRD.payload;
|
||||||
return this.getBreadcrumbs(parent, getDSORoute(parent));
|
return this.getBreadcrumbs(parent, getDSORoute(parent));
|
||||||
}
|
}
|
||||||
|
@@ -2,27 +2,66 @@ import { BrowseDefinitionDataService } from './browse-definition-data.service';
|
|||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
import { EMPTY } from 'rxjs';
|
import { EMPTY } from 'rxjs';
|
||||||
import { FindListOptions } from '../data/find-list-options.model';
|
import { FindListOptions } from '../data/find-list-options.model';
|
||||||
|
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock';
|
||||||
|
|
||||||
describe(`BrowseDefinitionDataService`, () => {
|
describe(`BrowseDefinitionDataService`, () => {
|
||||||
|
let requestService: RequestService;
|
||||||
let service: BrowseDefinitionDataService;
|
let service: BrowseDefinitionDataService;
|
||||||
const findAllDataSpy = jasmine.createSpyObj('findAllData', {
|
let findAllDataSpy;
|
||||||
findAll: EMPTY,
|
let searchDataSpy;
|
||||||
});
|
const browsesEndpointURL = 'https://rest.api/browses';
|
||||||
|
const halService: any = new HALEndpointServiceStub(browsesEndpointURL);
|
||||||
|
|
||||||
const options = new FindListOptions();
|
const options = new FindListOptions();
|
||||||
const linksToFollow = [
|
const linksToFollow = [
|
||||||
followLink('entries'),
|
followLink('entries'),
|
||||||
followLink('items')
|
followLink('items')
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function initTestService() {
|
||||||
|
return new BrowseDefinitionDataService(
|
||||||
|
requestService,
|
||||||
|
getMockRemoteDataBuildService(),
|
||||||
|
getMockObjectCacheService(),
|
||||||
|
halService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service = new BrowseDefinitionDataService(null, null, null, null);
|
service = initTestService();
|
||||||
|
findAllDataSpy = jasmine.createSpyObj('findAllData', {
|
||||||
|
findAll: EMPTY,
|
||||||
|
});
|
||||||
|
searchDataSpy = jasmine.createSpyObj('searchData', {
|
||||||
|
searchBy: EMPTY,
|
||||||
|
getSearchByHref: EMPTY,
|
||||||
|
});
|
||||||
(service as any).findAllData = findAllDataSpy;
|
(service as any).findAllData = findAllDataSpy;
|
||||||
|
(service as any).searchData = searchDataSpy;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('findByFields', () => {
|
||||||
|
it(`should call searchByHref on searchData`, () => {
|
||||||
|
service.findByFields(['test'], true, false, ...linksToFollow);
|
||||||
|
expect(searchDataSpy.getSearchByHref).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('searchBy', () => {
|
||||||
|
it(`should call searchBy on searchData`, () => {
|
||||||
|
service.searchBy('test', options, true, false, ...linksToFollow);
|
||||||
|
expect(searchDataSpy.searchBy).toHaveBeenCalledWith('test', options, true, false, ...linksToFollow);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe(`findAll`, () => {
|
describe(`findAll`, () => {
|
||||||
it(`should call findAll on findAllData`, () => {
|
it(`should call findAll on findAllData`, () => {
|
||||||
service.findAll(options, true, false, ...linksToFollow);
|
service.findAll(options, true, false, ...linksToFollow);
|
||||||
expect(findAllDataSpy.findAll).toHaveBeenCalledWith(options, true, false, ...linksToFollow);
|
expect(findAllDataSpy.findAll).toHaveBeenCalledWith(options, true, false, ...linksToFollow);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -13,6 +13,8 @@ import { FindListOptions } from '../data/find-list-options.model';
|
|||||||
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
|
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
|
||||||
import { FindAllData, FindAllDataImpl } from '../data/base/find-all-data';
|
import { FindAllData, FindAllDataImpl } from '../data/base/find-all-data';
|
||||||
import { dataService } from '../data/base/data-service.decorator';
|
import { dataService } from '../data/base/data-service.decorator';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { SearchData, SearchDataImpl } from '../data/base/search-data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data service responsible for retrieving browse definitions from the REST server
|
* Data service responsible for retrieving browse definitions from the REST server
|
||||||
@@ -21,8 +23,9 @@ import { dataService } from '../data/base/data-service.decorator';
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
@dataService(BROWSE_DEFINITION)
|
@dataService(BROWSE_DEFINITION)
|
||||||
export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseDefinition> implements FindAllData<BrowseDefinition> {
|
export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseDefinition> implements FindAllData<BrowseDefinition>, SearchData<BrowseDefinition> {
|
||||||
private findAllData: FindAllDataImpl<BrowseDefinition>;
|
private findAllData: FindAllDataImpl<BrowseDefinition>;
|
||||||
|
private searchData: SearchDataImpl<BrowseDefinition>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -31,7 +34,7 @@ export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseD
|
|||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
) {
|
) {
|
||||||
super('browses', requestService, rdbService, objectCache, halService);
|
super('browses', requestService, rdbService, objectCache, halService);
|
||||||
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,5 +55,71 @@ export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseD
|
|||||||
findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
|
findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
|
||||||
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a new FindListRequest with given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||||
|
* Return an observable that emits response from the server
|
||||||
|
*/
|
||||||
|
public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
|
||||||
|
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the HREF for a specific object's search method with given options object
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @return {Observable<string>}
|
||||||
|
* Return an observable that emits created HREF
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
public getSearchByHref(searchMethod: string, options?: FindListOptions, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<string> {
|
||||||
|
return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the browse URL by providing a list of metadata keys. The first matching browse index definition
|
||||||
|
* for any of the fields is returned. This is used in eg. item page field component, which can be configured
|
||||||
|
* with several fields for a component like 'Author', and needs to know if and how to link the values
|
||||||
|
* to configured browse indices.
|
||||||
|
*
|
||||||
|
* @param fields an array of field strings, eg. ['dc.contributor.author', 'dc.creator']
|
||||||
|
* @param useCachedVersionIfAvailable Override the data service useCachedVersionIfAvailable parameter (default: true)
|
||||||
|
* @param reRequestOnStale Override the data service reRequestOnStale parameter (default: true)
|
||||||
|
* @param linksToFollow Override the data service linksToFollow parameter (default: empty array)
|
||||||
|
*/
|
||||||
|
findByFields(
|
||||||
|
fields: string[],
|
||||||
|
useCachedVersionIfAvailable = true,
|
||||||
|
reRequestOnStale = true,
|
||||||
|
...linksToFollow: FollowLinkConfig<BrowseDefinition>[]
|
||||||
|
): Observable<RemoteData<BrowseDefinition>> {
|
||||||
|
const searchParams = [];
|
||||||
|
searchParams.push(new RequestParam('fields', fields));
|
||||||
|
|
||||||
|
const hrefObs = this.getSearchByHref(
|
||||||
|
'byFields',
|
||||||
|
{ searchParams },
|
||||||
|
...linksToFollow
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.findByHref(
|
||||||
|
hrefObs,
|
||||||
|
useCachedVersionIfAvailable,
|
||||||
|
reRequestOnStale,
|
||||||
|
...linksToFollow,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -19,9 +19,9 @@ import {
|
|||||||
} from '../shared/operators';
|
} from '../shared/operators';
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
||||||
import { BrowseDefinitionDataService } from './browse-definition-data.service';
|
|
||||||
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
||||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { BrowseDefinitionDataService } from './browse-definition-data.service';
|
||||||
|
|
||||||
|
|
||||||
export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig<BrowseEntry | Item>[] = [
|
export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig<BrowseEntry | Item>[] = [
|
||||||
@@ -35,7 +35,7 @@ export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig<BrowseEntry | Item>[] = [
|
|||||||
export class BrowseService {
|
export class BrowseService {
|
||||||
protected linkPath = 'browses';
|
protected linkPath = 'browses';
|
||||||
|
|
||||||
private static toSearchKeyArray(metadataKey: string): string[] {
|
public static toSearchKeyArray(metadataKey: string): string[] {
|
||||||
const keyParts = metadataKey.split('.');
|
const keyParts = metadataKey.split('.');
|
||||||
const searchFor = [];
|
const searchFor = [];
|
||||||
searchFor.push('*');
|
searchFor.push('*');
|
||||||
|
@@ -7,7 +7,6 @@ import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
|
|||||||
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
|
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
|
||||||
import { RouteEffects } from './services/route.effects';
|
import { RouteEffects } from './services/route.effects';
|
||||||
import { RouterEffects } from './router/router.effects';
|
import { RouterEffects } from './router/router.effects';
|
||||||
import { MenuEffects } from '../shared/menu/menu.effects';
|
|
||||||
|
|
||||||
export const coreEffects = [
|
export const coreEffects = [
|
||||||
RequestEffects,
|
RequestEffects,
|
||||||
@@ -19,5 +18,4 @@ export const coreEffects = [
|
|||||||
ObjectUpdatesEffects,
|
ObjectUpdatesEffects,
|
||||||
RouteEffects,
|
RouteEffects,
|
||||||
RouterEffects,
|
RouterEffects,
|
||||||
MenuEffects
|
|
||||||
];
|
];
|
||||||
|
@@ -157,6 +157,9 @@ import { SequenceService } from './shared/sequence.service';
|
|||||||
import { CoreState } from './core-state.model';
|
import { CoreState } from './core-state.model';
|
||||||
import { GroupDataService } from './eperson/group-data.service';
|
import { GroupDataService } from './eperson/group-data.service';
|
||||||
import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model';
|
import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model';
|
||||||
|
import { RatingAdvancedWorkflowInfo } from './tasks/models/rating-advanced-workflow-info.model';
|
||||||
|
import { AdvancedWorkflowInfo } from './tasks/models/advanced-workflow-info.model';
|
||||||
|
import { SelectReviewerAdvancedWorkflowInfo } from './tasks/models/select-reviewer-advanced-workflow-info.model';
|
||||||
import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model';
|
import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model';
|
||||||
import { AccessStatusDataService } from './data/access-status-data.service';
|
import { AccessStatusDataService } from './data/access-status-data.service';
|
||||||
import { LinkHeadService } from './services/link-head.service';
|
import { LinkHeadService } from './services/link-head.service';
|
||||||
@@ -170,6 +173,9 @@ import { OrcidHistory } from './orcid/model/orcid-history.model';
|
|||||||
import { OrcidAuthService } from './orcid/orcid-auth.service';
|
import { OrcidAuthService } from './orcid/orcid-auth.service';
|
||||||
import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service';
|
import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service';
|
||||||
import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service';
|
import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service';
|
||||||
|
import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model';
|
||||||
|
import { Subscription } from '../shared/subscriptions/models/subscription.model';
|
||||||
|
import { SupervisionOrderDataService } from './supervision-order/supervision-order-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
|
||||||
@@ -292,6 +298,7 @@ const PROVIDERS = [
|
|||||||
OrcidAuthService,
|
OrcidAuthService,
|
||||||
OrcidQueueDataService,
|
OrcidQueueDataService,
|
||||||
OrcidHistoryDataService,
|
OrcidHistoryDataService,
|
||||||
|
SupervisionOrderDataService
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -338,6 +345,9 @@ export const models =
|
|||||||
Version,
|
Version,
|
||||||
VersionHistory,
|
VersionHistory,
|
||||||
WorkflowAction,
|
WorkflowAction,
|
||||||
|
AdvancedWorkflowInfo,
|
||||||
|
RatingAdvancedWorkflowInfo,
|
||||||
|
SelectReviewerAdvancedWorkflowInfo,
|
||||||
TemplateItem,
|
TemplateItem,
|
||||||
Feature,
|
Feature,
|
||||||
Authorization,
|
Authorization,
|
||||||
@@ -356,7 +366,9 @@ export const models =
|
|||||||
ResearcherProfile,
|
ResearcherProfile,
|
||||||
OrcidQueue,
|
OrcidQueue,
|
||||||
OrcidHistory,
|
OrcidHistory,
|
||||||
AccessStatusObject
|
AccessStatusObject,
|
||||||
|
IdentifierData,
|
||||||
|
Subscription,
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -41,21 +41,28 @@ describe('ArrayMoveChangeAnalyzer', () => {
|
|||||||
], new MoveTest(0, 3));
|
], new MoveTest(0, 3));
|
||||||
|
|
||||||
testMove([
|
testMove([
|
||||||
|
{ op: 'move', from: '/2', path: '/3' },
|
||||||
{ op: 'move', from: '/0', path: '/3' },
|
{ op: 'move', from: '/0', path: '/3' },
|
||||||
{ op: 'move', from: '/2', path: '/1' }
|
|
||||||
], new MoveTest(0, 3), new MoveTest(1, 2));
|
], new MoveTest(0, 3), new MoveTest(1, 2));
|
||||||
|
|
||||||
testMove([
|
testMove([
|
||||||
|
{ op: 'move', from: '/3', path: '/4' },
|
||||||
{ op: 'move', from: '/0', path: '/1' },
|
{ op: 'move', from: '/0', path: '/1' },
|
||||||
{ op: 'move', from: '/3', path: '/4' }
|
|
||||||
], new MoveTest(0, 1), new MoveTest(3, 4));
|
], new MoveTest(0, 1), new MoveTest(3, 4));
|
||||||
|
|
||||||
testMove([], new MoveTest(0, 4), new MoveTest(4, 0));
|
testMove([], new MoveTest(0, 4), new MoveTest(4, 0));
|
||||||
|
|
||||||
testMove([
|
testMove([
|
||||||
|
{ op: 'move', from: '/2', path: '/3' },
|
||||||
{ op: 'move', from: '/0', path: '/3' },
|
{ op: 'move', from: '/0', path: '/3' },
|
||||||
{ op: 'move', from: '/2', path: '/1' }
|
|
||||||
], new MoveTest(0, 4), new MoveTest(1, 3), new MoveTest(2, 4));
|
], new MoveTest(0, 4), new MoveTest(1, 3), new MoveTest(2, 4));
|
||||||
|
|
||||||
|
testMove([
|
||||||
|
{ op: 'move', from: '/3', path: '/4' },
|
||||||
|
{ op: 'move', from: '/2', path: '/4' },
|
||||||
|
{ op: 'move', from: '/1', path: '/3' },
|
||||||
|
{ op: 'move', from: '/0', path: '/3' },
|
||||||
|
], new MoveTest(4, 1), new MoveTest(4, 2), new MoveTest(0, 3));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when some values are undefined (index 2 and 3)', () => {
|
describe('when some values are undefined (index 2 and 3)', () => {
|
||||||
|
@@ -16,22 +16,31 @@ export class ArrayMoveChangeAnalyzer<T> {
|
|||||||
* @param array2 The custom array to compare with the original
|
* @param array2 The custom array to compare with the original
|
||||||
*/
|
*/
|
||||||
diff(array1: T[], array2: T[]): MoveOperation[] {
|
diff(array1: T[], array2: T[]): MoveOperation[] {
|
||||||
const result = [];
|
return this.getMoves(array1, array2).map((move) => Object.assign({
|
||||||
const moved = [...array1];
|
|
||||||
array1.forEach((value: T, index: number) => {
|
|
||||||
if (hasValue(value)) {
|
|
||||||
const otherIndex = array2.indexOf(value);
|
|
||||||
const movedIndex = moved.indexOf(value);
|
|
||||||
if (index !== otherIndex && movedIndex !== otherIndex) {
|
|
||||||
moveItemInArray(moved, movedIndex, otherIndex);
|
|
||||||
result.push(Object.assign({
|
|
||||||
op: 'move',
|
op: 'move',
|
||||||
from: '/' + movedIndex,
|
from: '/' + move[0],
|
||||||
path: '/' + otherIndex
|
path: '/' + move[1],
|
||||||
}) as MoveOperation);
|
}) as MoveOperation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine a set of moves required to transform array1 into array2
|
||||||
|
* The moves are returned as an array of pairs of numbers where the first number is the original index and the second
|
||||||
|
* is the new index
|
||||||
|
* It is assumed the operations are executed in the order they're returned (and not simultaneously)
|
||||||
|
* @param array1
|
||||||
|
* @param array2
|
||||||
|
*/
|
||||||
|
private getMoves(array1: any[], array2: any[]): number[][] {
|
||||||
|
const moved = [...array2];
|
||||||
|
|
||||||
|
return array1.reduce((moves, item, index) => {
|
||||||
|
if (hasValue(item) && item !== moved[index]) {
|
||||||
|
const last = moved.lastIndexOf(item);
|
||||||
|
moveItemInArray(moved, last, index);
|
||||||
|
moves.unshift([index, last]);
|
||||||
}
|
}
|
||||||
});
|
return moves;
|
||||||
return result;
|
}, []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ import { RemoteData } from './remote-data';
|
|||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
import { HttpParams } from '@angular/common/http';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -55,7 +56,7 @@ export class EpersonRegistrationService {
|
|||||||
* @param email
|
* @param email
|
||||||
* @param captchaToken the value of x-recaptcha-token header
|
* @param captchaToken the value of x-recaptcha-token header
|
||||||
*/
|
*/
|
||||||
registerEmail(email: string, captchaToken: string = null): Observable<RemoteData<Registration>> {
|
registerEmail(email: string, captchaToken: string = null, type?: string): Observable<RemoteData<Registration>> {
|
||||||
const registration = new Registration();
|
const registration = new Registration();
|
||||||
registration.email = email;
|
registration.email = email;
|
||||||
|
|
||||||
@@ -70,6 +71,11 @@ export class EpersonRegistrationService {
|
|||||||
}
|
}
|
||||||
options.headers = headers;
|
options.headers = headers;
|
||||||
|
|
||||||
|
if (hasValue(type)) {
|
||||||
|
options.params = type ?
|
||||||
|
new HttpParams({ fromString: 'accountRequestType=' + type }) : new HttpParams();
|
||||||
|
}
|
||||||
|
|
||||||
href$.pipe(
|
href$.pipe(
|
||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href)),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
|
@@ -29,5 +29,9 @@ export enum FeatureID {
|
|||||||
CanViewUsageStatistics = 'canViewUsageStatistics',
|
CanViewUsageStatistics = 'canViewUsageStatistics',
|
||||||
CanSendFeedback = 'canSendFeedback',
|
CanSendFeedback = 'canSendFeedback',
|
||||||
CanClaimItem = 'canClaimItem',
|
CanClaimItem = 'canClaimItem',
|
||||||
CanSynchronizeWithORCID = 'canSynchronizeWithORCID'
|
CanSynchronizeWithORCID = 'canSynchronizeWithORCID',
|
||||||
|
CanSubmit = 'canSubmit',
|
||||||
|
CanEditItem = 'canEditItem',
|
||||||
|
CanRegisterDOI = 'canRegisterDOI',
|
||||||
|
CanSubscribe = 'canSubscribeDso',
|
||||||
}
|
}
|
||||||
|
85
src/app/core/data/identifier-data.service.ts
Normal file
85
src/app/core/data/identifier-data.service.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { dataService } from './base/data-service.decorator';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { BaseDataService } from './base/base-data.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
|
import { CoreState } from '../core-state.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type';
|
||||||
|
import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import {ConfigurationProperty} from '../shared/configuration-property.model';
|
||||||
|
import {ConfigurationDataService} from './configuration-data.service';
|
||||||
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
import { PostRequest } from './request.models';
|
||||||
|
import { sendRequest } from '../shared/request.operators';
|
||||||
|
import { RestRequest } from './rest-request.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service handling all REST requests to get item identifiers like handles and DOIs
|
||||||
|
* from the /identifiers endpoint, as well as the backend configuration that controls whether a 'Register DOI'
|
||||||
|
* button appears for admins in the item status page
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@dataService(IDENTIFIERS)
|
||||||
|
export class IdentifierDataService extends BaseDataService<IdentifierData> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected comparator: DefaultChangeAnalyzer<IdentifierData>,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
private configurationService: ConfigurationDataService,
|
||||||
|
) {
|
||||||
|
super('identifiers', requestService, rdbService, objectCache, halService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link RemoteData} of {@link IdentifierData} representing identifiers for this item
|
||||||
|
* @param item Item we are querying
|
||||||
|
*/
|
||||||
|
getIdentifierDataFor(item: Item): Observable<RemoteData<IdentifierData>> {
|
||||||
|
return this.findByHref(item._links.identifiers.href, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should we allow registration of new DOIs via the item status page?
|
||||||
|
*/
|
||||||
|
public getIdentifierRegistrationConfiguration(): Observable<string[]> {
|
||||||
|
return this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((propertyRD: RemoteData<ConfigurationProperty>) => propertyRD.hasSucceeded ? propertyRD.payload.values : [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerIdentifier(item: Item, type: string): Observable<RemoteData<any>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
return this.getEndpoint().pipe(
|
||||||
|
map((endpointURL: string) => {
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
|
options.headers = headers;
|
||||||
|
let params = new HttpParams();
|
||||||
|
params = params.append('type', type);
|
||||||
|
options.params = params;
|
||||||
|
return new PostRequest(requestId, endpointURL, item._links.self.href, options);
|
||||||
|
}),
|
||||||
|
sendRequest(this.requestService),
|
||||||
|
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid) as Observable<RemoteData<any>>)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -232,6 +232,16 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
|
|||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint for an item's identifiers
|
||||||
|
* @param itemId
|
||||||
|
*/
|
||||||
|
public getIdentifiersEndpoint(itemId: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
switchMap((url: string) => this.halService.getEndpoint('identifiers', `${url}/${itemId}`))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the endpoint to move the item
|
* Get the endpoint to move the item
|
||||||
* @param itemId
|
* @param itemId
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user