Merge remote-tracking branch 'upstream/main' into theme-fixes_contribute-main

# Conflicts:
#	src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.spec.ts
This commit is contained in:
Alexandre Vryghem
2024-04-17 00:20:23 +02:00
42 changed files with 2377 additions and 2516 deletions

1
.gitignore vendored
View File

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

View File

@@ -109,22 +109,22 @@
"serve": { "serve": {
"builder": "@angular-builders/custom-webpack:dev-server", "builder": "@angular-builders/custom-webpack:dev-server",
"options": { "options": {
"browserTarget": "dspace-angular:build", "buildTarget": "dspace-angular:build",
"port": 4000 "port": 4000
}, },
"configurations": { "configurations": {
"development": { "development": {
"browserTarget": "dspace-angular:build:development" "buildTarget": "dspace-angular:build:development"
}, },
"production": { "production": {
"browserTarget": "dspace-angular:build:production" "buildTarget": "dspace-angular:build:production"
} }
} }
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "dspace-angular:build" "buildTarget": "dspace-angular:build"
} }
}, },
"test": { "test": {
@@ -217,23 +217,23 @@
} }
}, },
"serve-ssr": { "serve-ssr": {
"builder": "@nguniversal/builders:ssr-dev-server", "builder": "@angular-devkit/build-angular:ssr-dev-server",
"options": { "options": {
"browserTarget": "dspace-angular:build", "buildTarget": "dspace-angular:build",
"serverTarget": "dspace-angular:server", "serverTarget": "dspace-angular:server",
"port": 4000 "port": 4000
}, },
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "dspace-angular:build:production", "buildTarget": "dspace-angular:build:production",
"serverTarget": "dspace-angular:server:production" "serverTarget": "dspace-angular:server:production"
} }
} }
}, },
"prerender": { "prerender": {
"builder": "@nguniversal/builders:prerender", "builder": "@angular-devkit/build-angular:prerender",
"options": { "options": {
"browserTarget": "dspace-angular:build:production", "buildTarget": "dspace-angular:build:production",
"serverTarget": "dspace-angular:server:production", "serverTarget": "dspace-angular:server:production",
"routes": [ "routes": [
"/" "/"

View File

@@ -4,10 +4,11 @@
"**/*.ts" "**/*.ts"
], ],
"compilerOptions": { "compilerOptions": {
"sourceMap": false,
"types": [ "types": [
"cypress", "cypress",
"cypress-axe", "cypress-axe",
"node" "node"
] ]
} }
} }

View File

@@ -55,17 +55,18 @@
"ts-node": "10.2.1" "ts-node": "10.2.1"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^16.2.12", "@angular/animations": "^17.3.4",
"@angular/cdk": "^16.2.12", "@angular/cdk": "^17.3.4",
"@angular/common": "^16.2.12", "@angular/common": "^17.3.4",
"@angular/compiler": "^16.2.12", "@angular/compiler": "^17.3.4",
"@angular/core": "^16.2.12", "@angular/core": "^17.3.4",
"@angular/forms": "^16.2.12", "@angular/forms": "^17.3.4",
"@angular/localize": "16.2.12", "@angular/localize": "17.3.4",
"@angular/platform-browser": "^16.2.12", "@angular/platform-browser": "^17.3.4",
"@angular/platform-browser-dynamic": "^16.2.12", "@angular/platform-browser-dynamic": "^17.3.4",
"@angular/platform-server": "^16.2.12", "@angular/platform-server": "^17.3.4",
"@angular/router": "^16.2.12", "@angular/router": "^17.3.4",
"@angular/ssr": "^17.3.0",
"@babel/runtime": "7.21.0", "@babel/runtime": "7.21.0",
"@kolkov/ngx-gallery": "^2.0.1", "@kolkov/ngx-gallery": "^2.0.1",
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
@@ -73,10 +74,9 @@
"@ng-bootstrap/ng-bootstrap": "^11.0.0", "@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@ng-dynamic-forms/core": "^16.0.0", "@ng-dynamic-forms/core": "^16.0.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0", "@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
"@ngrx/effects": "^16.3.0", "@ngrx/effects": "^17.1.1",
"@ngrx/router-store": "^16.3.0", "@ngrx/router-store": "^17.1.1",
"@ngrx/store": "^16.3.0", "@ngrx/store": "^17.1.1",
"@nguniversal/express-engine": "^16.2.0",
"@ngx-translate/core": "^14.0.0", "@ngx-translate/core": "^14.0.0",
"@nicky-lenaers/ngx-scroll-to": "^14.0.0", "@nicky-lenaers/ngx-scroll-to": "^14.0.0",
"@types/grecaptcha": "^3.0.4", "@types/grecaptcha": "^3.0.4",
@@ -130,24 +130,23 @@
"sortablejs": "1.15.0", "sortablejs": "1.15.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"webfontloader": "1.6.28", "webfontloader": "1.6.28",
"zone.js": "~0.13.3" "zone.js": "~0.14.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "~16.0.0", "@angular-builders/custom-webpack": "~17.0.1",
"@angular-devkit/build-angular": "^16.2.12", "@angular-devkit/build-angular": "^17.3.0",
"@angular-eslint/builder": "16.3.1", "@angular-eslint/builder": "17.2.1",
"@angular-eslint/eslint-plugin": "16.3.1", "@angular-eslint/eslint-plugin": "17.2.1",
"@angular-eslint/eslint-plugin-template": "16.3.1", "@angular-eslint/eslint-plugin-template": "17.2.1",
"@angular-eslint/schematics": "16.3.1", "@angular-eslint/schematics": "17.2.1",
"@angular-eslint/template-parser": "16.3.1", "@angular-eslint/template-parser": "17.2.1",
"@angular/cli": "^16.2.12", "@angular/cli": "^17.3.0",
"@angular/compiler-cli": "^16.2.12", "@angular/compiler-cli": "^17.3.4",
"@angular/language-service": "^16.2.12", "@angular/language-service": "^17.3.4",
"@cypress/schematic": "^1.5.0", "@cypress/schematic": "^1.5.0",
"@fortawesome/fontawesome-free": "^6.4.0", "@fortawesome/fontawesome-free": "^6.4.0",
"@ngrx/store-devtools": "^16.3.0", "@ngrx/store-devtools": "^17.1.1",
"@ngtools/webpack": "^16.2.12", "@ngtools/webpack": "^16.2.12",
"@nguniversal/builders": "^16.2.0",
"@types/deep-freeze": "0.1.2", "@types/deep-freeze": "0.1.2",
"@types/ejs": "^3.1.2", "@types/ejs": "^3.1.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
@@ -159,6 +158,7 @@
"@typescript-eslint/eslint-plugin": "^5.59.1", "@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1", "@typescript-eslint/parser": "^5.59.1",
"axe-core": "^4.7.2", "axe-core": "^4.7.2",
"browser-sync": "^3.0.0",
"compression-webpack-plugin": "^9.2.0", "compression-webpack-plugin": "^9.2.0",
"copy-webpack-plugin": "^6.4.1", "copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@@ -200,10 +200,10 @@
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",
"sass-resources-loader": "^2.2.5", "sass-resources-loader": "^2.2.5",
"ts-node": "^8.10.2", "ts-node": "^8.10.2",
"typescript": "~4.9.3", "typescript": "~5.3.3",
"webpack": "5.76.1", "webpack": "5.76.1",
"webpack-bundle-analyzer": "^4.8.0", "webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^4.2.0", "webpack-cli": "^4.2.0",
"webpack-dev-server": "^4.13.3" "webpack-dev-server": "^4.13.3"
} }
} }

150
server.ts
View File

@@ -17,7 +17,6 @@
import 'zone.js/node'; import 'zone.js/node';
import 'reflect-metadata'; import 'reflect-metadata';
import 'rxjs';
/* eslint-disable import/no-namespace */ /* eslint-disable import/no-namespace */
import * as morgan from 'morgan'; import * as morgan from 'morgan';
@@ -39,23 +38,26 @@ import { join } from 'path';
import { enableProdMode } from '@angular/core'; import { enableProdMode } from '@angular/core';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
import { environment } from './src/environments/environment'; import { environment } from './src/environments/environment';
import { createProxyMiddleware } from 'http-proxy-middleware'; import { createProxyMiddleware } from 'http-proxy-middleware';
import { hasNoValue, hasValue } from './src/app/shared/empty.util'; import { hasValue } from './src/app/shared/empty.util';
import { UIServerConfig } from './src/config/ui-server-config.interface'; import { UIServerConfig } from './src/config/ui-server-config.interface';
import bootstrap from './src/main.server'; import bootstrap from './src/main.server';
import { buildAppConfig } from './src/config/config.server'; import { buildAppConfig } from './src/config/config.server';
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface'; import {
APP_CONFIG,
AppConfig,
} from './src/config/app-config.interface';
import { extendEnvironmentWithAppConfig } from './src/config/config.util'; import { extendEnvironmentWithAppConfig } from './src/config/config.util';
import { logStartupMessage } from './startup-message'; import { logStartupMessage } from './startup-message';
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model'; import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
import { CommonEngine } from '@angular/ssr';
import { APP_BASE_HREF } from '@angular/common';
import {
REQUEST,
RESPONSE,
} from './src/express.tokens';
/* /*
* Set path for the browser application's dist folder * Set path for the browser application's dist folder
@@ -127,28 +129,6 @@ export function app() {
*/ */
server.use(json()); server.use(json());
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', (_, options, callback) =>
ngExpressEngine({
bootstrap,
inlineCriticalCss: environment.universal.inlineCriticalCss,
providers: [
{
provide: REQUEST,
useValue: (options as any).req,
},
{
provide: RESPONSE,
useValue: (options as any).req.res,
},
{
provide: APP_CONFIG,
useValue: environment,
},
],
})(_, (options as any), callback),
);
server.engine('ejs', ejs.renderFile); server.engine('ejs', ejs.renderFile);
/* /*
@@ -237,10 +217,10 @@ export function app() {
/* /*
* The callback function to serve server side angular * The callback function to serve server side angular
*/ */
function ngApp(req, res) { function ngApp(req, res, next) {
if (environment.universal.preboot) { if (environment.ssr.enabled) {
// Render the page to user via SSR (server side rendering) // Render the page to user via SSR (server side rendering)
serverSideRender(req, res); serverSideRender(req, res, next);
} else { } else {
// If preboot is disabled, just serve the client // If preboot is disabled, just serve the client
console.log('Universal off, serving for direct client-side rendering (CSR)'); console.log('Universal off, serving for direct client-side rendering (CSR)');
@@ -253,45 +233,66 @@ function ngApp(req, res) {
* returned to the user. * returned to the user.
* @param req current request * @param req current request
* @param res current response * @param res current response
* @param next the next function
* @param sendToUser if true (default), send the rendered content to the user. * @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). * If false, then only save this rendered content to the in-memory cache (to refresh cache).
*/ */
function serverSideRender(req, res, sendToUser: boolean = true) { function serverSideRender(req, res, next, sendToUser: boolean = true) {
const { protocol, originalUrl, baseUrl, headers } = req;
const commonEngine = new CommonEngine({ enablePerformanceProfiler: environment.ssr.enablePerformanceProfiler });
// Render the page via SSR (server side rendering) // Render the page via SSR (server side rendering)
res.render(indexHtml, { commonEngine
req, .render({
res, bootstrap,
preboot: environment.universal.preboot, documentFilePath: indexHtml,
async: environment.universal.async, inlineCriticalCss: environment.ssr.inlineCriticalCss,
time: environment.universal.time, url: `${protocol}://${headers.host}${originalUrl}`,
baseUrl: environment.ui.nameSpace, publicPath: DIST_FOLDER,
originUrl: environment.ui.baseUrl, providers: [
requestUrl: req.originalUrl, { provide: APP_BASE_HREF, useValue: baseUrl },
}, (err, data) => { {
if (hasNoValue(err) && hasValue(data)) { provide: REQUEST,
// save server side rendered page to cache (if any are enabled) useValue: req,
saveToCache(req, data); },
if (sendToUser) { {
res.locals.ssr = true; // mark response as SSR (enables text compression) provide: RESPONSE,
// send rendered page to user useValue: res,
res.send(data); },
{
provide: APP_CONFIG,
useValue: environment,
},
],
})
.then((html) => {
if (hasValue(html)) {
// save server side rendered page to cache (if any are enabled)
saveToCache(req, html);
if (sendToUser) {
res.locals.ssr = true; // mark response as SSR (enables text compression)
// send rendered page to user
res.send(html);
}
} }
} 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 .catch((err) => {
// sent. These errors occur for various reasons in universal, not all of which are in our if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
// control to solve. // When this error occurs we can't fall back to CSR because the response has already been
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client'); // sent. These errors occur for various reasons in universal, not all of which are in our
} else { // control to solve.
console.warn('Error in server-side rendering (SSR)'); console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
if (hasValue(err)) { } else {
console.warn('Error details : ', err); 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);
}
} }
if (sendToUser) { next(err);
console.warn('Falling back to serving direct client-side rendering (CSR).'); });
clientSideRender(req, res);
}
}
});
} }
/** /**
@@ -349,7 +350,7 @@ function initCache() {
function botCacheEnabled(): boolean { function botCacheEnabled(): boolean {
// Caching is only enabled if SSR is enabled AND // Caching is only enabled if SSR is enabled AND
// "max" pages to cache is greater than zero // "max" pages to cache is greater than zero
return environment.universal.preboot && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0); return environment.ssr.enabled && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0);
} }
/** /**
@@ -358,7 +359,7 @@ function botCacheEnabled(): boolean {
function anonymousCacheEnabled(): boolean { function anonymousCacheEnabled(): boolean {
// Caching is only enabled if SSR is enabled AND // Caching is only enabled if SSR is enabled AND
// "max" pages to cache is greater than zero // "max" pages to cache is greater than zero
return environment.universal.preboot && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0); return environment.ssr.enabled && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0);
} }
/** /**
@@ -371,9 +372,9 @@ function cacheCheck(req, res, next) {
// If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page. // 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'))) { if (botCacheEnabled() && isbot(req.get('user-agent'))) {
cachedCopy = checkCacheForRequest('bot', botCache, req, res); cachedCopy = checkCacheForRequest('bot', botCache, req, res, next);
} else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) { } else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) {
cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res); cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res, next);
} }
// If cached copy exists, return it to the user. // If cached copy exists, return it to the user.
@@ -409,9 +410,10 @@ function cacheCheck(req, res, next) {
* @param cache LRU cache to check * @param cache LRU cache to check
* @param req current request to look for in the cache * @param req current request to look for in the cache
* @param res current response * @param res current response
* @param next the next function
* @returns cached copy (if found) or undefined (if not found) * @returns cached copy (if found) or undefined (if not found)
*/ */
function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, res): any { function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, res, next): any {
// Get the cache key for this request // Get the cache key for this request
const key = getCacheKey(req); const key = getCacheKey(req);
@@ -427,7 +429,7 @@ function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, r
// Update cached copy by rerendering server-side // Update cached copy by rerendering server-side
// NOTE: In this scenario the currently cached copy will be returned to the current user. // 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. // This re-render is peformed behind the scenes to update cached copy for next user.
serverSideRender(req, res, false); serverSideRender(req, res, next, false);
} }
} else { } else {
if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); } if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); }
@@ -531,7 +533,7 @@ function createHttpsServer(keys) {
const listener = createServer({ const listener = createServer({
key: keys.serviceKey, key: keys.serviceKey,
cert: keys.certificate, cert: keys.certificate,
}, app).listen(environment.ui.port, environment.ui.host, () => { }, app()).listen(environment.ui.port, environment.ui.host, () => {
serverStarted(); serverStarted();
}); });

View File

@@ -35,7 +35,7 @@
[checked]="isSelected(bitstreamFormat) | async" [checked]="isSelected(bitstreamFormat) | async"
(change)="selectBitStreamFormat(bitstreamFormat, $event)" (change)="selectBitStreamFormat(bitstreamFormat, $event)"
> >
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span> <span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}&#125;</span>
</label> </label>
</td> </td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td> <td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>

View File

@@ -12,7 +12,6 @@ import {
Store, Store,
StoreModule, StoreModule,
} from '@ngrx/store'; } from '@ngrx/store';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { cold } from 'jasmine-marbles'; import { cold } from 'jasmine-marbles';
import { import {
@@ -20,6 +19,7 @@ import {
of as observableOf, of as observableOf,
} from 'rxjs'; } from 'rxjs';
import { REQUEST } from '../../../express.tokens';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';

View File

@@ -9,10 +9,6 @@ import {
select, select,
Store, Store,
} from '@ngrx/store'; } from '@ngrx/store';
import {
REQUEST,
RESPONSE,
} from '@nguniversal/express-engine/tokens';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CookieAttributes } from 'js-cookie'; import { CookieAttributes } from 'js-cookie';
import { import {
@@ -28,6 +24,10 @@ import {
} from 'rxjs/operators'; } from 'rxjs/operators';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import {
REQUEST,
RESPONSE,
} from '../../../express.tokens';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { import {
hasNoValue, hasNoValue,

View File

@@ -4,8 +4,8 @@ import {
HttpTestingController, HttpTestingController,
} from '@angular/common/http/testing'; } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { REQUEST } from '../../../express.tokens';
import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
import { ForwardClientIpInterceptor } from './forward-client-ip.interceptor'; import { ForwardClientIpInterceptor } from './forward-client-ip.interceptor';

View File

@@ -8,9 +8,10 @@ import {
Inject, Inject,
Injectable, Injectable,
} from '@angular/core'; } from '@angular/core';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { REQUEST } from '../../../express.tokens';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
/** /**
* Http Interceptor intercepting Http Requests, adding the client's IP to their X-Forwarded-For header * Http Interceptor intercepting Http Requests, adding the client's IP to their X-Forwarded-For header

View File

@@ -3,7 +3,6 @@ import {
Inject, Inject,
Injectable, Injectable,
} from '@angular/core'; } from '@angular/core';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
combineLatest, combineLatest,
@@ -16,6 +15,7 @@ import {
take, take,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { REQUEST } from '../../../express.tokens';
import { import {
hasValue, hasValue,
isEmpty, isEmpty,

View File

@@ -2,8 +2,8 @@ import {
TestBed, TestBed,
waitForAsync, waitForAsync,
} from '@angular/core/testing'; } from '@angular/core/testing';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { REQUEST } from '../../../express.tokens';
import { import {
CookieService, CookieService,
ICookieService, ICookieService,

View File

@@ -2,13 +2,14 @@ import {
Inject, Inject,
Injectable, Injectable,
} from '@angular/core'; } from '@angular/core';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { CookieAttributes } from 'js-cookie'; import { CookieAttributes } from 'js-cookie';
import { import {
Observable, Observable,
Subject, Subject,
} from 'rxjs'; } from 'rxjs';
import { REQUEST } from '../../../express.tokens';
export interface ICookieService { export interface ICookieService {
readonly cookies$: Observable<{ readonly [key: string]: any }>; readonly cookies$: Observable<{ readonly [key: string]: any }>;

View File

@@ -2,15 +2,15 @@ import {
Inject, Inject,
Injectable, Injectable,
} from '@angular/core'; } from '@angular/core';
import {
REQUEST,
RESPONSE,
} from '@nguniversal/express-engine/tokens';
import { import {
Request, Request,
Response, Response,
} from 'express'; } from 'express';
import {
REQUEST,
RESPONSE,
} from '../../../express.tokens';
import { HardRedirectService } from './hard-redirect.service'; import { HardRedirectService } from './hard-redirect.service';
/** /**

View File

@@ -3,9 +3,10 @@ import {
Injectable, Injectable,
Optional, Optional,
} from '@angular/core'; } from '@angular/core';
import { RESPONSE } from '@nguniversal/express-engine/tokens';
import { Response } from 'express'; import { Response } from 'express';
import { RESPONSE } from '../../../express.tokens';
/** /**
* Service responsible to provide method to manage the response object * Service responsible to provide method to manage the response object
*/ */

View File

@@ -2,12 +2,12 @@ import {
Inject, Inject,
Injectable, Injectable,
} from '@angular/core'; } from '@angular/core';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { import {
Observable, Observable,
of as observableOf, of as observableOf,
} from 'rxjs'; } from 'rxjs';
import { REQUEST } from '../../../express.tokens';
import { ReferrerService } from './referrer.service'; import { ReferrerService } from './referrer.service';
/** /**

View File

@@ -48,7 +48,7 @@ export const DEBOUNCE_TIME_OPERATOR = new InjectionToken<<T>(dueTime: number) =>
export const getRemoteDataPayload = <T>() => export const getRemoteDataPayload = <T>() =>
(source: Observable<RemoteData<T>>): Observable<T> => (source: Observable<RemoteData<T>>): Observable<T> =>
source.pipe(map((remoteData: RemoteData<T>) => remoteData.payload)); source.pipe(map((remoteData: RemoteData<T>) => remoteData?.payload));
export const getPaginatedListPayload = <T>() => export const getPaginatedListPayload = <T>() =>
(source: Observable<PaginatedList<T>>): Observable<T[]> => (source: Observable<PaginatedList<T>>): Observable<T[]> =>

View File

@@ -13,7 +13,6 @@ import {
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
Observable, Observable,
@@ -21,6 +20,7 @@ import {
} from 'rxjs'; } from 'rxjs';
import { APP_CONFIG } from '../../../../../../config/app-config.interface'; import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { REQUEST } from '../../../../../../express.tokens';
import { AuthService } from '../../../../../core/auth/auth.service'; import { AuthService } from '../../../../../core/auth/auth.service';
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../../../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../../../core/cache/object-cache.service';

View File

@@ -13,7 +13,6 @@ import {
Router, Router,
} from '@angular/router'; } from '@angular/router';
import { provideMockStore } from '@ngrx/store/testing'; import { provideMockStore } from '@ngrx/store/testing';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { AuthRequestService } from 'src/app/core/auth/auth-request.service'; import { AuthRequestService } from 'src/app/core/auth/auth-request.service';
@@ -23,6 +22,7 @@ import { ActivatedRouteStub } from 'src/app/shared/testing/active-router.stub';
import { AuthRequestServiceStub } from 'src/app/shared/testing/auth-request-service.stub'; import { AuthRequestServiceStub } from 'src/app/shared/testing/auth-request-service.stub';
import { APP_CONFIG } from '../../../../../config/app-config.interface'; import { APP_CONFIG } from '../../../../../config/app-config.interface';
import { REQUEST } from '../../../../../express.tokens';
import { LinkService } from '../../../../core/cache/builders/link.service'; import { LinkService } from '../../../../core/cache/builders/link.service';
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model';

View File

@@ -0,0 +1,53 @@
<div class="left-column">
<span *ngIf="(workspaceId$ | async) || (workflowId$ | async); then versionNumberWithoutLink else versionNumberWithLink"></span>
<ng-template #versionNumberWithLink>
<a [routerLink]="getVersionRoute(version.id)">{{version.version}}</a>
</ng-template>
<ng-template #versionNumberWithoutLink>
{{version.version}}
</ng-template>
<span *ngIf="version?.id === itemVersion?.id">*</span>
<span *ngIf="workspaceId$ | async" class="text-light badge badge-primary ml-3">
{{ "item.version.history.table.workspaceItem" | translate }}
</span>
<span *ngIf="workflowId$ | async" class="text-light badge badge-info ml-3">
{{ "item.version.history.table.workflowItem" | translate }}
</span>
</div>
<div class="right-column">
<div class="btn-group edit-field space-children-mr" *ngIf="displayActions">
<!--EDIT WORKSPACE ITEM-->
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
*ngIf="workspaceId$ | async"
(click)="editWorkspaceItem(workspaceId$)"
title="{{'item.version.history.table.action.editWorkspaceItem' | translate }}">
<i class="fas fa-pencil-alt fa-fw"></i>
</button>
<!--CREATE-->
<ng-container *ngIf="canCreateVersion$ | async">
<button class="btn btn-outline-primary btn-sm version-row-element-create"
[disabled]="isAnyBeingEdited() || hasDraftVersion"
(click)="createNewVersion(version)"
title="{{createVersionTitle | translate }}">
<i class="fas fa-code-branch fa-fw"></i>
</button>
</ng-container>
<!--DELETE-->
<ng-container *ngIf="canDeleteVersion$ | async">
<button class="btn btn-sm version-row-element-delete"
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
[disabled]="isAnyBeingEdited()"
(click)="deleteVersion(version, version.id === itemVersion.id)"
title="{{'item.version.history.table.action.deleteVersion' | translate}}">
<i class="fas fa-trash fa-fw"></i>
</button>
</ng-container>
</div>
</div>

View File

@@ -0,0 +1,9 @@
.left-column {
float: left;
text-align: left;
}
.right-column {
float: right;
text-align: right;
}

View File

@@ -0,0 +1,188 @@
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
ComponentFixture,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import {
FormsModule,
ReactiveFormsModule,
} from '@angular/forms';
import {
BrowserModule,
By,
} from '@angular/platform-browser';
import {
ActivatedRoute,
RouterModule,
} from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import {
EMPTY,
of as observableOf,
of,
} from 'rxjs';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { ItemDataService } from '../../../core/data/item-data.service';
import { VersionDataService } from '../../../core/data/version-data.service';
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
import { Item } from '../../../core/shared/item.model';
import { Version } from '../../../core/shared/version.model';
import { VersionHistory } from '../../../core/shared/version-history.model';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { createPaginatedList } from '../../../shared/testing/utils.test';
import { ItemVersionsComponent } from '../item-versions.component';
import { ItemVersionsRowElementVersionComponent } from './item-versions-row-element-version.component';
describe('ItemVersionsRowElementVersionComponent', () => {
let component: ItemVersionsRowElementVersionComponent;
let fixture: ComponentFixture<ItemVersionsRowElementVersionComponent>;
const versionHistory = Object.assign(new VersionHistory(), {
id: '1',
draftVersion: true,
});
const version = Object.assign(new Version(), {
id: '1',
version: 1,
created: new Date(2020, 1, 1),
summary: 'first version',
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
_links: {
self: {
href: 'version2-url',
},
},
});
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList([version]));
const item = Object.assign(new Item(), { // is a workspace item
id: 'item-identifier-1',
uuid: 'item-identifier-1',
handle: '123456789/1',
version: createSuccessfulRemoteDataObject$(version),
_links: {
self: {
href: '/items/item-identifier-1',
},
},
});
version.item = createSuccessfulRemoteDataObject$(item);
const versionHistoryServiceSpy = jasmine.createSpyObj('versionHistoryService', {
getVersions: createSuccessfulRemoteDataObject$(createPaginatedList([version])),
getVersionHistoryFromVersion$: of(versionHistory),
getLatestVersionItemFromHistory$: of(item),
});
const authorizationServiceSpy = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true),
});
const workspaceItemDataServiceSpy = jasmine.createSpyObj('workspaceItemDataService', {
findByItem: EMPTY,
});
const workflowItemDataServiceSpy = jasmine.createSpyObj('workflowItemDataService', {
findByItem: EMPTY,
});
const versionServiceSpy = jasmine.createSpyObj('versionService', {
findById: EMPTY,
});
const itemDataServiceSpy = jasmine.createSpyObj('itemDataService', {
delete: createSuccessfulRemoteDataObject$({}),
});
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterModule.forRoot([
{ path: 'items/:id/edit/versionhistory', component: {} as any },
]), CommonModule, FormsModule, ReactiveFormsModule, BrowserModule, ItemVersionsComponent],
providers: [
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: AuthorizationDataService, useValue: authorizationServiceSpy },
{ provide: VersionHistoryDataService, useValue: versionHistoryServiceSpy },
{ provide: ItemDataService, useValue: itemDataServiceSpy },
{ provide: VersionDataService, useValue: versionServiceSpy },
{ provide: WorkspaceitemDataService, useValue: workspaceItemDataServiceSpy },
{ provide: WorkflowItemDataService, useValue: workflowItemDataServiceSpy },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
],
schemas: [NO_ERRORS_SCHEMA],
})
.compileComponents();
fixture = TestBed.createComponent(ItemVersionsRowElementVersionComponent);
component = fixture.componentInstance;
component.version = version;
component.itemVersion = version;
component.item = item;
component.displayActions = true;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it(`should display version ${version.version} in the correct column for version ${version.id}`, () => {
const id = fixture.debugElement.query(By.css(`.left-column`));
expect(id.nativeElement.textContent).toContain(version.version.toString());
});
it(`should displau an asterisk in the correct column for current version`, () => {
const draft = fixture.debugElement.query(By.css(`.left-column`));
expect(draft.nativeElement.textContent).toContain('*');
});
it('should display action buttons in the correct column if displayActions is true', () => {
fixture.detectChanges();
const actions = fixture.debugElement.query(By.css(`.right-column`));
expect(actions).toBeTruthy();
});
describe('when deleting a version', () => {
let deleteButton;
beforeEach(() => {
deleteButton = fixture.debugElement.queryAll(By.css('.version-row-element-delete'))[0].nativeElement;
itemDataServiceSpy.delete.calls.reset();
});
describe('if confirmed via modal', () => {
beforeEach(waitForAsync(() => {
deleteButton.click();
fixture.detectChanges();
(document as any).querySelector('.modal-footer .confirm').click();
}));
it('should call ItemService.delete', () => {
expect(itemDataServiceSpy.delete).toHaveBeenCalledWith(item.id);
});
});
describe('if canceled via modal', () => {
beforeEach(waitForAsync(() => {
deleteButton.click();
fixture.detectChanges();
(document as any).querySelector('.modal-footer .cancel').click();
}));
it('should not call ItemService.delete', () => {
expect(itemDataServiceSpy.delete).not.toHaveBeenCalled();
});
});
});
});

View File

@@ -0,0 +1,302 @@
import {
AsyncPipe,
NgClass,
NgIf,
} from '@angular/common';
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import {
Router,
RouterLink,
} from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {
combineLatest,
concatMap,
Observable,
of,
} from 'rxjs';
import {
map,
mergeMap,
switchMap,
take,
tap,
} from 'rxjs/operators';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { ItemDataService } from '../../../core/data/item-data.service';
import { RemoteData } from '../../../core/data/remote-data';
import { VersionDataService } from '../../../core/data/version-data.service';
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
import { Item } from '../../../core/shared/item.model';
import {
getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload,
} from '../../../core/shared/operators';
import { Version } from '../../../core/shared/version.model';
import { VersionHistory } from '../../../core/shared/version-history.model';
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import {
getItemEditVersionhistoryRoute,
getItemVersionRoute,
} from '../../item-page-routing-paths';
import { ItemVersionsDeleteModalComponent } from '../item-versions-delete-modal/item-versions-delete-modal.component';
import { ItemVersionsSharedService } from '../item-versions-shared.service';
import { ItemVersionsSummaryModalComponent } from '../item-versions-summary-modal/item-versions-summary-modal.component';
@Component({
selector: 'ds-item-versions-row-element-version',
standalone: true,
imports: [
AsyncPipe,
RouterLink,
TranslateModule,
NgClass,
NgIf,
],
templateUrl: './item-versions-row-element-version.component.html',
styleUrl: './item-versions-row-element-version.component.scss',
})
export class ItemVersionsRowElementVersionComponent implements OnInit {
@Input() hasDraftVersion: boolean | null;
@Input() version: Version;
@Input() itemVersion: Version;
@Input() item: Item;
@Input() displayActions: boolean;
@Input() versionBeingEditedNumber: number;
@Output() versionsHistoryChange = new EventEmitter<Observable<VersionHistory>>();
workspaceId$: Observable<string>;
workflowId$: Observable<string>;
canDeleteVersion$: Observable<boolean>;
canCreateVersion$: Observable<boolean>;
createVersionTitle: string;
constructor(
private workspaceItemDataService: WorkspaceitemDataService,
private workflowItemDataService: WorkflowItemDataService,
private router: Router,
private itemService: ItemDataService,
private authorizationService: AuthorizationDataService,
private itemVersionShared: ItemVersionsSharedService,
private versionHistoryService: VersionHistoryDataService,
private versionService: VersionDataService,
private notificationsService: NotificationsService,
private translateService: TranslateService,
private modalService: NgbModal,
) {
}
ngOnInit(): void {
this.workspaceId$ = this.getWorkspaceId(this.version.item);
this.workflowId$ = this.getWorkflowId(this.version.item);
this.canDeleteVersion$ = this.canDeleteVersion(this.version);
this.canCreateVersion$ = this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, this.item.self);
this.createVersionTitle = this.hasDraftVersion ? 'item.version.history.table.action.hasDraft' : 'item.version.history.table.action.newVersion';
}
/**
* Get the ID of the workspace item, if present, otherwise return undefined
* @param versionItem the item for which retrieve the workspace item id
*/
getWorkspaceId(versionItem: Observable<RemoteData<Item>>): Observable<string> {
if (!this.hasDraftVersion) {
return of(undefined);
}
return versionItem.pipe(
getFirstSucceededRemoteDataPayload(),
map((item: Item) => item.uuid),
switchMap((itemUuid: string) => this.workspaceItemDataService.findByItem(itemUuid, true)),
getFirstCompletedRemoteData<WorkspaceItem>(),
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id),
);
}
/**
* Get the ID of the workflow item, if present, otherwise return undefined
* @param versionItem the item for which retrieve the workspace item id
*/
getWorkflowId(versionItem: Observable<RemoteData<Item>>): Observable<string> {
return this.getWorkspaceId(versionItem).pipe(
concatMap((workspaceId: string) => {
if (workspaceId) {
return of(undefined);
}
return versionItem.pipe(
getFirstSucceededRemoteDataPayload(),
map((item: Item) => item.uuid),
switchMap((itemUuid: string) => this.workflowItemDataService.findByItem(itemUuid, true)),
getFirstCompletedRemoteData<WorkspaceItem>(),
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id),
);
}),
);
}
/**
* redirect to the edit page of the workspace item
* @param id$ the id of the workspace item
*/
editWorkspaceItem(id$: Observable<string>) {
id$.subscribe((id) => {
void this.router.navigateByUrl('workspaceitems/' + id + '/edit');
});
}
/**
* Check if the current user can delete the version
* @param version
*/
canDeleteVersion(version: Version): Observable<boolean> {
return this.authorizationService.isAuthorized(FeatureID.CanDeleteVersion, version.self);
}
/**
* Get the route to the specified version
* @param versionId the ID of the version for which the route will be retrieved
*/
getVersionRoute(versionId: string) {
return getItemVersionRoute(versionId);
}
/**
* Creates a new version starting from the specified one
* @param version the version from which a new one will be created
*/
createNewVersion(version: Version) {
const versionNumber = version.version;
// Open modal and set current version number
const activeModal = this.modalService.open(ItemVersionsSummaryModalComponent);
activeModal.componentInstance.versionNumber = versionNumber;
// On createVersionEvent emitted create new version and notify
activeModal.componentInstance.createVersionEvent.pipe(
mergeMap((summary: string) => combineLatest([
of(summary),
version.item.pipe(getFirstSucceededRemoteDataPayload()),
])),
mergeMap(([summary, item]: [string, Item]) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
getFirstCompletedRemoteData(),
// close model (should be displaying loading/waiting indicator) when version creation failed/succeeded
tap(() => activeModal.close()),
// show success/failure notification
tap((newVersionRD: RemoteData<Version>) => {
this.itemVersionShared.notifyCreateNewVersion(newVersionRD);
if (newVersionRD.hasSucceeded) {
const versionHistory$ = this.versionService.getHistoryFromVersion(version).pipe(
tap((versionHistory: VersionHistory) => {
this.itemService.invalidateItemCache(this.item.uuid);
this.versionHistoryService.invalidateVersionHistoryCache(versionHistory.id);
}),
);
this.versionsHistoryChange.emit(versionHistory$);
}
}),
// get workspace item
getFirstSucceededRemoteDataPayload<Version>(),
switchMap((newVersion: Version) => this.itemService.findByHref(newVersion._links.item.href)),
getFirstSucceededRemoteDataPayload<Item>(),
switchMap((newVersionItem: Item) => this.workspaceItemDataService.findByItem(newVersionItem.uuid, true, false)),
getFirstSucceededRemoteDataPayload<WorkspaceItem>(),
).subscribe((wsItem) => {
const wsiId = wsItem.id;
const route = 'workspaceitems/' + wsiId + '/edit';
this.router.navigateByUrl(route);
});
}
/**
* Deletes the specified version, notify the success/failure and redirect to latest version
* @param version the version to be deleted
* @param redirectToLatest force the redirect to the latest version in the history
*/
deleteVersion(version: Version, redirectToLatest: boolean): void {
const successMessageKey = 'item.version.delete.notification.success';
const failureMessageKey = 'item.version.delete.notification.failure';
const versionNumber = version.version;
const versionItem$ = version.item;
// Open modal
const activeModal = this.modalService.open(ItemVersionsDeleteModalComponent);
activeModal.componentInstance.versionNumber = version.version;
activeModal.componentInstance.firstVersion = false;
// On modal submit/dismiss
activeModal.componentInstance.response.pipe(take(1)).subscribe((ok) => {
if (ok) {
versionItem$.pipe(
getFirstSucceededRemoteDataPayload<Item>(),
// Retrieve version history
mergeMap((item: Item) => combineLatest([
of(item),
this.versionHistoryService.getVersionHistoryFromVersion$(version),
])),
// Delete item
mergeMap(([item, versionHistory]: [Item, VersionHistory]) => combineLatest([
this.deleteItemAndGetResult$(item),
of(versionHistory),
])),
// Retrieve new latest version
mergeMap(([deleteItemResult, versionHistory]: [boolean, VersionHistory]) => combineLatest([
of(deleteItemResult),
this.versionHistoryService.getLatestVersionItemFromHistory$(versionHistory).pipe(
tap(() => {
this.versionsHistoryChange.emit(of(versionHistory));
}),
),
])),
).subscribe(([deleteHasSucceeded, newLatestVersionItem]: [boolean, Item]) => {
// Notify operation result and redirect to latest item
if (deleteHasSucceeded) {
this.notificationsService.success(null, this.translateService.get(successMessageKey, { 'version': versionNumber }));
} else {
this.notificationsService.error(null, this.translateService.get(failureMessageKey, { 'version': versionNumber }));
}
if (redirectToLatest) {
const path = getItemEditVersionhistoryRoute(newLatestVersionItem);
this.router.navigateByUrl(path);
}
});
}
});
}
/**
* Delete the item and get the result of the operation
* @param item
*/
deleteItemAndGetResult$(item: Item): Observable<boolean> {
return this.itemService.delete(item.id).pipe(
getFirstCompletedRemoteData(),
map((deleteItemRes) => deleteItemRes.hasSucceeded),
take(1),
);
}
/**
* True when a version is being edited
* (used to disable buttons for other versions)
*/
isAnyBeingEdited(): boolean {
return this.versionBeingEditedNumber != null;
}
}

View File

@@ -17,7 +17,7 @@
<thead> <thead>
<tr> <tr>
<th scope="col">{{"item.version.history.table.version" | translate}}</th> <th scope="col">{{"item.version.history.table.version" | translate}}</th>
<th scope="col" *ngIf="(showSubmitter() | async)">{{"item.version.history.table.editor" | translate}}</th> <th scope="col" *ngIf="(showSubmitter$ | async)">{{"item.version.history.table.editor" | translate}}</th>
<th scope="col">{{"item.version.history.table.date" | translate}}</th> <th scope="col">{{"item.version.history.table.date" | translate}}</th>
<th scope="col">{{"item.version.history.table.summary" | translate}}</th> <th scope="col">{{"item.version.history.table.summary" | translate}}</th>
</tr> </tr>
@@ -25,69 +25,15 @@
<tbody> <tbody>
<tr *ngFor="let version of versions?.page" [id]="'version-row-' + version.id"> <tr *ngFor="let version of versions?.page" [id]="'version-row-' + version.id">
<td class="version-row-element-version"> <td class="version-row-element-version">
<!-- Get the ID of the workspace/workflow item (`undefined` if they don't exist). <ds-item-versions-row-element-version [hasDraftVersion]="hasDraftVersion$ | async"
Conditionals inside *ngVar are needed in order to avoid useless calls. --> [version]="version"
<ng-container *ngVar="((hasDraftVersion$ | async) ? getWorkspaceId(version?.item) : undefined) as workspaceId$"> [item]="item" [displayActions]="displayActions"
<ng-container *ngVar=" ((workspaceId$ | async) ? undefined : getWorkflowId(version?.item)) as workflowId$"> [itemVersion]="itemVersion"
[versionBeingEditedNumber]="versionBeingEditedNumber"
<div class="left-column"> (versionsHistoryChange)="getAllVersions($event)"
></ds-item-versions-row-element-version>
<span *ngIf="(workspaceId$ | async) || (workflowId$ | async); then versionNumberWithoutLink else versionNumberWithLink"></span>
<ng-template #versionNumberWithLink>
<a [routerLink]="getVersionRoute(version.id)">{{version.version}}</a>
</ng-template>
<ng-template #versionNumberWithoutLink>
{{version.version}}
</ng-template>
<span *ngIf="version?.id === itemVersion?.id">*</span>
<span *ngIf="workspaceId$ | async" class="text-light badge badge-primary ml-3">
{{ "item.version.history.table.workspaceItem" | translate }}
</span>
<span *ngIf="workflowId$ | async" class="text-light badge badge-info ml-3">
{{ "item.version.history.table.workflowItem" | translate }}
</span>
</div>
<div class="right-column">
<div class="btn-group edit-field space-children-mr" *ngIf="displayActions">
<!--EDIT WORKSPACE ITEM-->
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
*ngIf="workspaceId$ | async"
(click)="editWorkspaceItem(workspaceId$)"
title="{{'item.version.history.table.action.editWorkspaceItem' | translate }}">
<i class="fas fa-pencil-alt fa-fw"></i>
</button>
<!--CREATE-->
<ng-container *ngIf="canCreateVersion$ | async">
<button class="btn btn-outline-primary btn-sm version-row-element-create"
[disabled]="isAnyBeingEdited() || (hasDraftVersion$ | async)"
(click)="createNewVersion(version)"
title="{{createVersionTitle$ | async | translate }}">
<i class="fas fa-code-branch fa-fw"></i>
</button>
</ng-container>
<!--DELETE-->
<ng-container *ngIf="canDeleteVersion$(version) | async">
<button class="btn btn-sm version-row-element-delete"
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
[disabled]="isAnyBeingEdited()"
(click)="deleteVersion(version, version.id===itemVersion.id)"
title="{{'item.version.history.table.action.deleteVersion' | translate}}">
<i class="fas fa-trash fa-fw"></i>
</button>
</ng-container>
</div>
</div>
</ng-container>
</ng-container>
</td> </td>
<td class="version-row-element-editor" *ngIf="(showSubmitter() | async)"> <td class="version-row-element-editor" *ngIf="(showSubmitter$ | async)">
{{version?.submitterName}} {{version?.submitterName}}
</td> </td>
<td class="version-row-element-date"> <td class="version-row-element-date">

View File

@@ -1,9 +0,0 @@
.left-column {
float: left;
text-align: left;
}
.right-column {
float: right;
text-align: right;
}

View File

@@ -206,19 +206,6 @@ describe('ItemVersionsComponent', () => {
versions.forEach((version: Version, index: number) => { versions.forEach((version: Version, index: number) => {
const versionItem = items[index]; const versionItem = items[index];
it(`should display version ${version.version} in the correct column for version ${version.id}`, () => {
const id = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-version`));
expect(id.nativeElement.textContent).toContain(version.version.toString());
});
// Check if the current version contains an asterisk
if (item1.uuid === versionItem.uuid) {
it('should add an asterisk to the version of the selected item', () => {
const item = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-version`));
expect(item.nativeElement.textContent).toContain('*');
});
}
it(`should display date ${version.created} in the correct column for version ${version.id}`, () => { it(`should display date ${version.created} in the correct column for version ${version.id}`, () => {
const date = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-date`)); const date = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-date`));
switch (versionItem.uuid) { switch (versionItem.uuid) {
@@ -319,44 +306,4 @@ describe('ItemVersionsComponent', () => {
expect(component.isThisBeingEdited(version2)).toBeFalse(); expect(component.isThisBeingEdited(version2)).toBeFalse();
}); });
}); });
describe('when deleting a version', () => {
let deleteButton;
beforeEach(() => {
const canDelete = (featureID: FeatureID, url: string ) => of(featureID === FeatureID.CanDeleteVersion);
authorizationServiceSpy.isAuthorized.and.callFake(canDelete);
fixture.detectChanges();
// delete the last version in the table (version2 → item2)
deleteButton = fixture.debugElement.queryAll(By.css('.version-row-element-delete'))[1].nativeElement;
itemDataServiceSpy.delete.calls.reset();
});
describe('if confirmed via modal', () => {
beforeEach(waitForAsync(() => {
deleteButton.click();
fixture.detectChanges();
(document as any).querySelector('.modal-footer .confirm').click();
}));
it('should call ItemService.delete', () => {
expect(itemDataServiceSpy.delete).toHaveBeenCalledWith(item2.id);
});
});
describe('if canceled via modal', () => {
beforeEach(waitForAsync(() => {
deleteButton.click();
fixture.detectChanges();
(document as any).querySelector('.modal-footer .cancel').click();
}));
it('should not call ItemService.delete', () => {
expect(itemDataServiceSpy.delete).not.toHaveBeenCalled();
});
});
});
}); });

View File

@@ -11,15 +11,8 @@ import {
OnDestroy, OnDestroy,
OnInit, OnInit,
} from '@angular/core'; } from '@angular/core';
import { import { FormsModule } from '@angular/forms';
FormsModule, import { RouterLink } from '@angular/router';
UntypedFormBuilder,
} from '@angular/forms';
import {
Router,
RouterLink,
} from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { import {
TranslateModule, TranslateModule,
TranslateService, TranslateService,
@@ -28,22 +21,18 @@ import {
BehaviorSubject, BehaviorSubject,
combineLatest, combineLatest,
Observable, Observable,
of,
Subscription, Subscription,
} from 'rxjs'; } from 'rxjs';
import { import {
map, map,
mergeMap,
startWith, startWith,
switchMap, switchMap,
take, take,
tap,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { ItemDataService } from '../../core/data/item-data.service';
import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { VersionDataService } from '../../core/data/version-data.service'; import { VersionDataService } from '../../core/data/version-data.service';
@@ -60,9 +49,6 @@ import {
} from '../../core/shared/operators'; } from '../../core/shared/operators';
import { Version } from '../../core/shared/version.model'; import { Version } from '../../core/shared/version.model';
import { VersionHistory } from '../../core/shared/version-history.model'; import { VersionHistory } from '../../core/shared/version-history.model';
import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model';
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service';
import { AlertComponent } from '../../shared/alert/alert.component'; import { AlertComponent } from '../../shared/alert/alert.component';
import { AlertType } from '../../shared/alert/alert-type'; import { AlertType } from '../../shared/alert/alert-type';
import { import {
@@ -75,21 +61,15 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
import { followLink } from '../../shared/utils/follow-link-config.model'; import { followLink } from '../../shared/utils/follow-link-config.model';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { import { getItemPageRoute } from '../item-page-routing-paths';
getItemEditVersionhistoryRoute, import { ItemVersionsRowElementVersionComponent } from './item-versions-row-element-version/item-versions-row-element-version.component';
getItemPageRoute,
getItemVersionRoute,
} from '../item-page-routing-paths';
import { ItemVersionsDeleteModalComponent } from './item-versions-delete-modal/item-versions-delete-modal.component';
import { ItemVersionsSharedService } from './item-versions-shared.service';
import { ItemVersionsSummaryModalComponent } from './item-versions-summary-modal/item-versions-summary-modal.component';
@Component({ @Component({
selector: 'ds-item-versions', selector: 'ds-item-versions',
templateUrl: './item-versions.component.html', templateUrl: './item-versions.component.html',
styleUrls: ['./item-versions.component.scss'], styleUrls: ['./item-versions.component.scss'],
standalone: true, standalone: true,
imports: [VarDirective, NgIf, AlertComponent, PaginationComponent, NgFor, RouterLink, NgClass, FormsModule, AsyncPipe, DatePipe, TranslateModule], imports: [VarDirective, NgIf, AlertComponent, PaginationComponent, NgFor, RouterLink, NgClass, FormsModule, AsyncPipe, DatePipe, TranslateModule, ItemVersionsRowElementVersionComponent],
}) })
/** /**
@@ -162,6 +142,11 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
*/ */
hasDraftVersion$: Observable<boolean>; hasDraftVersion$: Observable<boolean>;
/**
* Show submitter in version history table
*/
showSubmitter$: Observable<boolean> = this.showSubmitter();
/** /**
* The amount of versions to display per page * The amount of versions to display per page
*/ */
@@ -206,17 +191,10 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
constructor(private versionHistoryService: VersionHistoryDataService, constructor(private versionHistoryService: VersionHistoryDataService,
private versionService: VersionDataService, private versionService: VersionDataService,
private itemService: ItemDataService,
private paginationService: PaginationService, private paginationService: PaginationService,
private formBuilder: UntypedFormBuilder,
private modalService: NgbModal,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private translateService: TranslateService, private translateService: TranslateService,
private router: Router,
private itemVersionShared: ItemVersionsSharedService,
private authorizationService: AuthorizationDataService, private authorizationService: AuthorizationDataService,
private workspaceItemDataService: WorkspaceitemDataService,
private workflowItemDataService: WorkflowItemDataService,
private configurationService: ConfigurationDataService, private configurationService: ConfigurationDataService,
) { ) {
} }
@@ -255,14 +233,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
this.versionBeingEditedId = undefined; this.versionBeingEditedId = undefined;
} }
/**
* Get the route to the specified version
* @param versionId the ID of the version for which the route will be retrieved
*/
getVersionRoute(versionId: string) {
return getItemVersionRoute(versionId);
}
/** /**
* Applies changes to version currently being edited * Applies changes to version currently being edited
*/ */
@@ -291,121 +261,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
); );
} }
/**
* Delete the item and get the result of the operation
* @param item
*/
deleteItemAndGetResult$(item: Item): Observable<boolean> {
return this.itemService.delete(item.id).pipe(
getFirstCompletedRemoteData(),
map((deleteItemRes) => deleteItemRes.hasSucceeded),
take(1),
);
}
/**
* Deletes the specified version, notify the success/failure and redirect to latest version
* @param version the version to be deleted
* @param redirectToLatest force the redirect to the latest version in the history
*/
deleteVersion(version: Version, redirectToLatest: boolean): void {
const successMessageKey = 'item.version.delete.notification.success';
const failureMessageKey = 'item.version.delete.notification.failure';
const versionNumber = version.version;
const versionItem$ = version.item;
// Open modal
const activeModal = this.modalService.open(ItemVersionsDeleteModalComponent);
activeModal.componentInstance.versionNumber = version.version;
activeModal.componentInstance.firstVersion = false;
// On modal submit/dismiss
activeModal.componentInstance.response.pipe(take(1)).subscribe((ok) => {
if (ok) {
versionItem$.pipe(
getFirstSucceededRemoteDataPayload<Item>(),
// Retrieve version history
mergeMap((item: Item) => combineLatest([
of(item),
this.versionHistoryService.getVersionHistoryFromVersion$(version),
])),
// Delete item
mergeMap(([item, versionHistory]: [Item, VersionHistory]) => combineLatest([
this.deleteItemAndGetResult$(item),
of(versionHistory),
])),
// Retrieve new latest version
mergeMap(([deleteItemResult, versionHistory]: [boolean, VersionHistory]) => combineLatest([
of(deleteItemResult),
this.versionHistoryService.getLatestVersionItemFromHistory$(versionHistory).pipe(
tap(() => {
this.getAllVersions(of(versionHistory));
}),
),
])),
).subscribe(([deleteHasSucceeded, newLatestVersionItem]: [boolean, Item]) => {
// Notify operation result and redirect to latest item
if (deleteHasSucceeded) {
this.notificationsService.success(null, this.translateService.get(successMessageKey, { 'version': versionNumber }));
} else {
this.notificationsService.error(null, this.translateService.get(failureMessageKey, { 'version': versionNumber }));
}
if (redirectToLatest) {
const path = getItemEditVersionhistoryRoute(newLatestVersionItem);
this.router.navigateByUrl(path);
}
});
}
});
}
/**
* Creates a new version starting from the specified one
* @param version the version from which a new one will be created
*/
createNewVersion(version: Version) {
const versionNumber = version.version;
// Open modal and set current version number
const activeModal = this.modalService.open(ItemVersionsSummaryModalComponent);
activeModal.componentInstance.versionNumber = versionNumber;
// On createVersionEvent emitted create new version and notify
activeModal.componentInstance.createVersionEvent.pipe(
mergeMap((summary: string) => combineLatest([
of(summary),
version.item.pipe(getFirstSucceededRemoteDataPayload()),
])),
mergeMap(([summary, item]: [string, Item]) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
getFirstCompletedRemoteData(),
// close model (should be displaying loading/waiting indicator) when version creation failed/succeeded
tap(() => activeModal.close()),
// show success/failure notification
tap((newVersionRD: RemoteData<Version>) => {
this.itemVersionShared.notifyCreateNewVersion(newVersionRD);
if (newVersionRD.hasSucceeded) {
const versionHistory$ = this.versionService.getHistoryFromVersion(version).pipe(
tap((versionHistory: VersionHistory) => {
this.itemService.invalidateItemCache(this.item.uuid);
this.versionHistoryService.invalidateVersionHistoryCache(versionHistory.id);
}),
);
this.getAllVersions(versionHistory$);
}
}),
// get workspace item
getFirstSucceededRemoteDataPayload<Version>(),
switchMap((newVersion: Version) => this.itemService.findByHref(newVersion._links.item.href)),
getFirstSucceededRemoteDataPayload<Item>(),
switchMap((newVersionItem: Item) => this.workspaceItemDataService.findByItem(newVersionItem.uuid, true, false)),
getFirstSucceededRemoteDataPayload<WorkspaceItem>(),
).subscribe((wsItem) => {
const wsiId = wsItem.id;
const route = 'workspaceitems/' + wsiId + '/edit';
this.router.navigateByUrl(route);
});
}
/** /**
* Check is the current user can edit the version summary * Check is the current user can edit the version summary
* @param version * @param version
@@ -444,14 +299,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
} }
/**
* Check if the current user can delete the version
* @param version
*/
canDeleteVersion$(version: Version): Observable<boolean> {
return this.authorizationService.isAuthorized(FeatureID.CanDeleteVersion, version.self);
}
/** /**
* Get all versions for the given version history and store them in versionRD$ * Get all versions for the given version history and store them in versionRD$
* @param versionHistory$ * @param versionHistory$
@@ -477,44 +324,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
this.getAllVersions(this.versionHistory$); this.getAllVersions(this.versionHistory$);
} }
/**
* Get the ID of the workspace item, if present, otherwise return undefined
* @param versionItem the item for which retrieve the workspace item id
*/
getWorkspaceId(versionItem): Observable<string> {
return versionItem.pipe(
getFirstSucceededRemoteDataPayload(),
map((item: Item) => item.uuid),
switchMap((itemUuid: string) => this.workspaceItemDataService.findByItem(itemUuid, true)),
getFirstCompletedRemoteData<WorkspaceItem>(),
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id ),
);
}
/**
* Get the ID of the workflow item, if present, otherwise return undefined
* @param versionItem the item for which retrieve the workspace item id
*/
getWorkflowId(versionItem): Observable<string> {
return versionItem.pipe(
getFirstSucceededRemoteDataPayload(),
map((item: Item) => item.uuid),
switchMap((itemUuid: string) => this.workflowItemDataService.findByItem(itemUuid, true)),
getFirstCompletedRemoteData<WorkspaceItem>(),
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id ),
);
}
/**
* redirect to the edit page of the workspace item
* @param id$ the id of the workspace item
*/
editWorkspaceItem(id$: Observable<string>) {
id$.subscribe((id) => {
this.router.navigateByUrl('workspaceitems/' + id + '/edit');
});
}
/** /**
* Initialize all observables * Initialize all observables
*/ */
@@ -532,19 +341,12 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
hasValueOperator(), hasValueOperator(),
); );
this.canCreateVersion$ = this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, this.item.self);
// If there is a draft item in the version history the 'Create version' button is disabled and a different tooltip message is shown // If there is a draft item in the version history the 'Create version' button is disabled and a different tooltip message is shown
this.hasDraftVersion$ = this.versionHistoryRD$.pipe( this.hasDraftVersion$ = this.versionHistoryRD$.pipe(
getFirstSucceededRemoteDataPayload(), getFirstSucceededRemoteDataPayload(),
map((res) => Boolean(res?.draftVersion)), map((res) => Boolean(res?.draftVersion)),
); );
this.createVersionTitle$ = this.hasDraftVersion$.pipe(
take(1),
switchMap((res) => of(res ? 'item.version.history.table.action.hasDraft' : 'item.version.history.table.action.newVersion')),
);
this.getAllVersions(this.versionHistory$); this.getAllVersions(this.versionHistory$);
this.hasEpersons$ = this.versionsRD$.pipe( this.hasEpersons$ = this.versionsRD$.pipe(
getAllSucceededRemoteData(), getAllSucceededRemoteData(),

View File

@@ -47,7 +47,7 @@
(showNotification)="showNotification($event)"></ds-google-recaptcha> (showNotification)="showNotification($event)"></ds-google-recaptcha>
</div> </div>
<ng-container *ngIf="((googleRecaptchaService.captchaVersion() | async) !== 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible'); else v2Invisible"> <ng-container *ngIf="(!registrationVerification || ((googleRecaptchaService.captchaVersion() | async) !== 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible')); else v2Invisible">
<button class="btn btn-primary" [disabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()"> <button class="btn btn-primary" [disabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()">
{{ MESSAGE_PREFIX + '.submit' | translate }} {{ MESSAGE_PREFIX + '.submit' | translate }}
</button> </button>

View File

@@ -0,0 +1,48 @@
import { isPlatformBrowser } from '@angular/common';
import {
ChangeDetectorRef,
Directive,
Inject,
OnInit,
PLATFORM_ID,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
@Directive({
selector: '[dsRenderOnlyForBrowser]',
standalone: true,
})
/**
* Structural Directive for rendering a template reference on client side only
*/
export class BrowserOnlyDirective implements OnInit {
constructor(
@Inject(PLATFORM_ID) protected platformId: string,
private viewContainer: ViewContainerRef,
private changeDetector: ChangeDetectorRef,
private templateRef: TemplateRef<any>,
) {
}
ngOnInit(): void {
this.showTemplateBlockInView();
}
/**
* Show template in view container according to platform
*/
private showTemplateBlockInView(): void {
if (!this.templateRef) {
return;
}
this.viewContainer.clear();
if (isPlatformBrowser(this.platformId)) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.changeDetector.markForCheck();
}
}
}

View File

@@ -1,6 +1,6 @@
import { AppConfig } from './app-config.interface'; import { AppConfig } from './app-config.interface';
import { UniversalConfig } from './universal-config.interface'; import { SSRConfig } from './ssr-config.interface';
export interface BuildConfig extends AppConfig { export interface BuildConfig extends AppConfig {
universal: UniversalConfig; ssr: SSRConfig;
} }

View File

@@ -34,7 +34,7 @@ export class DefaultAppConfig implements AppConfig {
// NOTE: will log all redux actions and transfers in console // NOTE: will log all redux actions and transfers in console
debug = false; debug = false;
// Angular Universal server settings // Angular express server settings
// NOTE: these must be 'synced' with the 'dspace.ui.url' setting in your backend's local.cfg. // NOTE: these must be 'synced' with the 'dspace.ui.url' setting in your backend's local.cfg.
ui: UIServerConfig = { ui: UIServerConfig = {
ssl: false, ssl: false,

View File

@@ -0,0 +1,21 @@
import { Config } from './config.interface';
export interface SSRConfig extends Config {
/**
* A boolean flag indicating whether the SSR configuration is enabled
* Defaults to true.
*/
enabled: boolean;
/**
* Enable request performance profiling data collection and printing the results in the server console.
* Defaults to false.
*/
enablePerformanceProfiler: boolean;
/**
* Reduce render blocking requests by inlining critical CSS.
* Defaults to true.
*/
inlineCriticalCss: boolean;
}

View File

@@ -6,5 +6,5 @@ export const StoreDevModules = [
StoreDevtoolsModule.instrument({ StoreDevtoolsModule.instrument({
maxAge: 1000, maxAge: 1000,
logOnly: false, logOnly: false,
}), connectInZone: true }),
]; ];

View File

@@ -1,16 +0,0 @@
import { Config } from './config.interface';
export interface UniversalConfig extends Config {
preboot: boolean;
async: boolean;
time: boolean;
/**
* Whether to inline "critical" styles into the server-side rendered HTML.
*
* Determining which styles are critical is a relatively expensive operation;
* this option can be disabled to boost server performance at the expense of
* loading smoothness.
*/
inlineCriticalCss?: boolean;
}

View File

@@ -3,11 +3,10 @@ import { BuildConfig } from '../config/build-config.interface';
export const environment: Partial<BuildConfig> = { export const environment: Partial<BuildConfig> = {
production: true, production: true,
// Angular Universal settings // Angular SSR settings
universal: { ssr: {
preboot: true, enabled: true,
async: true, enablePerformanceProfiler: false,
time: false,
inlineCriticalCss: true, inlineCriticalCss: true,
}, },
}; };

View File

@@ -7,14 +7,14 @@ import { NotificationAnimationsType } from '../app/shared/notifications/models/n
export const environment: BuildConfig = { export const environment: BuildConfig = {
production: false, production: false,
// Angular Universal settings // Angular SSR settings
universal: { ssr: {
preboot: true, enabled: true,
async: true, enablePerformanceProfiler: false,
time: false, inlineCriticalCss: true,
}, },
// Angular Universal server settings. // Angular express server settings.
ui: { ui: {
ssl: false, ssl: false,
host: 'dspace.com', host: 'dspace.com',

View File

@@ -8,11 +8,10 @@ import { BuildConfig } from '../config/build-config.interface';
export const environment: Partial<BuildConfig> = { export const environment: Partial<BuildConfig> = {
production: false, production: false,
// Angular Universal settings // Angular SSR settings
universal: { ssr: {
preboot: false, enabled: false,
async: true, enablePerformanceProfiler: false,
time: false,
inlineCriticalCss: true, inlineCriticalCss: true,
}, },
}; };

8
src/express.tokens.ts Normal file
View File

@@ -0,0 +1,8 @@
import { InjectionToken } from '@angular/core';
import {
Request,
Response,
} from 'express';
export const REQUEST: InjectionToken<Request> = new InjectionToken<Request>('REQUEST');
export const RESPONSE: InjectionToken<Response> = new InjectionToken<Response>('RESPONSE');

View File

@@ -14,6 +14,4 @@ import { serverAppConfig } from './modules/app/server-app.config';
const bootstrap = () => bootstrapApplication(AppComponent, serverAppConfig); const bootstrap = () => bootstrapApplication(AppComponent, serverAppConfig);
export { renderModule } from '@angular/platform-server';
export { ngExpressEngine } from '@nguniversal/express-engine';
export default bootstrap; export default bootstrap;

View File

@@ -20,7 +20,6 @@ import {
StoreConfig, StoreConfig,
StoreModule, StoreModule,
} from '@ngrx/store'; } from '@ngrx/store';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { import {
MissingTranslationHandler, MissingTranslationHandler,
TranslateLoader, TranslateLoader,
@@ -59,6 +58,7 @@ import { KlaroService } from '../../app/shared/cookies/klaro.service';
import { MissingTranslationHelper } from '../../app/shared/translate/missing-translation.helper'; import { MissingTranslationHelper } from '../../app/shared/translate/missing-translation.helper';
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service'; import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
import { SubmissionService } from '../../app/submission/submission.service'; import { SubmissionService } from '../../app/submission/submission.service';
import { REQUEST } from '../../express.tokens';
import { TranslateBrowserLoader } from '../../ngx-translate-loaders/translate-browser.loader'; import { TranslateBrowserLoader } from '../../ngx-translate-loaders/translate-browser.loader';
import { BrowserInitService } from './browser-init.service'; import { BrowserInitService } from './browser-init.service';

View File

@@ -23,10 +23,10 @@
<p>The test user accounts below have their password set to the name of this <p>The test user accounts below have their password set to the name of this
software in lowercase.</p> software in lowercase.</p>
<ul> <ul>
<li>Demo Site Administrator = dspacedemo+admin@gmail.com</li> <li>Demo Site Administrator = dspacedemo+admin&#64;gmail.com</li>
<li>Demo Community Administrator = dspacedemo+commadmin@gmail.com</li> <li>Demo Community Administrator = dspacedemo+commadmin&#64;gmail.com</li>
<li>Demo Collection Administrator = dspacedemo+colladmin@gmail.com</li> <li>Demo Collection Administrator = dspacedemo+colladmin&#64;gmail.com</li>
<li>Demo Submitter = dspacedemo+submit@gmail.com</li> <li>Demo Submitter = dspacedemo+submit&#64;gmail.com</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -35,5 +35,5 @@
<source type="image/jpg" srcset="assets/dspace/images/banner.jpg 2000w, assets/dspace/images/banner-half.jpg 1200w, assets/dspace/images/banner-tall.jpg 768w"> <source type="image/jpg" srcset="assets/dspace/images/banner.jpg 2000w, assets/dspace/images/banner-half.jpg 1200w, assets/dspace/images/banner-tall.jpg 768w">
<img alt="" [src]="'assets/dspace/images/banner.jpg'"/><!-- without the []="''" Firefox downloads both the fallback and the resolved image --> <img alt="" [src]="'assets/dspace/images/banner.jpg'"/><!-- without the []="''" Firefox downloads both the fallback and the resolved image -->
</picture> </picture>
<small class="credits">Photo by <a href="https://www.pexels.com/@inspiredimages">@inspiredimages</a></small> <small class="credits">Photo by <a href="https://www.pexels.com/@inspiredimages">&#64;inspiredimages</a></small>
</div> </div>

3567
yarn.lock

File diff suppressed because it is too large Load Diff