diff --git a/.eslintrc.json b/.eslintrc.json
index 5fb4c12171..888c968b5c 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -293,7 +293,8 @@
],
"rules": {
// Custom DSpace Angular rules
- "dspace-angular-html/themed-component-usages": "error"
+ "dspace-angular-html/themed-component-usages": "error",
+ "dspace-angular-html/no-disabled-attribute-on-button": "error"
}
},
{
diff --git a/config/config.example.yml b/config/config.example.yml
index d1a40e6b1f..8639dc0010 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -23,6 +23,31 @@ ssr:
# Determining which styles are critical is a relatively expensive operation; this option is
# disabled (false) by default to boost server performance at the expense of loading smoothness.
inlineCriticalCss: false
+ # Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects.
+ # NOTE: The "/handle/" path ensures Handle redirects work via SSR. The "/reload/" path ensures
+ # hard refreshes (e.g. after login) trigger SSR while fully reloading the page.
+ paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/', '/reload/' ]
+ # Whether to enable rendering of Search component on SSR.
+ # If set to true the component will be included in the HTML returned from the server side rendering.
+ # If set to false the component will not be included in the HTML returned from the server side rendering.
+ enableSearchComponent: false
+ # Whether to enable rendering of Browse component on SSR.
+ # If set to true the component will be included in the HTML returned from the server side rendering.
+ # If set to false the component will not be included in the HTML returned from the server side rendering.
+ enableBrowseComponent: false
+ # Enable state transfer from the server-side application to the client-side application.
+ # Defaults to true.
+ # Note: When using an external application cache layer, it's recommended not to transfer the state to avoid caching it.
+ # Disabling it ensures that dynamic state information is not inadvertently cached, which can improve security and
+ # ensure that users always use the most up-to-date state.
+ transferState: true
+ # When a different REST base URL is used for the server-side application, the generated state contains references to
+ # REST resources with the internal URL configured. By default, these internal URLs are replaced with public URLs.
+ # Disable this setting to avoid URL replacement during SSR. In this the state is not transferred to avoid security issues.
+ replaceRestUrl: true
+ # Enable request performance profiling data collection and printing the results in the server console.
+ # Defaults to false. Enabling in production is NOT recommended
+ #enablePerformanceProfiler: false
# The REST API server settings
# NOTE: these settings define which (publicly available) REST API to use. They are usually
@@ -33,6 +58,9 @@ rest:
port: 443
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
nameSpace: /server
+ # Provide a different REST url to be used during SSR execution. It must contain the whole url including protocol, server port and
+ # server namespace (uncomment to use it).
+ #ssrBaseUrl: http://localhost:8080/server
# Caching settings
cache:
@@ -448,6 +476,12 @@ search:
enabled: false
# List of filters to enable in "Advanced Search" dropdown
filter: [ 'title', 'author', 'subject', 'entityType' ]
+ #
+ # Number used to render n UI elements called loading skeletons that act as placeholders.
+ # These elements indicate that some content will be loaded in their stead.
+ # Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count.
+ # e.g. If we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved.
+ defaultFiltersCount: 5
# Notify metrics
diff --git a/docs/lint/html/index.md b/docs/lint/html/index.md
index 15d693843c..e134e1070f 100644
--- a/docs/lint/html/index.md
+++ b/docs/lint/html/index.md
@@ -2,3 +2,4 @@
_______
- [`dspace-angular-html/themed-component-usages`](./rules/themed-component-usages.md): Themeable components should be used via the selector of their `ThemedComponent` wrapper class
+- [`dspace-angular-html/no-disabled-attribute-on-button`](./rules/no-disabled-attribute-on-button.md): Buttons should use the `dsBtnDisabled` directive instead of the HTML `disabled` attribute.
diff --git a/docs/lint/html/rules/no-disabled-attribute-on-button.md b/docs/lint/html/rules/no-disabled-attribute-on-button.md
new file mode 100644
index 0000000000..d9d39ce82c
--- /dev/null
+++ b/docs/lint/html/rules/no-disabled-attribute-on-button.md
@@ -0,0 +1,78 @@
+[DSpace ESLint plugins](../../../../lint/README.md) > [HTML rules](../index.md) > `dspace-angular-html/no-disabled-attribute-on-button`
+_______
+
+Buttons should use the `dsBtnDisabled` directive instead of the HTML `disabled` attribute.
+ This should be done to ensure that users with a screen reader are able to understand that the a button button is present, and that it is disabled.
+ The native html disabled attribute does not allow users to navigate to the button by keyboard, and thus they have no way of knowing that the button is present.
+
+_______
+
+[Source code](../../../../lint/src/rules/html/no-disabled-attribute-on-button.ts)
+
+### Examples
+
+
+#### Valid code
+
+##### should use [dsBtnDisabled] in HTML templates
+
+```html
+
+```
+
+##### disabled attribute is still valid on non-button elements
+
+```html
+
+```
+
+##### [disabled] attribute is still valid on non-button elements
+
+```html
+
+```
+
+##### angular dynamic attributes that use disabled are still valid
+
+```html
+
+```
+
+
+
+
+#### Invalid code & automatic fixes
+
+##### should not use disabled attribute in HTML templates
+
+```html
+
+```
+Will produce the following error(s):
+```
+Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
+```
+
+Result of `yarn lint --fix`:
+```html
+
+```
+
+
+##### should not use [disabled] attribute in HTML templates
+
+```html
+
+```
+Will produce the following error(s):
+```
+Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
+```
+
+Result of `yarn lint --fix`:
+```html
+
+```
+
+
+
diff --git a/lint/src/rules/html/index.ts b/lint/src/rules/html/index.ts
index 7c1370ae2d..3d425c3ad4 100644
--- a/lint/src/rules/html/index.ts
+++ b/lint/src/rules/html/index.ts
@@ -10,10 +10,13 @@ import {
bundle,
RuleExports,
} from '../../util/structure';
+import * as noDisabledAttributeOnButton from './no-disabled-attribute-on-button';
import * as themedComponentUsages from './themed-component-usages';
const index = [
themedComponentUsages,
+ noDisabledAttributeOnButton,
+
] as unknown as RuleExports[];
export = {
diff --git a/lint/src/rules/html/no-disabled-attribute-on-button.ts b/lint/src/rules/html/no-disabled-attribute-on-button.ts
new file mode 100644
index 0000000000..bf1a72d70d
--- /dev/null
+++ b/lint/src/rules/html/no-disabled-attribute-on-button.ts
@@ -0,0 +1,147 @@
+import {
+ TmplAstBoundAttribute,
+ TmplAstTextAttribute,
+} from '@angular-eslint/bundled-angular-compiler';
+import { TemplateParserServices } from '@angular-eslint/utils';
+import {
+ ESLintUtils,
+ TSESLint,
+} from '@typescript-eslint/utils';
+
+import {
+ DSpaceESLintRuleInfo,
+ NamedTests,
+} from '../../util/structure';
+import { getSourceCode } from '../../util/typescript';
+
+export enum Message {
+ USE_DSBTN_DISABLED = 'mustUseDsBtnDisabled',
+}
+
+export const info = {
+ name: 'no-disabled-attribute-on-button',
+ meta: {
+ docs: {
+ description: `Buttons should use the \`dsBtnDisabled\` directive instead of the HTML \`disabled\` attribute.
+ This should be done to ensure that users with a screen reader are able to understand that the a button button is present, and that it is disabled.
+ The native html disabled attribute does not allow users to navigate to the button by keyboard, and thus they have no way of knowing that the button is present.`,
+ },
+ type: 'problem',
+ fixable: 'code',
+ schema: [],
+ messages: {
+ [Message.USE_DSBTN_DISABLED]: 'Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.',
+ },
+ },
+ defaultOptions: [],
+} as DSpaceESLintRuleInfo;
+
+export const rule = ESLintUtils.RuleCreator.withoutDocs({
+ ...info,
+ create(context: TSESLint.RuleContext) {
+ const parserServices = getSourceCode(context).parserServices as TemplateParserServices;
+
+ /**
+ * Some dynamic angular inputs will have disabled as name because of how Angular handles this internally (e.g [class.disabled]="isDisabled")
+ * But these aren't actually the disabled attribute we're looking for, we can determine this by checking the details of the keySpan
+ */
+ function isOtherAttributeDisabled(node: TmplAstBoundAttribute | TmplAstTextAttribute): boolean {
+ // if the details are not null, and the details are not 'disabled', then it's not the disabled attribute we're looking for
+ return node.keySpan?.details !== null && node.keySpan?.details !== 'disabled';
+ }
+
+ /**
+ * Replace the disabled text with [dsBtnDisabled] in the template
+ */
+ function replaceDisabledText(text: string ): string {
+ const hasBrackets = text.includes('[') && text.includes(']');
+ const newDisabledText = hasBrackets ? 'dsBtnDisabled' : '[dsBtnDisabled]="true"';
+ return text.replace('disabled', newDisabledText);
+ }
+
+ function inputIsChildOfButton(node: any): boolean {
+ return (node.parent?.tagName === 'button' || node.parent?.name === 'button');
+ }
+
+ function reportAndFix(node: TmplAstBoundAttribute | TmplAstTextAttribute) {
+ if (!inputIsChildOfButton(node) || isOtherAttributeDisabled(node)) {
+ return;
+ }
+
+ const sourceSpan = node.sourceSpan;
+ context.report({
+ messageId: Message.USE_DSBTN_DISABLED,
+ loc: parserServices.convertNodeSourceSpanToLoc(sourceSpan),
+ fix(fixer) {
+ const templateText = sourceSpan.start.file.content;
+ const disabledText = templateText.slice(sourceSpan.start.offset, sourceSpan.end.offset);
+ const newText = replaceDisabledText(disabledText);
+ return fixer.replaceTextRange([sourceSpan.start.offset, sourceSpan.end.offset], newText);
+ },
+ });
+ }
+
+ return {
+ 'BoundAttribute[name="disabled"]'(node: TmplAstBoundAttribute) {
+ reportAndFix(node);
+ },
+ 'TextAttribute[name="disabled"]'(node: TmplAstTextAttribute) {
+ reportAndFix(node);
+ },
+ };
+ },
+});
+
+export const tests = {
+ plugin: info.name,
+ valid: [
+ {
+ name: 'should use [dsBtnDisabled] in HTML templates',
+ code: `
+
+ `,
+ },
+ {
+ name: 'disabled attribute is still valid on non-button elements',
+ code: `
+
+ `,
+ },
+ {
+ name: '[disabled] attribute is still valid on non-button elements',
+ code: `
+
+ `,
+ },
+ {
+ name: 'angular dynamic attributes that use disabled are still valid',
+ code: `
+
+ `,
+ },
+ ],
+ invalid: [
+ {
+ name: 'should not use disabled attribute in HTML templates',
+ code: `
+
+ `,
+ errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
+ output: `
+
+ `,
+ },
+ {
+ name: 'should not use [disabled] attribute in HTML templates',
+ code: `
+
+ `,
+ errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
+ output: `
+
+ `,
+ },
+ ],
+} as NamedTests;
+
+export default rule;
diff --git a/package.json b/package.json
index f1fb80645d..408eb0e134 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "dspace-angular",
- "version": "8.1.0-next",
+ "version": "8.2.0-next",
"scripts": {
"ng": "ng",
"config:watch": "nodemon",
@@ -67,7 +67,7 @@
"@angular/platform-server": "^17.3.11",
"@angular/router": "^17.3.11",
"@angular/ssr": "^17.3.11",
- "@babel/runtime": "7.26.0",
+ "@babel/runtime": "7.26.7",
"@kolkov/ngx-gallery": "^2.0.1",
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@ng-dynamic-forms/core": "^16.0.0",
@@ -85,7 +85,7 @@
"colors": "^1.4.0",
"compression": "^1.7.5",
"cookie-parser": "1.4.7",
- "core-js": "^3.39.0",
+ "core-js": "^3.40.0",
"date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7",
"deepmerge": "^4.3.1",
@@ -96,7 +96,7 @@
"filesize": "^6.1.0",
"http-proxy-middleware": "^2.0.7",
"http-terminator": "^3.2.0",
- "isbot": "^5.1.17",
+ "isbot": "^5.1.22",
"js-cookie": "2.2.1",
"js-yaml": "^4.1.0",
"json5": "^2.2.3",
@@ -106,7 +106,7 @@
"lodash": "^4.17.21",
"lru-cache": "^7.14.1",
"markdown-it": "^13.0.1",
- "mirador": "^3.4.2",
+ "mirador": "^3.4.3",
"mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.16.0",
"morgan": "^1.10.0",
@@ -114,6 +114,7 @@
"ng2-nouislider": "^2.0.0",
"ngx-infinite-scroll": "^16.0.0",
"ngx-pagination": "6.0.3",
+ "ngx-skeleton-loader": "^9.0.0",
"ngx-ui-switch": "^14.1.0",
"nouislider": "^15.7.1",
"pem": "1.14.8",
@@ -146,7 +147,7 @@
"@types/grecaptcha": "^3.0.9",
"@types/jasmine": "~3.6.0",
"@types/js-cookie": "2.2.6",
- "@types/lodash": "^4.17.14",
+ "@types/lodash": "^4.17.15",
"@types/node": "^14.14.9",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
@@ -158,7 +159,7 @@
"cross-env": "^7.0.3",
"csstype": "^3.1.3",
"cypress": "^13.17.0",
- "cypress-axe": "^1.5.0",
+ "cypress-axe": "^1.6.0",
"deep-freeze": "0.0.1",
"eslint": "^8.39.0",
"eslint-plugin-deprecation": "^1.4.1",
@@ -167,7 +168,7 @@
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-import-newlines": "^1.3.1",
"eslint-plugin-jsdoc": "^45.0.0",
- "eslint-plugin-jsonc": "^2.18.2",
+ "eslint-plugin-jsonc": "^2.19.1",
"eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-rxjs": "^5.0.3",
"eslint-plugin-simple-import-sort": "^10.0.0",
@@ -182,10 +183,10 @@
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"karma-mocha-reporter": "2.2.5",
- "ng-mocks": "^14.13.1",
+ "ng-mocks": "^14.13.2",
"ngx-mask": "14.2.4",
"nodemon": "^2.0.22",
- "postcss": "^8.4",
+ "postcss": "^8.5",
"postcss-import": "^14.0.0",
"postcss-loader": "^4.0.3",
"postcss-preset-env": "^7.4.2",
@@ -194,7 +195,7 @@
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^16.14.0",
"rimraf": "^3.0.2",
- "sass": "~1.83.1",
+ "sass": "~1.84.0",
"sass-loader": "^12.6.0",
"sass-resources-loader": "^2.2.5",
"ts-node": "^8.10.2",
diff --git a/server.ts b/server.ts
index 032b79b8f2..4544a7c3ba 100644
--- a/server.ts
+++ b/server.ts
@@ -81,6 +81,9 @@ let anonymousCache: LRU;
// extend environment with app config for server
extendEnvironmentWithAppConfig(environment, appConfig);
+// The REST server base URL
+const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl;
+
// The Express app is exported so that it can be used by serverless Functions.
export function app() {
@@ -156,7 +159,7 @@ export function app() {
* Proxy the sitemaps
*/
router.use('/sitemap**', createProxyMiddleware({
- target: `${environment.rest.baseUrl}/sitemaps`,
+ target: `${REST_BASE_URL}/sitemaps`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true,
}));
@@ -165,7 +168,7 @@ export function app() {
* Proxy the linksets
*/
router.use('/signposting**', createProxyMiddleware({
- target: `${environment.rest.baseUrl}`,
+ target: `${REST_BASE_URL}`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true,
}));
@@ -218,7 +221,7 @@ export function app() {
* The callback function to serve server side angular
*/
function ngApp(req, res, next) {
- if (environment.ssr.enabled) {
+ if (environment.ssr.enabled && req.method === 'GET' && (req.path === '/' || environment.ssr.paths.some(pathPrefix => req.path.startsWith(pathPrefix)))) {
// Render the page to user via SSR (server side rendering)
serverSideRender(req, res, next);
} else {
@@ -266,6 +269,11 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) {
})
.then((html) => {
if (hasValue(html)) {
+ // Replace REST URL with UI URL
+ if (environment.ssr.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
+ html = html.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
+ }
+
// save server side rendered page to cache (if any are enabled)
saveToCache(req, html);
if (sendToUser) {
@@ -623,7 +631,7 @@ function start() {
* The callback function to serve health check requests
*/
function healthCheck(req, res) {
- const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
+ const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
axios.get(baseUrl)
.then((response) => {
res.status(response.status).send(response.data);
diff --git a/src/app/access-control/bulk-access/bulk-access.component.html b/src/app/access-control/bulk-access/bulk-access.component.html
index c164cc5c31..cda6b805bc 100644
--- a/src/app/access-control/bulk-access/bulk-access.component.html
+++ b/src/app/access-control/bulk-access/bulk-access.component.html
@@ -10,7 +10,7 @@
-