mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'main' into 2071
This commit is contained in:
@@ -32,12 +32,60 @@ cache:
|
||||
# NOTE: how long should objects be cached for by default
|
||||
msToLive:
|
||||
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:
|
||||
defaultTime: 0
|
||||
maxBufferSize: 100
|
||||
timePerMethod:
|
||||
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
|
||||
auth:
|
||||
|
@@ -99,6 +99,7 @@
|
||||
"fast-json-patch": "^3.0.0-1",
|
||||
"filesize": "^6.1.0",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"isbot": "^3.6.5",
|
||||
"js-cookie": "2.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json5": "^2.2.2",
|
||||
@@ -106,6 +107,7 @@
|
||||
"jwt-decode": "^3.1.2",
|
||||
"klaro": "^0.7.18",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^7.14.1",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-mathjax3": "^4.3.1",
|
||||
"mirador": "^3.3.0",
|
||||
|
250
server.ts
250
server.ts
@@ -28,6 +28,8 @@ import * as expressStaticGzip from 'express-static-gzip';
|
||||
/* eslint-enable import/no-namespace */
|
||||
|
||||
import axios from 'axios';
|
||||
import LRU from 'lru-cache';
|
||||
import isbot from 'isbot';
|
||||
import { createCertificate } from 'pem';
|
||||
import { createServer } from 'https';
|
||||
import { json } from 'body-parser';
|
||||
@@ -53,6 +55,8 @@ import { buildAppConfig } from './src/config/config.server';
|
||||
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
|
||||
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
||||
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
|
||||
@@ -67,6 +71,12 @@ const cookieParser = require('cookie-parser');
|
||||
|
||||
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
|
||||
extendEnvironmentWithAppConfig(environment, appConfig);
|
||||
|
||||
@@ -87,10 +97,12 @@ export function app() {
|
||||
/*
|
||||
* If production mode is enabled in the environment file:
|
||||
* - 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)
|
||||
*/
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
initCache();
|
||||
server.use(compression({
|
||||
// only compress responses we've marked as SSR
|
||||
// otherwise, this middleware may compress files we've chosen not to compress via compression-webpack-plugin
|
||||
@@ -106,13 +118,13 @@ export function app() {
|
||||
|
||||
/*
|
||||
* Add cookie parser middleware
|
||||
* See [morgan](https://github.com/expressjs/cookie-parser)
|
||||
* See [cookie-parser](https://github.com/expressjs/cookie-parser)
|
||||
*/
|
||||
server.use(cookieParser());
|
||||
|
||||
/*
|
||||
* Add parser for request bodies
|
||||
* See [morgan](https://github.com/expressjs/body-parser)
|
||||
* Add JSON parser for request bodies
|
||||
* See [body-parser](https://github.com/expressjs/body-parser)
|
||||
*/
|
||||
server.use(json());
|
||||
|
||||
@@ -186,7 +198,7 @@ export function app() {
|
||||
* Serve static resources (images, i18n messages, …)
|
||||
* 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,
|
||||
enableBrotli: true,
|
||||
orderPreference: ['br', 'gzip'],
|
||||
@@ -202,8 +214,11 @@ export function app() {
|
||||
*/
|
||||
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);
|
||||
|
||||
@@ -215,6 +230,25 @@ export function app() {
|
||||
*/
|
||||
function ngApp(req, res) {
|
||||
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, {
|
||||
req,
|
||||
res,
|
||||
@@ -227,18 +261,37 @@ function ngApp(req, res) {
|
||||
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
|
||||
}, (err, 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);
|
||||
}
|
||||
} 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
|
||||
// sent. These errors occur for various reasons in universal, not all of which are in our
|
||||
// control to solve.
|
||||
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
|
||||
} else {
|
||||
console.warn('Error in SSR, serving for direct CSR.');
|
||||
console.warn('Error in server-side rendering (SSR)');
|
||||
if (hasValue(err)) {
|
||||
console.warn('Error details : ', err);
|
||||
}
|
||||
if (sendToUser) {
|
||||
console.warn('Falling back to serving direct client-side rendering (CSR).');
|
||||
clientSideRender(req, res);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send back response to user to trigger direct client-side rendering (CSR)
|
||||
* @param req current request
|
||||
* @param res current response
|
||||
*/
|
||||
function clientSideRender(req, res) {
|
||||
res.render(indexHtml, {
|
||||
req,
|
||||
providers: [{
|
||||
@@ -247,29 +300,180 @@ function ngApp(req, res) {
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* 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
|
||||
});
|
||||
} else {
|
||||
// If preboot is disabled, just serve the client
|
||||
console.log('Universal off, serving for direct CSR');
|
||||
res.render(indexHtml, {
|
||||
req,
|
||||
providers: [{
|
||||
provide: APP_BASE_HREF,
|
||||
useValue: req.baseUrl
|
||||
}]
|
||||
}
|
||||
|
||||
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
|
||||
* The cache control value can be configured in the environments file and defaults to max-age=60
|
||||
/**
|
||||
* Return whether bot-specific server side caching is enabled in configuration.
|
||||
*/
|
||||
function cacheControl(req, res, next) {
|
||||
// instruct browser to revalidate
|
||||
res.header('Cache-Control', environment.cache.control || 'max-age=60');
|
||||
function botCacheEnabled(): 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.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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
|
||||
/*
|
||||
* Callback function for when the server has started
|
||||
|
@@ -11,6 +11,7 @@ import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import objectContaining = jasmine.objectContaining;
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
describe(`AuthRequestService`, () => {
|
||||
let halService: HALEndpointService;
|
||||
@@ -34,8 +35,8 @@ describe(`AuthRequestService`, () => {
|
||||
super(hes, rs, rdbs);
|
||||
}
|
||||
|
||||
protected createShortLivedTokenRequest(href: string): PostRequest {
|
||||
return new PostRequest(this.requestService.generateRequestId(), href);
|
||||
protected createShortLivedTokenRequest(href: string): Observable<PostRequest> {
|
||||
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
|
||||
* 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.
|
||||
* Factory function to create the request object to send.
|
||||
*
|
||||
* @param href The href to send the request to
|
||||
* @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
|
||||
@@ -117,7 +115,7 @@ export abstract class AuthRequestService {
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
distinctUntilChanged(),
|
||||
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)),
|
||||
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<ShortLivedToken>(request.uuid)),
|
||||
getFirstCompletedRemoteData(),
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { BrowserAuthRequestService } from './browser-auth-request.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PostRequest } from '../data/request.models';
|
||||
|
||||
describe(`BrowserAuthRequestService`, () => {
|
||||
let href: string;
|
||||
@@ -16,14 +18,20 @@ describe(`BrowserAuthRequestService`, () => {
|
||||
});
|
||||
|
||||
describe(`createShortLivedTokenRequest`, () => {
|
||||
it(`should return a PostRequest`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
it(`should return a PostRequest`, (done) => {
|
||||
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||
obs.subscribe((result: PostRequest) => {
|
||||
expect(result.constructor.name).toBe('PostRequest');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return a request with the given href`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
it(`should return a request with the given href`, (done) => {
|
||||
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||
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 { RequestService } from '../data/request.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
|
||||
@@ -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
|
||||
* 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.
|
||||
* Factory function to create the request object to send.
|
||||
*
|
||||
* @param href The href to send the request to
|
||||
* @protected
|
||||
*/
|
||||
protected createShortLivedTokenRequest(href: string): PostRequest {
|
||||
return new PostRequest(this.requestService.generateRequestId(), href);
|
||||
protected createShortLivedTokenRequest(href: string): Observable<PostRequest> {
|
||||
return observableOf(new PostRequest(this.requestService.generateRequestId(), href));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,34 +1,68 @@
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { RequestService } from '../data/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`, () => {
|
||||
let href: string;
|
||||
let requestService: RequestService;
|
||||
let service: AuthRequestService;
|
||||
let httpClient: HttpClient;
|
||||
let httpResponse: HttpResponse<any>;
|
||||
let halService: HALEndpointService;
|
||||
const mockToken = 'mock-token';
|
||||
|
||||
beforeEach(() => {
|
||||
href = 'https://rest.api/auth/shortlivedtokens';
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'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`, () => {
|
||||
it(`should return a GetRequest`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.constructor.name).toBe('GetRequest');
|
||||
it(`should return a PostRequest`, (done) => {
|
||||
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||
obs.subscribe((result: PostRequest) => {
|
||||
expect(result.constructor.name).toBe('PostRequest');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return a request with the given href`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
it(`should return a request with the given href`, (done) => {
|
||||
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||
obs.subscribe((result: PostRequest) => {
|
||||
expect(result.href).toBe(href);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should have a responseMsToLive of 2 seconds`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.responseMsToLive).toBe(2 * 1000) ;
|
||||
it(`should return a request with a xsrf header`, (done) => {
|
||||
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
|
||||
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 { 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 { RequestService } from '../data/request.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
|
||||
@@ -14,23 +26,42 @@ export class ServerAuthRequestService extends AuthRequestService {
|
||||
constructor(
|
||||
halService: HALEndpointService,
|
||||
requestService: RequestService,
|
||||
rdbService: RemoteDataBuildService
|
||||
rdbService: RemoteDataBuildService,
|
||||
protected httpClient: HttpClient,
|
||||
) {
|
||||
super(halService, requestService, rdbService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create the request object to send. This needs to be a POST client side and
|
||||
* 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.
|
||||
* Factory function to create the request object to send.
|
||||
*
|
||||
* @param href The href to send the request to
|
||||
* @protected
|
||||
*/
|
||||
protected createShortLivedTokenRequest(href: string): GetRequest {
|
||||
return Object.assign(new GetRequest(this.requestService.generateRequestId(), href), {
|
||||
responseMsToLive: 2 * 1000 // A short lived token is only valid for 2 seconds.
|
||||
});
|
||||
protected createShortLivedTokenRequest(href: string): Observable<PostRequest> {
|
||||
// First do a call to the root endpoint in order to get an XSRF token
|
||||
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,
|
||||
},
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -2,27 +2,66 @@ import { BrowseDefinitionDataService } from './browse-definition-data.service';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
import { EMPTY } from 'rxjs';
|
||||
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`, () => {
|
||||
let requestService: RequestService;
|
||||
let service: BrowseDefinitionDataService;
|
||||
const findAllDataSpy = jasmine.createSpyObj('findAllData', {
|
||||
findAll: EMPTY,
|
||||
});
|
||||
let findAllDataSpy;
|
||||
let searchDataSpy;
|
||||
const browsesEndpointURL = 'https://rest.api/browses';
|
||||
const halService: any = new HALEndpointServiceStub(browsesEndpointURL);
|
||||
|
||||
const options = new FindListOptions();
|
||||
const linksToFollow = [
|
||||
followLink('entries'),
|
||||
followLink('items')
|
||||
];
|
||||
|
||||
function initTestService() {
|
||||
return new BrowseDefinitionDataService(
|
||||
requestService,
|
||||
getMockRemoteDataBuildService(),
|
||||
getMockObjectCacheService(),
|
||||
halService,
|
||||
);
|
||||
}
|
||||
|
||||
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).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`, () => {
|
||||
it(`should call findAll on findAllData`, () => {
|
||||
service.findAll(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 { FindAllData, FindAllDataImpl } from '../data/base/find-all-data';
|
||||
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
|
||||
@@ -21,8 +23,9 @@ import { dataService } from '../data/base/data-service.decorator';
|
||||
providedIn: 'root',
|
||||
})
|
||||
@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 searchData: SearchDataImpl<BrowseDefinition>;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
@@ -31,7 +34,7 @@ export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseD
|
||||
protected halService: HALEndpointService,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -52,5 +55,71 @@ export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseD
|
||||
findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
|
||||
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';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
||||
import { BrowseDefinitionDataService } from './browse-definition-data.service';
|
||||
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
||||
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>[] = [
|
||||
@@ -35,7 +35,7 @@ export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig<BrowseEntry | Item>[] = [
|
||||
export class BrowseService {
|
||||
protected linkPath = 'browses';
|
||||
|
||||
private static toSearchKeyArray(metadataKey: string): string[] {
|
||||
public static toSearchKeyArray(metadataKey: string): string[] {
|
||||
const keyParts = metadataKey.split('.');
|
||||
const searchFor = [];
|
||||
searchFor.push('*');
|
||||
|
@@ -14,6 +14,7 @@ import { RemoteData } from './remote-data';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import { HttpParams } from '@angular/common/http';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -55,7 +56,7 @@ export class EpersonRegistrationService {
|
||||
* @param email
|
||||
* @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();
|
||||
registration.email = email;
|
||||
|
||||
@@ -70,6 +71,11 @@ export class EpersonRegistrationService {
|
||||
}
|
||||
options.headers = headers;
|
||||
|
||||
if (hasValue(type)) {
|
||||
options.params = type ?
|
||||
new HttpParams({ fromString: 'accountRequestType=' + type }) : new HttpParams();
|
||||
}
|
||||
|
||||
href$.pipe(
|
||||
find((href: string) => hasValue(href)),
|
||||
map((href: string) => {
|
||||
|
16
src/app/core/services/server-xhr.service.ts
Normal file
16
src/app/core/services/server-xhr.service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { XhrFactory } from '@angular/common';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { prototype, XMLHttpRequest } from 'xhr2';
|
||||
|
||||
/**
|
||||
* Overrides the default XhrFactory server side, to allow us to set cookies in requests to the
|
||||
* backend. This was added to be able to perform a working XSRF request from the node server, as it
|
||||
* needs to set a cookie for the XSRF token
|
||||
*/
|
||||
@Injectable()
|
||||
export class ServerXhrService implements XhrFactory {
|
||||
build(): XMLHttpRequest {
|
||||
prototype._restrictedHeaders.cookie = false;
|
||||
return new XMLHttpRequest();
|
||||
}
|
||||
}
|
@@ -1,11 +1,14 @@
|
||||
/**
|
||||
* An Enum defining the representation type of metadata
|
||||
*/
|
||||
import { BrowseDefinition } from '../browse-definition.model';
|
||||
|
||||
export enum MetadataRepresentationType {
|
||||
None = 'none',
|
||||
Item = 'item',
|
||||
AuthorityControlled = 'authority_controlled',
|
||||
PlainText = 'plain_text'
|
||||
PlainText = 'plain_text',
|
||||
BrowseLink = 'browse_link'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,8 +27,14 @@ export interface MetadataRepresentation {
|
||||
*/
|
||||
representationType: MetadataRepresentationType;
|
||||
|
||||
/**
|
||||
* The browse definition (optional)
|
||||
*/
|
||||
browseDefinition?: BrowseDefinition;
|
||||
|
||||
/**
|
||||
* Fetches the value to be displayed
|
||||
*/
|
||||
getValue(): string;
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { MetadataRepresentation, MetadataRepresentationType } from '../metadata-representation.model';
|
||||
import { hasValue } from '../../../../shared/empty.util';
|
||||
import { MetadataValue } from '../../metadata.models';
|
||||
import { BrowseDefinition } from '../../browse-definition.model';
|
||||
|
||||
/**
|
||||
* This class defines the way the metadatum it extends should be represented
|
||||
@@ -12,9 +13,15 @@ export class MetadatumRepresentation extends MetadataValue implements MetadataRe
|
||||
*/
|
||||
itemType: string;
|
||||
|
||||
constructor(itemType: string) {
|
||||
/**
|
||||
* The browse definition ID passed in with the metadatum, if any
|
||||
*/
|
||||
browseDefinition?: BrowseDefinition;
|
||||
|
||||
constructor(itemType: string, browseDefinition?: BrowseDefinition) {
|
||||
super();
|
||||
this.itemType = itemType;
|
||||
this.browseDefinition = browseDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,6 +30,8 @@ export class MetadatumRepresentation extends MetadataValue implements MetadataRe
|
||||
get representationType(): MetadataRepresentationType {
|
||||
if (hasValue(this.authority)) {
|
||||
return MetadataRepresentationType.AuthorityControlled;
|
||||
} else if (hasValue(this.browseDefinition)) {
|
||||
return MetadataRepresentationType.BrowseLink;
|
||||
} else {
|
||||
return MetadataRepresentationType.PlainText;
|
||||
}
|
||||
|
@@ -19,6 +19,8 @@ export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN';
|
||||
export const XSRF_RESPONSE_HEADER = 'DSPACE-XSRF-TOKEN';
|
||||
// Name of cookie where we store the XSRF token
|
||||
export const XSRF_COOKIE = 'XSRF-TOKEN';
|
||||
// Name of cookie the backend expects the XSRF token to be in
|
||||
export const DSPACE_XSRF_COOKIE = 'DSPACE-XSRF-COOKIE';
|
||||
|
||||
/**
|
||||
* Custom Http Interceptor intercepting Http Requests & Responses to
|
||||
|
@@ -35,6 +35,10 @@ import { VersionDataService } from '../../../../core/data/version-data.service';
|
||||
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { mockRouteService } from '../../../../item-page/simple/item-types/shared/item.component.spec';
|
||||
import {
|
||||
BrowseDefinitionDataServiceStub
|
||||
} from '../../../../shared/testing/browse-definition-data-service.stub';
|
||||
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
|
||||
|
||||
let comp: JournalComponent;
|
||||
let fixture: ComponentFixture<JournalComponent>;
|
||||
@@ -100,7 +104,8 @@ describe('JournalComponent', () => {
|
||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||
{ provide: WorkspaceitemDataService, useValue: {} },
|
||||
{ provide: SearchService, useValue: {} },
|
||||
{ provide: RouteService, useValue: mockRouteService }
|
||||
{ provide: RouteService, useValue: mockRouteService },
|
||||
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<ds-register-email-form
|
||||
[MESSAGE_PREFIX]="'forgot-email.form'">
|
||||
[MESSAGE_PREFIX]="'forgot-email.form'" [typeRequest]="typeRequest">
|
||||
</ds-register-email-form>
|
@@ -1,4 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { TYPE_REQUEST_FORGOT } from '../../register-email-form/register-email-form.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-forgot-email',
|
||||
@@ -9,5 +10,5 @@ import { Component } from '@angular/core';
|
||||
* Component responsible the forgot password email step
|
||||
*/
|
||||
export class ForgotEmailComponent {
|
||||
|
||||
typeRequest = TYPE_REQUEST_FORGOT;
|
||||
}
|
||||
|
@@ -3,8 +3,8 @@
|
||||
<div class="col-12">
|
||||
<h2 class="border-bottom">{{'item.edit.head' | translate}}</h2>
|
||||
<div class="pt-2">
|
||||
<ul class="nav nav-tabs justify-content-start">
|
||||
<li *ngFor="let page of pages" class="nav-item">
|
||||
<ul class="nav nav-tabs justify-content-start" role="tablist">
|
||||
<li *ngFor="let page of pages" class="nav-item" [attr.aria-selected]="page.page === currentPage" role="tab">
|
||||
<a *ngIf="(page.enabled | async)"
|
||||
class="nav-link"
|
||||
[ngClass]="{'active' : page.page === currentPage}"
|
||||
|
@@ -1,16 +1,38 @@
|
||||
<ds-metadata-field-wrapper [label]="label | translate">
|
||||
<ng-container *ngFor="let mdValue of mdValues; let last=last;">
|
||||
<ng-container *ngTemplateOutlet="(renderMarkdown ? markdown : simple); context: {value: mdValue.value}">
|
||||
<!--
|
||||
Choose a template. Priority: markdown, link, browse link.
|
||||
-->
|
||||
<ng-container *ngTemplateOutlet="(renderMarkdown ? markdown : (hasLink(mdValue) ? link : (hasBrowseDefinition() ? browselink : simple)));
|
||||
context: {value: mdValue.value}">
|
||||
</ng-container>
|
||||
<span class="separator" *ngIf="!last" [innerHTML]="separator"></span>
|
||||
</ng-container>
|
||||
</ds-metadata-field-wrapper>
|
||||
|
||||
<!-- Render value as markdown -->
|
||||
<ng-template #markdown let-value="value">
|
||||
<span class="dont-break-out" [innerHTML]="value | dsMarkdown | async">
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
<!-- Render value as a link (href and label) -->
|
||||
<ng-template #link let-value="value">
|
||||
<a class="dont-break-out ds-simple-metadata-link" target="_blank" [href]="value">
|
||||
{{value}}
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<!-- Render simple value in a span -->
|
||||
<ng-template #simple let-value="value">
|
||||
<span class="dont-break-out preserve-line-breaks">{{value}}</span>
|
||||
</ng-template>
|
||||
|
||||
<!-- Render value as a link to browse index -->
|
||||
<ng-template #browselink let-value="value">
|
||||
<a class="dont-break-out preserve-line-breaks ds-browse-link"
|
||||
[routerLink]="['/browse', browseDefinition.id]"
|
||||
[queryParams]="getQueryParams(value)">
|
||||
{{value}}
|
||||
</a>
|
||||
</ng-template>
|
||||
|
@@ -52,6 +52,7 @@ describe('MetadataValuesComponent', () => {
|
||||
comp.mdValues = mockMetadata;
|
||||
comp.separator = mockSeperator;
|
||||
comp.label = mockLabel;
|
||||
comp.urlRegex = /^.*test.*$/;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
@@ -67,4 +68,9 @@ describe('MetadataValuesComponent', () => {
|
||||
expect(separators.length).toBe(mockMetadata.length - 1);
|
||||
});
|
||||
|
||||
it('should correctly detect a pattern on string containing "test"', () => {
|
||||
const mdValue = {value: 'This is a test value'} as MetadataValue;
|
||||
expect(comp.hasLink(mdValue)).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { Component, Inject, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
|
||||
import { BrowseDefinition } from '../../../core/shared/browse-definition.model';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
|
||||
@@ -40,12 +42,51 @@ export class MetadataValuesComponent implements OnChanges {
|
||||
*/
|
||||
@Input() enableMarkdown = false;
|
||||
|
||||
/**
|
||||
* Whether any valid HTTP(S) URL should be rendered as a link
|
||||
*/
|
||||
@Input() urlRegex?;
|
||||
|
||||
/**
|
||||
* This variable will be true if both {@link environment.markdown.enabled} and {@link enableMarkdown} are true.
|
||||
*/
|
||||
renderMarkdown;
|
||||
|
||||
@Input() browseDefinition?: BrowseDefinition;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.renderMarkdown = !!this.appConfig.markdown.enabled && this.enableMarkdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this metadata value have a configured link to a browse definition?
|
||||
*/
|
||||
hasBrowseDefinition(): boolean {
|
||||
return hasValue(this.browseDefinition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this metadata value have a valid URL that should be rendered as a link?
|
||||
* @param value A MetadataValue being displayed
|
||||
*/
|
||||
hasLink(value: MetadataValue): boolean {
|
||||
if (hasValue(this.urlRegex)) {
|
||||
const pattern = new RegExp(this.urlRegex);
|
||||
return pattern.test(value.value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a queryparams object for use in a link, with the key dependent on whether this browse
|
||||
* definition is metadata browse, or item browse
|
||||
* @param value the specific metadata value being linked
|
||||
*/
|
||||
getQueryParams(value) {
|
||||
let queryParams = {startsWith: value};
|
||||
if (this.browseDefinition.metadataBrowse) {
|
||||
return {value: value};
|
||||
}
|
||||
return queryParams;
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,8 @@ import { SharedModule } from '../../../../../shared/shared.module';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service';
|
||||
import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub';
|
||||
|
||||
let comp: ItemPageAbstractFieldComponent;
|
||||
let fixture: ComponentFixture<ItemPageAbstractFieldComponent>;
|
||||
@@ -25,6 +27,7 @@ describe('ItemPageAbstractFieldComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
|
||||
],
|
||||
declarations: [ItemPageAbstractFieldComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -3,10 +3,12 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
|
||||
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||
import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec';
|
||||
import { ItemPageAuthorFieldComponent } from './item-page-author-field.component';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service';
|
||||
import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub';
|
||||
|
||||
let comp: ItemPageAuthorFieldComponent;
|
||||
let fixture: ComponentFixture<ItemPageAuthorFieldComponent>;
|
||||
@@ -25,6 +27,7 @@ describe('ItemPageAuthorFieldComponent', () => {
|
||||
})],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
|
||||
],
|
||||
declarations: [ItemPageAuthorFieldComponent, MetadataValuesComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -37,7 +40,7 @@ describe('ItemPageAuthorFieldComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(ItemPageAuthorFieldComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.item = mockItemWithMetadataFieldAndValue(field, mockValue);
|
||||
comp.item = mockItemWithMetadataFieldsAndValue([field], mockValue);
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
|
@@ -3,10 +3,12 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
|
||||
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||
import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec';
|
||||
import { ItemPageDateFieldComponent } from './item-page-date-field.component';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service';
|
||||
import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub';
|
||||
|
||||
let comp: ItemPageDateFieldComponent;
|
||||
let fixture: ComponentFixture<ItemPageDateFieldComponent>;
|
||||
@@ -25,6 +27,7 @@ describe('ItemPageDateFieldComponent', () => {
|
||||
})],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
|
||||
],
|
||||
declarations: [ItemPageDateFieldComponent, MetadataValuesComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -36,7 +39,7 @@ describe('ItemPageDateFieldComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(ItemPageDateFieldComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||
comp.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue);
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
|
@@ -3,10 +3,12 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
|
||||
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||
import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec';
|
||||
import { GenericItemPageFieldComponent } from './generic-item-page-field.component';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service';
|
||||
import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub';
|
||||
|
||||
let comp: GenericItemPageFieldComponent;
|
||||
let fixture: ComponentFixture<GenericItemPageFieldComponent>;
|
||||
@@ -27,6 +29,7 @@ describe('GenericItemPageFieldComponent', () => {
|
||||
})],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
|
||||
],
|
||||
declarations: [GenericItemPageFieldComponent, MetadataValuesComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -38,7 +41,7 @@ describe('GenericItemPageFieldComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(GenericItemPageFieldComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||
comp.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue);
|
||||
comp.fields = mockFields;
|
||||
comp.label = mockLabel;
|
||||
fixture.detectChanges();
|
||||
|
@@ -40,5 +40,10 @@ export class GenericItemPageFieldComponent extends ItemPageFieldComponent {
|
||||
*/
|
||||
@Input() enableMarkdown = false;
|
||||
|
||||
/**
|
||||
* Whether any valid HTTP(S) URL should be rendered as a link
|
||||
*/
|
||||
@Input() urlRegex?: string;
|
||||
|
||||
|
||||
}
|
||||
|
@@ -4,5 +4,7 @@
|
||||
[separator]="separator"
|
||||
[label]="label"
|
||||
[enableMarkdown]="enableMarkdown"
|
||||
[urlRegex]="urlRegex"
|
||||
[browseDefinition]="browseDefinition|async"
|
||||
></ds-metadata-values>
|
||||
</div>
|
||||
|
@@ -12,6 +12,10 @@ import { environment } from '../../../../../environments/environment';
|
||||
import { MarkdownPipe } from '../../../../shared/utils/markdown.pipe';
|
||||
import { SharedModule } from '../../../../shared/shared.module';
|
||||
import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
|
||||
import { BrowseDefinitionDataServiceStub } from '../../../../shared/testing/browse-definition-data-service.stub';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
let comp: ItemPageFieldComponent;
|
||||
let fixture: ComponentFixture<ItemPageFieldComponent>;
|
||||
@@ -20,7 +24,9 @@ let markdownSpy;
|
||||
const mockValue = 'test value';
|
||||
const mockField = 'dc.test';
|
||||
const mockLabel = 'test label';
|
||||
const mockFields = [mockField];
|
||||
const mockAuthorField = 'dc.contributor.author';
|
||||
const mockDateIssuedField = 'dc.date.issued';
|
||||
const mockFields = [mockField, mockAuthorField, mockDateIssuedField];
|
||||
|
||||
describe('ItemPageFieldComponent', () => {
|
||||
|
||||
@@ -34,6 +40,7 @@ describe('ItemPageFieldComponent', () => {
|
||||
const buildTestEnvironment = async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule.withRoutes([]),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
@@ -44,6 +51,7 @@ describe('ItemPageFieldComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: appConfig },
|
||||
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
|
||||
],
|
||||
declarations: [ItemPageFieldComponent, MetadataValuesComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -53,7 +61,7 @@ describe('ItemPageFieldComponent', () => {
|
||||
markdownSpy = spyOn(MarkdownPipe.prototype, 'transform');
|
||||
fixture = TestBed.createComponent(ItemPageFieldComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||
comp.item = mockItemWithMetadataFieldsAndValue(mockFields, mockValue);
|
||||
comp.fields = mockFields;
|
||||
comp.label = mockLabel;
|
||||
fixture.detectChanges();
|
||||
@@ -126,17 +134,57 @@ describe('ItemPageFieldComponent', () => {
|
||||
expect(markdownSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('test rendering of configured browse links', () => {
|
||||
beforeEach(() => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
waitForAsync(() => {
|
||||
it('should have a browse link', () => {
|
||||
expect(fixture.debugElement.query(By.css('a.ds-browse-link')).nativeElement.innerHTML).toContain(mockValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export function mockItemWithMetadataFieldAndValue(field: string, value: string): Item {
|
||||
describe('test rendering of configured regex-based links', () => {
|
||||
beforeEach(() => {
|
||||
comp.urlRegex = '^test';
|
||||
fixture.detectChanges();
|
||||
});
|
||||
waitForAsync(() => {
|
||||
it('should have a rendered (non-browse) link since the value matches ^test', () => {
|
||||
expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link')).nativeElement.innerHTML).toContain(mockValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('test skipping of configured links that do NOT match regex', () => {
|
||||
beforeEach(() => {
|
||||
comp.urlRegex = '^nope';
|
||||
fixture.detectChanges();
|
||||
});
|
||||
beforeEach(waitForAsync(() => {
|
||||
it('should NOT have a rendered (non-browse) link since the value matches ^test', () => {
|
||||
expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link'))).toBeNull();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
export function mockItemWithMetadataFieldsAndValue(fields: string[], value: string): Item {
|
||||
const item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: new MetadataMap()
|
||||
});
|
||||
fields.forEach((field: string) => {
|
||||
item.metadata[field] = [{
|
||||
language: 'en_US',
|
||||
value: value
|
||||
}] as MetadataValue[];
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
@@ -1,5 +1,10 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BrowseDefinition } from '../../../../core/shared/browse-definition.model';
|
||||
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
|
||||
import { getRemoteDataPayload } from '../../../../core/shared/operators';
|
||||
|
||||
/**
|
||||
* This component can be used to represent metadata on a simple item page.
|
||||
@@ -12,6 +17,9 @@ import { Item } from '../../../../core/shared/item.model';
|
||||
})
|
||||
export class ItemPageFieldComponent {
|
||||
|
||||
constructor(protected browseDefinitionDataService: BrowseDefinitionDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The item to display metadata for
|
||||
*/
|
||||
@@ -38,4 +46,19 @@ export class ItemPageFieldComponent {
|
||||
*/
|
||||
separator = '<br/>';
|
||||
|
||||
/**
|
||||
* Whether any valid HTTP(S) URL should be rendered as a link
|
||||
*/
|
||||
urlRegex?: string;
|
||||
|
||||
/**
|
||||
* Return browse definition that matches any field used in this component if it is configured as a browse
|
||||
* link in dspace.cfg (webui.browse.link.<n>)
|
||||
*/
|
||||
get browseDefinition(): Observable<BrowseDefinition> {
|
||||
return this.browseDefinitionDataService.findByFields(this.fields).pipe(
|
||||
getRemoteDataPayload(),
|
||||
map((def) => def)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
|
||||
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||
import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec';
|
||||
import { ItemPageTitleFieldComponent } from './item-page-title-field.component';
|
||||
|
||||
let comp: ItemPageTitleFieldComponent;
|
||||
@@ -31,7 +31,7 @@ describe('ItemPageTitleFieldComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(ItemPageTitleFieldComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||
comp.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue);
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
|
@@ -2,11 +2,13 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
|
||||
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||
import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec';
|
||||
import { ItemPageUriFieldComponent } from './item-page-uri-field.component';
|
||||
import { MetadataUriValuesComponent } from '../../../../field-components/metadata-uri-values/metadata-uri-values.component';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service';
|
||||
import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub';
|
||||
|
||||
let comp: ItemPageUriFieldComponent;
|
||||
let fixture: ComponentFixture<ItemPageUriFieldComponent>;
|
||||
@@ -26,6 +28,7 @@ describe('ItemPageUriFieldComponent', () => {
|
||||
})],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
|
||||
],
|
||||
declarations: [ItemPageUriFieldComponent, MetadataUriValuesComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -37,7 +40,7 @@ describe('ItemPageUriFieldComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(ItemPageUriFieldComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||
comp.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue);
|
||||
comp.fields = [mockField];
|
||||
comp.label = mockLabel;
|
||||
fixture.detectChanges();
|
||||
|
@@ -36,6 +36,10 @@ import { VersionDataService } from '../../../../core/data/version-data.service';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
|
||||
import {
|
||||
BrowseDefinitionDataServiceStub
|
||||
} from '../../../../shared/testing/browse-definition-data-service.stub';
|
||||
|
||||
const noMetadata = new MetadataMap();
|
||||
|
||||
@@ -87,7 +91,8 @@ describe('PublicationComponent', () => {
|
||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||
{ provide: WorkspaceitemDataService, useValue: {} },
|
||||
{ provide: SearchService, useValue: {} },
|
||||
{ provide: RouteService, useValue: mockRouteService }
|
||||
{ provide: RouteService, useValue: mockRouteService },
|
||||
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub },
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -23,7 +23,9 @@ import { UUIDService } from '../../../../core/shared/uuid.service';
|
||||
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||
import {
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../../../shared/remote-data.utils';
|
||||
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
|
||||
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
||||
@@ -38,6 +40,10 @@ import { VersionHistoryDataService } from '../../../../core/data/version-history
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { ResearcherProfileDataService } from '../../../../core/profile/researcher-profile-data.service';
|
||||
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
|
||||
import {
|
||||
BrowseDefinitionDataServiceStub
|
||||
} from '../../../../shared/testing/browse-definition-data-service.stub';
|
||||
|
||||
import { buildPaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||
@@ -125,7 +131,8 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
|
||||
{ provide: SearchService, useValue: {} },
|
||||
{ provide: RouteService, useValue: mockRouteService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: ResearcherProfileDataService, useValue: {} }
|
||||
{ provide: ResearcherProfileDataService, useValue: {} },
|
||||
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub },
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -444,7 +451,7 @@ describe('ItemComponent', () => {
|
||||
{ provide: SearchService, useValue: {} },
|
||||
{ provide: RouteService, useValue: mockRouteService },
|
||||
{ provide: AuthorizationDataService, useValue: {} },
|
||||
{ provide: ResearcherProfileDataService, useValue: {} }
|
||||
{ provide: ResearcherProfileDataService, useValue: {} },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(ItemComponent, {
|
||||
|
@@ -37,6 +37,10 @@ import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { ItemVersionsSharedService } from '../../../versions/item-versions-shared.service';
|
||||
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
|
||||
import {
|
||||
BrowseDefinitionDataServiceStub
|
||||
} from '../../../../shared/testing/browse-definition-data-service.stub';
|
||||
|
||||
const noMetadata = new MetadataMap();
|
||||
|
||||
@@ -90,7 +94,8 @@ describe('UntypedItemComponent', () => {
|
||||
{ provide: SearchService, useValue: {} },
|
||||
{ provide: ItemDataService, useValue: {} },
|
||||
{ provide: ItemVersionsSharedService, useValue: {} },
|
||||
{ provide: RouteService, useValue: mockRouteService }
|
||||
{ provide: RouteService, useValue: mockRouteService },
|
||||
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(UntypedItemComponent, {
|
||||
|
@@ -77,7 +77,7 @@ describe('VersionedItemComponent', () => {
|
||||
{ provide: WorkspaceitemDataService, useValue: {} },
|
||||
{ provide: SearchService, useValue: {} },
|
||||
{ provide: ItemDataService, useValue: {} },
|
||||
{ provide: RouteService, useValue: mockRouteService }
|
||||
{ provide: RouteService, useValue: mockRouteService },
|
||||
]
|
||||
}).compileComponents();
|
||||
versionService = TestBed.inject(VersionDataService);
|
||||
|
@@ -35,7 +35,7 @@ export class VersionedItemComponent extends ItemComponent {
|
||||
private workspaceItemDataService: WorkspaceitemDataService,
|
||||
private searchService: SearchService,
|
||||
private itemService: ItemDataService,
|
||||
protected routeService: RouteService,
|
||||
protected routeService: RouteService
|
||||
) {
|
||||
super(routeService, router);
|
||||
}
|
||||
|
@@ -11,6 +11,8 @@ import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
|
||||
import { BrowseDefinitionDataService } from '../../../core/browse/browse-definition-data.service';
|
||||
import { BrowseDefinitionDataServiceStub } from '../../../shared/testing/browse-definition-data-service.stub';
|
||||
|
||||
const itemType = 'Person';
|
||||
const metadataFields = ['dc.contributor.author', 'dc.creator'];
|
||||
@@ -104,7 +106,8 @@ describe('MetadataRepresentationListComponent', () => {
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [MetadataRepresentationListComponent, VarDirective],
|
||||
providers: [
|
||||
{ provide: RelationshipDataService, useValue: relationshipService }
|
||||
{ provide: RelationshipDataService, useValue: relationshipService },
|
||||
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(MetadataRepresentationListComponent, {
|
||||
|
@@ -8,6 +8,13 @@ import { RelationshipDataService } from '../../../core/data/relationship-data.se
|
||||
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { AbstractIncrementalListComponent } from '../abstract-incremental-list/abstract-incremental-list.component';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { getRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import {
|
||||
MetadatumRepresentation
|
||||
} from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
|
||||
import { BrowseService } from '../../../core/browse/browse.service';
|
||||
import { BrowseDefinitionDataService } from '../../../core/browse/browse-definition-data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-representation-list',
|
||||
@@ -52,7 +59,8 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
|
||||
*/
|
||||
total: number;
|
||||
|
||||
constructor(public relationshipService: RelationshipDataService) {
|
||||
constructor(public relationshipService: RelationshipDataService,
|
||||
private browseDefinitionDataService: BrowseDefinitionDataService) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -76,7 +84,21 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
|
||||
...metadata
|
||||
.slice((this.objects.length * this.incrementBy), (this.objects.length * this.incrementBy) + this.incrementBy)
|
||||
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
|
||||
.map((metadatum: MetadataValue) => this.relationshipService.resolveMetadataRepresentation(metadatum, this.parentItem, this.itemType)),
|
||||
.map((metadatum: MetadataValue) => {
|
||||
if (metadatum.isVirtual) {
|
||||
return this.relationshipService.resolveMetadataRepresentation(metadatum, this.parentItem, this.itemType);
|
||||
} else {
|
||||
// Check for a configured browse link and return a standard metadata representation
|
||||
let searchKeyArray: string[] = [];
|
||||
this.metadataFields.forEach((field: string) => {
|
||||
searchKeyArray = searchKeyArray.concat(BrowseService.toSearchKeyArray(field));
|
||||
});
|
||||
return this.browseDefinitionDataService.findByFields(this.metadataFields).pipe(
|
||||
getRemoteDataPayload(),
|
||||
map((def) => Object.assign(new MetadatumRepresentation(this.itemType, def), metadatum))
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,10 @@
|
||||
<h2>{{MESSAGE_PREFIX + '.header'|translate}}</h2>
|
||||
<p>{{MESSAGE_PREFIX + '.info' | translate}}</p>
|
||||
|
||||
<p *ngIf="validMailDomains.length != 0 && typeRequest === TYPE_REQUEST_REGISTER">
|
||||
{{ MESSAGE_PREFIX + '.info.maildomain' | translate}} {{ validMailDomains.join(', ')}}
|
||||
</p>
|
||||
|
||||
<form [class]="'ng-invalid'" [formGroup]="form">
|
||||
|
||||
<div class="form-group">
|
||||
@@ -16,8 +20,11 @@
|
||||
<span *ngIf="email.errors && email.errors.required">
|
||||
{{ MESSAGE_PREFIX + '.email.error.required' | translate }}
|
||||
</span>
|
||||
<span *ngIf="email.errors && email.errors.pattern">
|
||||
{{ MESSAGE_PREFIX + '.email.error.pattern' | translate }}
|
||||
<span *ngIf="email.errors && ((email.errors.pattern && this.typeRequest === TYPE_REQUEST_REGISTER) || email.errors.email)">
|
||||
{{ MESSAGE_PREFIX + '.email.error.not-email-form' | translate }}
|
||||
<ng-container *ngIf="validMailDomains.length > 0">
|
||||
{{ MESSAGE_PREFIX + '.email.error.not-valid-domain' | translate: { domains: validMailDomains.join(', ') } }}
|
||||
</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,5 +60,4 @@
|
||||
</ng-template>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
|
@@ -12,14 +12,19 @@ import { EpersonRegistrationService } from '../core/data/eperson-registration.se
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterStub } from '../shared/testing/router.stub';
|
||||
import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub';
|
||||
import { RegisterEmailFormComponent } from './register-email-form.component';
|
||||
import {
|
||||
RegisterEmailFormComponent,
|
||||
TYPE_REQUEST_REGISTER,
|
||||
TYPE_REQUEST_FORGOT
|
||||
} from './register-email-form.component';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||
import { ConfigurationDataService } from '../core/data/configuration-data.service';
|
||||
import { GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service';
|
||||
import { CookieService } from '../core/services/cookie.service';
|
||||
import { CookieServiceMock } from '../shared/mocks/cookie.service.mock';
|
||||
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
|
||||
|
||||
describe('RegisterEmailComponent', () => {
|
||||
describe('RegisterEmailFormComponent', () => {
|
||||
|
||||
let comp: RegisterEmailFormComponent;
|
||||
let fixture: ComponentFixture<RegisterEmailFormComponent>;
|
||||
@@ -53,6 +58,8 @@ describe('RegisterEmailComponent', () => {
|
||||
registerEmail: createSuccessfulRemoteDataObject$({})
|
||||
});
|
||||
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), ReactiveFormsModule],
|
||||
declarations: [RegisterEmailFormComponent],
|
||||
@@ -95,17 +102,53 @@ describe('RegisterEmailComponent', () => {
|
||||
comp.form.patchValue({email: 'valid@email.org'});
|
||||
expect(comp.form.invalid).toBeFalse();
|
||||
});
|
||||
it('should accept email with other domain names on TYPE_REQUEST_FORGOT form', () => {
|
||||
spyOn(configurationDataService, 'findByPropertyName').and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||
name: 'authentication-password.domain.valid',
|
||||
values: ['marvel.com'],
|
||||
})));
|
||||
comp.typeRequest = TYPE_REQUEST_FORGOT;
|
||||
|
||||
comp.ngOnInit();
|
||||
|
||||
comp.form.patchValue({ email: 'valid@email.org' });
|
||||
expect(comp.form.invalid).toBeFalse();
|
||||
});
|
||||
it('should be valid when uppercase letters are used', () => {
|
||||
comp.form.patchValue({email: 'VALID@email.org'});
|
||||
expect(comp.form.invalid).toBeFalse();
|
||||
});
|
||||
it('should not accept email with other domain names', () => {
|
||||
spyOn(configurationDataService, 'findByPropertyName').and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||
name: 'authentication-password.domain.valid',
|
||||
values: ['marvel.com'],
|
||||
})));
|
||||
comp.typeRequest = TYPE_REQUEST_REGISTER;
|
||||
|
||||
comp.ngOnInit();
|
||||
|
||||
comp.form.patchValue({ email: 'valid@email.org' });
|
||||
expect(comp.form.invalid).toBeTrue();
|
||||
});
|
||||
it('should accept email with the given domain name', () => {
|
||||
spyOn(configurationDataService, 'findByPropertyName').and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||
name: 'authentication-password.domain.valid',
|
||||
values: ['marvel.com'],
|
||||
})));
|
||||
comp.typeRequest = TYPE_REQUEST_REGISTER;
|
||||
|
||||
comp.ngOnInit();
|
||||
|
||||
comp.form.patchValue({ email: 'thor.odinson@marvel.com' });
|
||||
expect(comp.form.invalid).toBeFalse();
|
||||
});
|
||||
});
|
||||
describe('register', () => {
|
||||
it('should send a registration to the service and on success display a message and return to home', () => {
|
||||
comp.form.patchValue({email: 'valid@email.org'});
|
||||
|
||||
comp.register();
|
||||
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org');
|
||||
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', null, null);
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/home']);
|
||||
});
|
||||
@@ -115,7 +158,7 @@ describe('RegisterEmailComponent', () => {
|
||||
comp.form.patchValue({email: 'valid@email.org'});
|
||||
|
||||
comp.register();
|
||||
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org');
|
||||
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', null, null);
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -133,7 +176,7 @@ describe('RegisterEmailComponent', () => {
|
||||
comp.form.patchValue({email: 'valid@email.org'});
|
||||
comp.register();
|
||||
tick();
|
||||
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken');
|
||||
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken', null);
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/home']);
|
||||
}));
|
||||
@@ -144,7 +187,7 @@ describe('RegisterEmailComponent', () => {
|
||||
|
||||
comp.register();
|
||||
tick();
|
||||
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken');
|
||||
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken', null);
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { ChangeDetectorRef, Component, Input, OnInit, Optional } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit, Optional } from '@angular/core';
|
||||
import {EpersonRegistrationService} from '../core/data/eperson-registration.service';
|
||||
import {NotificationsService} from '../shared/notifications/notifications.service';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {Router} from '@angular/router';
|
||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { FormBuilder, FormControl, FormGroup, Validators, ValidatorFn } from '@angular/forms';
|
||||
import {Registration} from '../core/shared/registration.model';
|
||||
import {RemoteData} from '../core/data/remote-data';
|
||||
import {ConfigurationDataService} from '../core/data/configuration-data.service';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
|
||||
import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
|
||||
import {ConfigurationProperty} from '../core/shared/configuration-property.model';
|
||||
import {isNotEmpty} from '../shared/empty.util';
|
||||
import {BehaviorSubject, combineLatest, Observable, of, switchMap} from 'rxjs';
|
||||
@@ -16,6 +16,10 @@ import { CAPTCHA_NAME, GoogleRecaptchaService } from '../core/google-recaptcha/g
|
||||
import {AlertType} from '../shared/alert/aletr-type';
|
||||
import {KlaroService} from '../shared/cookies/klaro.service';
|
||||
import {CookieService} from '../core/services/cookie.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
export const TYPE_REQUEST_FORGOT = 'forgot';
|
||||
export const TYPE_REQUEST_REGISTER = 'register';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-register-email-form',
|
||||
@@ -24,7 +28,7 @@ import { CookieService } from '../core/services/cookie.service';
|
||||
/**
|
||||
* Component responsible to render an email registration form.
|
||||
*/
|
||||
export class RegisterEmailFormComponent implements OnInit {
|
||||
export class RegisterEmailFormComponent implements OnDestroy, OnInit {
|
||||
|
||||
/**
|
||||
* The form containing the mail address
|
||||
@@ -37,6 +41,12 @@ export class RegisterEmailFormComponent implements OnInit {
|
||||
@Input()
|
||||
MESSAGE_PREFIX: string;
|
||||
|
||||
/**
|
||||
* Type of register request to be done, register new email or forgot password (same endpoint)
|
||||
*/
|
||||
@Input()
|
||||
typeRequest: string = null;
|
||||
|
||||
public AlertTypeEnum = AlertType;
|
||||
|
||||
/**
|
||||
@@ -51,6 +61,11 @@ export class RegisterEmailFormComponent implements OnInit {
|
||||
|
||||
disableUntilChecked = true;
|
||||
|
||||
validMailDomains: string[];
|
||||
TYPE_REQUEST_REGISTER = TYPE_REQUEST_REGISTER;
|
||||
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
captchaVersion(): Observable<string> {
|
||||
return this.googleRecaptchaService.captchaVersion();
|
||||
}
|
||||
@@ -72,31 +87,54 @@ export class RegisterEmailFormComponent implements OnInit {
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private notificationsService: NotificationsService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach((sub: Subscription) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form = this.formBuilder.group({
|
||||
email: new FormControl('', {
|
||||
validators: [Validators.required,
|
||||
const validators: ValidatorFn[] = [
|
||||
Validators.required,
|
||||
Validators.email,
|
||||
// Regex pattern borrowed from HTML5 specs for a valid email address:
|
||||
// https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
|
||||
Validators.pattern('^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$')
|
||||
],
|
||||
];
|
||||
this.form = this.formBuilder.group({
|
||||
email: new FormControl('', {
|
||||
validators: validators,
|
||||
})
|
||||
});
|
||||
this.configService.findByPropertyName('registration.verification.enabled').pipe(
|
||||
this.validMailDomains = [];
|
||||
if (this.typeRequest === TYPE_REQUEST_REGISTER) {
|
||||
this.subscriptions.push(this.configService.findByPropertyName('authentication-password.domain.valid')
|
||||
.pipe(getAllSucceededRemoteDataPayload())
|
||||
.subscribe((remoteData: ConfigurationProperty) => {
|
||||
this.validMailDomains = remoteData.values;
|
||||
for (const remoteValue of remoteData.values) {
|
||||
if (this.validMailDomains.length !== 0) {
|
||||
this.form.get('email').setValidators([
|
||||
...validators,
|
||||
Validators.pattern(this.validMailDomains.map((domain: string) => '(^.*' + domain.replace(new RegExp('\\.', 'g'), '\\.') + '$)').join('|')),
|
||||
]);
|
||||
this.form.updateValueAndValidity();
|
||||
}
|
||||
}
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}));
|
||||
}
|
||||
this.subscriptions.push(this.configService.findByPropertyName('registration.verification.enabled').pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((res: ConfigurationProperty) => res?.values[0].toLowerCase() === 'true')
|
||||
).subscribe((res: boolean) => {
|
||||
this.registrationVerification = res;
|
||||
});
|
||||
}));
|
||||
|
||||
this.disableUntilCheckedFcn().subscribe((res) => {
|
||||
this.subscriptions.push(this.disableUntilCheckedFcn().subscribe((res) => {
|
||||
this.disableUntilChecked = res;
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +150,7 @@ export class RegisterEmailFormComponent implements OnInit {
|
||||
register(tokenV2?) {
|
||||
if (!this.form.invalid) {
|
||||
if (this.registrationVerification) {
|
||||
combineLatest([this.captchaVersion(), this.captchaMode()]).pipe(
|
||||
this.subscriptions.push(combineLatest([this.captchaVersion(), this.captchaMode()]).pipe(
|
||||
switchMap(([captchaVersion, captchaMode]) => {
|
||||
if (captchaVersion === 'v3') {
|
||||
return this.googleRecaptchaService.getRecaptchaToken('register_email');
|
||||
@@ -134,7 +172,7 @@ export class RegisterEmailFormComponent implements OnInit {
|
||||
this.showNotification('error');
|
||||
}
|
||||
}
|
||||
);
|
||||
));
|
||||
} else {
|
||||
this.registration();
|
||||
}
|
||||
@@ -146,18 +184,20 @@ export class RegisterEmailFormComponent implements OnInit {
|
||||
*/
|
||||
registration(captchaToken = null) {
|
||||
let registerEmail$ = captchaToken ?
|
||||
this.epersonRegistrationService.registerEmail(this.email.value, captchaToken) :
|
||||
this.epersonRegistrationService.registerEmail(this.email.value);
|
||||
registerEmail$.subscribe((response: RemoteData<Registration>) => {
|
||||
this.epersonRegistrationService.registerEmail(this.email.value, captchaToken, this.typeRequest) :
|
||||
this.epersonRegistrationService.registerEmail(this.email.value, null, this.typeRequest);
|
||||
this.subscriptions.push(registerEmail$.subscribe((response: RemoteData<Registration>) => {
|
||||
if (response.hasSucceeded) {
|
||||
this.notificationService.success(this.translateService.get(`${this.MESSAGE_PREFIX}.success.head`),
|
||||
this.translateService.get(`${this.MESSAGE_PREFIX}.success.content`, {email: this.email.value}));
|
||||
this.router.navigate(['/home']);
|
||||
} else if (response.statusCode === 422) {
|
||||
this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), this.translateService.get(`${this.MESSAGE_PREFIX}.error.maildomain`, {domains: this.validMailDomains.join(', ')}));
|
||||
} else {
|
||||
this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`),
|
||||
this.translateService.get(`${this.MESSAGE_PREFIX}.error.content`, {email: this.email.value}));
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<ds-register-email-form
|
||||
[MESSAGE_PREFIX]="'register-page.registration'">
|
||||
[MESSAGE_PREFIX]="'register-page.registration'" [typeRequest]="typeRequest">
|
||||
</ds-register-email-form>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { TYPE_REQUEST_REGISTER } from '../../register-email-form/register-email-form.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-register-email',
|
||||
@@ -9,5 +10,5 @@ import { Component } from '@angular/core';
|
||||
* Component responsible the email registration step when registering as a new user
|
||||
*/
|
||||
export class RegisterEmailComponent {
|
||||
|
||||
typeRequest = TYPE_REQUEST_REGISTER;
|
||||
}
|
||||
|
@@ -1,9 +1,6 @@
|
||||
input[type="text"] {
|
||||
margin-top: calc(-0.5 * var(--bs-font-size-base));
|
||||
|
||||
&:focus {
|
||||
background-color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
background-color: #fff !important;
|
||||
|
||||
&.collapsed {
|
||||
opacity: 0;
|
||||
|
@@ -0,0 +1,9 @@
|
||||
<div>
|
||||
<a *ngIf="(metadataRepresentation.representationType=='browse_link')"
|
||||
target="_blank" class="dont-break-out"
|
||||
[routerLink]="['/browse/', metadataRepresentation.browseDefinition.id]"
|
||||
[queryParams]="getQueryParams()">
|
||||
{{metadataRepresentation.getValue()}}
|
||||
</a>
|
||||
<b>(new browse link page)</b>
|
||||
</div>
|
@@ -0,0 +1,62 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { BrowseLinkMetadataListElementComponent } from './browse-link-metadata-list-element.component';
|
||||
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
|
||||
|
||||
const mockMetadataRepresentation = Object.assign(new MetadatumRepresentation('type'), {
|
||||
key: 'dc.contributor.author',
|
||||
value: 'Test Author'
|
||||
});
|
||||
|
||||
const mockMetadataRepresentationWithUrl = Object.assign(new MetadatumRepresentation('type'), {
|
||||
key: 'dc.subject',
|
||||
value: 'http://purl.org/test/subject'
|
||||
});
|
||||
|
||||
describe('BrowseLinkMetadataListElementComponent', () => {
|
||||
let comp: BrowseLinkMetadataListElementComponent;
|
||||
let fixture: ComponentFixture<BrowseLinkMetadataListElementComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
declarations: [BrowseLinkMetadataListElementComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(BrowseLinkMetadataListElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(BrowseLinkMetadataListElementComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.metadataRepresentation = mockMetadataRepresentation;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
waitForAsync(() => {
|
||||
it('should contain the value as a browse link', () => {
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain(mockMetadataRepresentation.value);
|
||||
});
|
||||
it('should NOT match isLink', () => {
|
||||
expect(comp.isLink).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(BrowseLinkMetadataListElementComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.metadataRepresentation = mockMetadataRepresentationWithUrl;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
waitForAsync(() => {
|
||||
it('should contain the value expected', () => {
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain(mockMetadataRepresentationWithUrl.value);
|
||||
});
|
||||
it('should match isLink', () => {
|
||||
expect(comp.isLink).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,29 @@
|
||||
import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
||||
import { Component } from '@angular/core';
|
||||
import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component';
|
||||
import { metadataRepresentationComponent } from '../../../metadata-representation/metadata-representation.decorator';
|
||||
//@metadataRepresentationComponent('Publication', MetadataRepresentationType.PlainText)
|
||||
// For now, authority controlled fields are rendered the same way as plain text fields
|
||||
//@metadataRepresentationComponent('Publication', MetadataRepresentationType.AuthorityControlled)
|
||||
@metadataRepresentationComponent('Publication', MetadataRepresentationType.BrowseLink)
|
||||
@Component({
|
||||
selector: 'ds-browse-link-metadata-list-element',
|
||||
templateUrl: './browse-link-metadata-list-element.component.html'
|
||||
})
|
||||
/**
|
||||
* A component for displaying MetadataRepresentation objects in the form of plain text
|
||||
* It will simply use the value retrieved from MetadataRepresentation.getValue() to display as plain text
|
||||
*/
|
||||
export class BrowseLinkMetadataListElementComponent extends MetadataRepresentationListElementComponent {
|
||||
/**
|
||||
* Get the appropriate query parameters for this browse link, depending on whether the browse definition
|
||||
* expects 'startsWith' (eg browse by date) or 'value' (eg browse by title)
|
||||
*/
|
||||
getQueryParams() {
|
||||
let queryParams = {startsWith: this.metadataRepresentation.getValue()};
|
||||
if (this.metadataRepresentation.browseDefinition.metadataBrowse) {
|
||||
return {value: this.metadataRepresentation.getValue()};
|
||||
}
|
||||
return queryParams;
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
|
||||
import { mockData } from '../../testing/browse-definition-data-service.stub';
|
||||
import { MetadataRepresentationListElementComponent } from './metadata-representation-list-element.component';
|
||||
|
||||
// Mock metadata representation values
|
||||
const mockMetadataRepresentation = Object.assign(new MetadatumRepresentation('type', mockData[1]), {
|
||||
key: 'dc.contributor.author',
|
||||
value: 'Test Author'
|
||||
});
|
||||
const mockMetadataRepresentationUrl = Object.assign(new MetadatumRepresentation('type', mockData[1]), {
|
||||
key: 'dc.subject',
|
||||
value: 'https://www.google.com'
|
||||
});
|
||||
|
||||
describe('MetadataRepresentationListElementComponent', () => {
|
||||
let comp: MetadataRepresentationListElementComponent;
|
||||
let fixture: ComponentFixture<MetadataRepresentationListElementComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
declarations: [MetadataRepresentationListElementComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(MetadataRepresentationListElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(MetadataRepresentationListElementComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
describe('when the value is not a URL', () => {
|
||||
beforeEach(() => {
|
||||
comp.metadataRepresentation = mockMetadataRepresentation;
|
||||
});
|
||||
it('isLink correctly detects a non-URL string as false', () => {
|
||||
waitForAsync(() => {
|
||||
expect(comp.isLink()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the value is a URL', () => {
|
||||
beforeEach(() => {
|
||||
comp.metadataRepresentation = mockMetadataRepresentationUrl;
|
||||
});
|
||||
it('isLink correctly detects a URL string as true', () => {
|
||||
waitForAsync(() => {
|
||||
expect(comp.isLink()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -13,4 +13,14 @@ export class MetadataRepresentationListElementComponent {
|
||||
* The metadata representation of this component
|
||||
*/
|
||||
metadataRepresentation: MetadataRepresentation;
|
||||
|
||||
/**
|
||||
* Returns true if this component's value matches a basic regex "Is this an HTTP URL" test
|
||||
*/
|
||||
isLink(): boolean {
|
||||
// Match any string that begins with http:// or https://
|
||||
const linkPattern = new RegExp(/^https?\/\/.*/);
|
||||
return linkPattern.test(this.metadataRepresentation.getValue());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,3 +1,17 @@
|
||||
<div>
|
||||
<span class="dont-break-out">{{metadataRepresentation.getValue()}}</span>
|
||||
<!-- Because this template is used by default, we will additionally test for representation type and display accordingly -->
|
||||
<span *ngIf="(metadataRepresentation.representationType=='plain_text') && !isLink()" class="dont-break-out">
|
||||
{{metadataRepresentation.getValue()}}
|
||||
</span>
|
||||
<a *ngIf="(metadataRepresentation.representationType=='plain_text') && isLink()" class="dont-break-out"
|
||||
target="_blank" [href]="metadataRepresentation.getValue()">
|
||||
{{metadataRepresentation.getValue()}}
|
||||
</a>
|
||||
<span *ngIf="(metadataRepresentation.representationType=='authority_controlled')" class="dont-break-out">{{metadataRepresentation.getValue()}}</span>
|
||||
<a *ngIf="(metadataRepresentation.representationType=='browse_link')"
|
||||
class="dont-break-out ds-browse-link"
|
||||
[routerLink]="['/browse/', metadataRepresentation.browseDefinition.id]"
|
||||
[queryParams]="getQueryParams()">
|
||||
{{metadataRepresentation.getValue()}}
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -2,8 +2,12 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { PlainTextMetadataListElementComponent } from './plain-text-metadata-list-element.component';
|
||||
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { mockData } from '../../../testing/browse-definition-data-service.stub';
|
||||
|
||||
const mockMetadataRepresentation = Object.assign(new MetadatumRepresentation('type'), {
|
||||
// Render the mock representation with the default mock author browse definition so it is also rendered as a link
|
||||
// without affecting other tests
|
||||
const mockMetadataRepresentation = Object.assign(new MetadatumRepresentation('type', mockData[1]), {
|
||||
key: 'dc.contributor.author',
|
||||
value: 'Test Author'
|
||||
});
|
||||
@@ -33,4 +37,8 @@ describe('PlainTextMetadataListElementComponent', () => {
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain(mockMetadataRepresentation.value);
|
||||
});
|
||||
|
||||
it('should contain the browse link as plain text', () => {
|
||||
expect(fixture.debugElement.query(By.css('a.ds-browse-link')).nativeElement.innerHTML).toContain(mockMetadataRepresentation.value);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -15,4 +15,15 @@ import { metadataRepresentationComponent } from '../../../metadata-representatio
|
||||
* It will simply use the value retrieved from MetadataRepresentation.getValue() to display as plain text
|
||||
*/
|
||||
export class PlainTextMetadataListElementComponent extends MetadataRepresentationListElementComponent {
|
||||
/**
|
||||
* Get the appropriate query parameters for this browse link, depending on whether the browse definition
|
||||
* expects 'startsWith' (eg browse by date) or 'value' (eg browse by title)
|
||||
*/
|
||||
getQueryParams() {
|
||||
let queryParams = {startsWith: this.metadataRepresentation.getValue()};
|
||||
if (this.metadataRepresentation.browseDefinition.metadataBrowse) {
|
||||
return {value: this.metadataRepresentation.getValue()};
|
||||
}
|
||||
return queryParams;
|
||||
}
|
||||
}
|
||||
|
@@ -84,6 +84,8 @@ import { LangSwitchComponent } from './lang-switch/lang-switch.component';
|
||||
import {
|
||||
PlainTextMetadataListElementComponent
|
||||
} from './object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component';
|
||||
import { BrowseLinkMetadataListElementComponent }
|
||||
from './object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component';
|
||||
import {
|
||||
ItemMetadataListElementComponent
|
||||
} from './object-list/metadata-representation-list-element/item/item-metadata-list-element.component';
|
||||
@@ -383,6 +385,7 @@ const ENTRY_COMPONENTS = [
|
||||
EditItemSelectorComponent,
|
||||
ThemedEditItemSelectorComponent,
|
||||
PlainTextMetadataListElementComponent,
|
||||
BrowseLinkMetadataListElementComponent,
|
||||
ItemMetadataListElementComponent,
|
||||
MetadataRepresentationListElementComponent,
|
||||
ItemMetadataRepresentationListElementComponent,
|
||||
|
@@ -0,0 +1,63 @@
|
||||
import { EMPTY, Observable, of as observableOf } from 'rxjs';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
|
||||
// This data is in post-serialized form (metadata -> metadataKeys)
|
||||
export const mockData: BrowseDefinition[] = [
|
||||
Object.assign(new BrowseDefinition, {
|
||||
'id' : 'dateissued',
|
||||
'metadataBrowse' : false,
|
||||
'dataType' : 'date',
|
||||
'sortOptions' : EMPTY,
|
||||
'order' : 'ASC',
|
||||
'type' : 'browse',
|
||||
'metadataKeys' : [ 'dc.date.issued' ],
|
||||
'_links' : EMPTY
|
||||
}),
|
||||
Object.assign(new BrowseDefinition, {
|
||||
'id' : 'author',
|
||||
'metadataBrowse' : true,
|
||||
'dataType' : 'text',
|
||||
'sortOptions' : EMPTY,
|
||||
'order' : 'ASC',
|
||||
'type' : 'browse',
|
||||
'metadataKeys' : [ 'dc.contributor.*', 'dc.creator' ],
|
||||
'_links' : EMPTY
|
||||
})
|
||||
];
|
||||
|
||||
export const BrowseDefinitionDataServiceStub: any = {
|
||||
|
||||
/**
|
||||
* Get all BrowseDefinitions
|
||||
*/
|
||||
findAll(): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
|
||||
return observableOf(createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), mockData)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all BrowseDefinitions with any link configuration
|
||||
*/
|
||||
findAllLinked(): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
|
||||
return observableOf(createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), mockData)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the browse URL by providing a list of metadata keys
|
||||
*
|
||||
* @param metadataKeys a list of fields eg. ['dc.contributor.author', 'dc.creator']
|
||||
*/
|
||||
findByFields(metadataKeys: string[]): Observable<RemoteData<BrowseDefinition>> {
|
||||
let searchKeyArray: string[] = [];
|
||||
metadataKeys.forEach((metadataKey) => {
|
||||
searchKeyArray = searchKeyArray.concat(BrowseService.toSearchKeyArray(metadataKey));
|
||||
});
|
||||
// Return just the first, as a pretend match
|
||||
return observableOf(createSuccessfulRemoteDataObject(mockData[0]));
|
||||
}
|
||||
|
||||
};
|
@@ -1538,7 +1538,7 @@
|
||||
|
||||
"forgot-email.form.email.error.required": "Please fill in an email address",
|
||||
|
||||
"forgot-email.form.email.error.pattern": "Please fill in a valid email address",
|
||||
"forgot-email.form.email.error.not-email-form": "Please fill in a valid email address",
|
||||
|
||||
"forgot-email.form.email.hint": "An email will be sent to this address with a further instructions.",
|
||||
|
||||
@@ -3342,7 +3342,9 @@
|
||||
|
||||
"register-page.registration.email.error.required": "Please fill in an email address",
|
||||
|
||||
"register-page.registration.email.error.pattern": "Please fill in a valid email address",
|
||||
"register-page.registration.email.error.not-email-form": "Please fill in a valid email address.",
|
||||
|
||||
"register-page.registration.email.error.not-valid-domain": "Use email with allowed domains: {{ domains }}",
|
||||
|
||||
"register-page.registration.email.hint": "This address will be verified and used as your login name.",
|
||||
|
||||
@@ -3359,6 +3361,8 @@
|
||||
"register-page.registration.error.recaptcha": "Error when trying to authenticate with recaptcha",
|
||||
|
||||
"register-page.registration.google-recaptcha.must-accept-cookies": "In order to register you must accept the <b>Registration and Password recovery</b> (Google reCaptcha) cookies.",
|
||||
"register-page.registration.error.maildomain": "This email address is not on the list of domains who can register. Allowed domains are {{ domains }}",
|
||||
|
||||
|
||||
"register-page.registration.google-recaptcha.open-cookie-settings": "Open cookie settings",
|
||||
|
||||
@@ -3367,6 +3371,7 @@
|
||||
"register-page.registration.google-recaptcha.notification.message.error": "An error occurred during reCaptcha verification",
|
||||
|
||||
"register-page.registration.google-recaptcha.notification.message.expired": "Verification expired. Please verify again.",
|
||||
"register-page.registration.info.maildomain": "Accounts can be registered for mail addresses of the domains",
|
||||
|
||||
"relationships.add.error.relationship-type.content": "No suitable match could be found for relationship type {{ type }} between the two items",
|
||||
|
||||
|
@@ -5,6 +5,31 @@ export interface CacheConfig extends Config {
|
||||
msToLive: {
|
||||
default: number;
|
||||
};
|
||||
// Cache-Control HTTP Header
|
||||
control: string;
|
||||
autoSync: AutoSyncConfig;
|
||||
// In-memory caches of server-side rendered (SSR) content. These caches can be used to limit the frequency
|
||||
// of re-generating SSR pages to improve performance.
|
||||
serverSide: {
|
||||
// Debug server-side caching. Set to true to see cache hits/misses/refreshes in console logs.
|
||||
debug: boolean,
|
||||
// Cache specific to known bots. Allows you to serve cached contents to bots only.
|
||||
botCache: {
|
||||
// Maximum number of pages (rendered via SSR) to cache. Setting max=0 disables the cache.
|
||||
max: number;
|
||||
// Amount of time after which cached pages are considered stale (in ms)
|
||||
timeToLive: number;
|
||||
// true = return page from cache after timeToLive expires. false = return a fresh page after timeToLive expires
|
||||
allowStale: boolean;
|
||||
},
|
||||
// Cache specific to anonymous users. Allows you to serve cached content to non-authenticated users.
|
||||
anonymousCache: {
|
||||
// Maximum number of pages (rendered via SSR) to cache. Setting max=0 disables the cache.
|
||||
max: number;
|
||||
// Amount of time after which cached pages are considered stale (in ms)
|
||||
timeToLive: number;
|
||||
// true = return page from cache after timeToLive expires. false = return a fresh page after timeToLive expires
|
||||
allowStale: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -67,11 +67,34 @@ export class DefaultAppConfig implements AppConfig {
|
||||
msToLive: {
|
||||
default: 15 * 60 * 1000 // 15 minutes
|
||||
},
|
||||
control: 'max-age=60', // revalidate browser
|
||||
// Cache-Control HTTP Header
|
||||
control: 'max-age=604800', // revalidate browser
|
||||
autoSync: {
|
||||
defaultTime: 0,
|
||||
maxBufferSize: 100,
|
||||
timePerMethod: { [RestRequestMethod.PATCH]: 3 } as any // time in seconds
|
||||
},
|
||||
// In-memory cache of server-side rendered content
|
||||
serverSide: {
|
||||
debug: false,
|
||||
// Cache specific to known bots. Allows you to serve cached contents to bots only.
|
||||
// Defaults to caching 1,000 pages. Each page expires after 1 day
|
||||
botCache: {
|
||||
// Maximum number of pages (rendered via SSR) to cache. Setting max=0 disables the cache.
|
||||
max: 1000,
|
||||
// Amount of time after which cached pages are considered stale (in ms)
|
||||
timeToLive: 24 * 60 * 60 * 1000, // 1 day
|
||||
allowStale: true,
|
||||
},
|
||||
// Cache specific to anonymous users. Allows you to serve cached content to non-authenticated users.
|
||||
// Defaults to caching 0 pages. But, when enabled, each page expires after 10 seconds (to minimize anonymous users seeing out-of-date content)
|
||||
anonymousCache: {
|
||||
// Maximum number of pages (rendered via SSR) to cache. Setting max=0 disables the cache.
|
||||
max: 0, // disabled by default
|
||||
// Amount of time after which cached pages are considered stale (in ms)
|
||||
timeToLive: 10 * 1000, // 10 seconds
|
||||
allowStale: true,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -55,6 +55,20 @@ export const environment: BuildConfig = {
|
||||
defaultTime: 0,
|
||||
maxBufferSize: 100,
|
||||
timePerMethod: { [RestRequestMethod.PATCH]: 3 } as any // time in seconds
|
||||
},
|
||||
// In-memory cache of server-side rendered pages. Disabled in test environment (max=0)
|
||||
serverSide: {
|
||||
debug: false,
|
||||
botCache: {
|
||||
max: 0,
|
||||
timeToLive: 24 * 60 * 60 * 1000, // 1 day
|
||||
allowStale: true,
|
||||
},
|
||||
anonymousCache: {
|
||||
max: 0,
|
||||
timeToLive: 10 * 1000, // 10 seconds
|
||||
allowStale: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -6,6 +6,7 @@
|
||||
<base href="/">
|
||||
<title>DSpace</title>
|
||||
<meta name="viewport" content="width=device-width,minimum-scale=1">
|
||||
<meta http-equiv="cache-control" content="no-store">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@@ -33,6 +33,8 @@ import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mo
|
||||
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
||||
import { ServerAuthRequestService } from '../../app/core/auth/server-auth-request.service';
|
||||
import { ServerInitService } from './server-init.service';
|
||||
import { XhrFactory } from '@angular/common';
|
||||
import { ServerXhrService } from '../../app/core/services/server-xhr.service';
|
||||
|
||||
export function createTranslateLoader(transferState: TransferState) {
|
||||
return new TranslateServerLoader(transferState, 'dist/server/assets/i18n/', '.json');
|
||||
@@ -104,6 +106,10 @@ export function createTranslateLoader(transferState: TransferState) {
|
||||
provide: HardRedirectService,
|
||||
useClass: ServerHardRedirectService,
|
||||
},
|
||||
{
|
||||
provide: XhrFactory,
|
||||
useClass: ServerXhrService,
|
||||
},
|
||||
]
|
||||
})
|
||||
export class ServerAppModule {
|
||||
|
@@ -6749,6 +6749,11 @@ isbinaryfile@^4.0.8:
|
||||
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3"
|
||||
integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==
|
||||
|
||||
isbot@^3.6.5:
|
||||
version "3.6.5"
|
||||
resolved "https://registry.yarnpkg.com/isbot/-/isbot-3.6.5.tgz#a749980d9dfba9ebcc03ee7b548d1f24dd8c9f1e"
|
||||
integrity sha512-BchONELXt6yMad++BwGpa0oQxo/uD0keL7N15cYVf0A1oMIoNQ79OqeYdPMFWDrNhCqCbRuw9Y9F3QBjvAxZ5g==
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
@@ -7468,7 +7473,7 @@ lru-cache@^6.0.0:
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
lru-cache@^7.7.1:
|
||||
lru-cache@^7.14.1, lru-cache@^7.7.1:
|
||||
version "7.14.1"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea"
|
||||
integrity sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==
|
||||
|
Reference in New Issue
Block a user