Merge branch 'main' into accessibility-settings-main

This commit is contained in:
Andreas Awouters
2025-02-21 15:52:34 +01:00
300 changed files with 7343 additions and 1904 deletions

View File

@@ -293,7 +293,8 @@
], ],
"rules": { "rules": {
// Custom DSpace Angular 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"
} }
}, },
{ {

View File

@@ -24,7 +24,30 @@ ssr:
# disabled (false) by default to boost server performance at the expense of loading smoothness. # disabled (false) by default to boost server performance at the expense of loading smoothness.
inlineCriticalCss: false inlineCriticalCss: false
# Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects. # Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects.
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ] # 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 # The REST API server settings
# NOTE: these settings define which (publicly available) REST API to use. They are usually # NOTE: these settings define which (publicly available) REST API to use. They are usually
@@ -35,6 +58,9 @@ rest:
port: 443 port: 443
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript # NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
nameSpace: /server 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 # Caching settings
cache: cache:
@@ -450,6 +476,12 @@ search:
enabled: false enabled: false
# List of filters to enable in "Advanced Search" dropdown # List of filters to enable in "Advanced Search" dropdown
filter: [ 'title', 'author', 'subject', 'entityType' ] 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 # Notify metrics

View File

@@ -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/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.

View File

@@ -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
<button [dsBtnDisabled]="true">Submit</button>
```
##### disabled attribute is still valid on non-button elements
```html
<input disabled>
```
##### [disabled] attribute is still valid on non-button elements
```html
<input [disabled]="true">
```
##### angular dynamic attributes that use disabled are still valid
```html
<button [class.disabled]="isDisabled">Submit</button>
```
#### Invalid code &amp; automatic fixes
##### should not use disabled attribute in HTML templates
```html
<button disabled>Submit</button>
```
Will produce the following error(s):
```
Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
```
Result of `yarn lint --fix`:
```html
<button [dsBtnDisabled]="true">Submit</button>
```
##### should not use [disabled] attribute in HTML templates
```html
<button [disabled]="true">Submit</button>
```
Will produce the following error(s):
```
Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
```
Result of `yarn lint --fix`:
```html
<button [dsBtnDisabled]="true">Submit</button>
```

View File

@@ -10,10 +10,13 @@ import {
bundle, bundle,
RuleExports, RuleExports,
} from '../../util/structure'; } from '../../util/structure';
import * as noDisabledAttributeOnButton from './no-disabled-attribute-on-button';
import * as themedComponentUsages from './themed-component-usages'; import * as themedComponentUsages from './themed-component-usages';
const index = [ const index = [
themedComponentUsages, themedComponentUsages,
noDisabledAttributeOnButton,
] as unknown as RuleExports[]; ] as unknown as RuleExports[];
export = { export = {

View File

@@ -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<Message, unknown[]>) {
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: `
<button [dsBtnDisabled]="true">Submit</button>
`,
},
{
name: 'disabled attribute is still valid on non-button elements',
code: `
<input disabled>
`,
},
{
name: '[disabled] attribute is still valid on non-button elements',
code: `
<input [disabled]="true">
`,
},
{
name: 'angular dynamic attributes that use disabled are still valid',
code: `
<button [class.disabled]="isDisabled">Submit</button>
`,
},
],
invalid: [
{
name: 'should not use disabled attribute in HTML templates',
code: `
<button disabled>Submit</button>
`,
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
output: `
<button [dsBtnDisabled]="true">Submit</button>
`,
},
{
name: 'should not use [disabled] attribute in HTML templates',
code: `
<button [disabled]="true">Submit</button>
`,
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
output: `
<button [dsBtnDisabled]="true">Submit</button>
`,
},
],
} as NamedTests;
export default rule;

133
package-lock.json generated
View File

@@ -21,7 +21,7 @@
"@angular/platform-server": "^17.3.12", "@angular/platform-server": "^17.3.12",
"@angular/router": "^17.3.12", "@angular/router": "^17.3.12",
"@angular/ssr": "^17.3.11", "@angular/ssr": "^17.3.11",
"@babel/runtime": "7.26.0", "@babel/runtime": "7.26.7",
"@kolkov/ngx-gallery": "^2.0.1", "@kolkov/ngx-gallery": "^2.0.1",
"@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",
@@ -39,27 +39,27 @@
"colors": "^1.4.0", "colors": "^1.4.0",
"compression": "^1.7.5", "compression": "^1.7.5",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"core-js": "^3.39.0", "core-js": "^3.40.0",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7", "date-fns-tz": "^1.3.7",
"deepmerge": "^4.3.1", "deepmerge": "^4.3.1",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^4.21.1", "express": "^4.21.2",
"express-rate-limit": "^5.1.3", "express-rate-limit": "^5.1.3",
"fast-json-patch": "^3.1.1", "fast-json-patch": "^3.1.1",
"filesize": "^6.1.0", "filesize": "^6.1.0",
"http-proxy-middleware": "^2.0.7", "http-proxy-middleware": "^2.0.7",
"http-terminator": "^3.2.0", "http-terminator": "^3.2.0",
"isbot": "^5.1.21", "isbot": "^5.1.22",
"js-cookie": "2.2.1", "js-cookie": "2.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"json5": "^2.2.3", "json5": "^2.2.3",
"jsonschema": "1.4.1", "jsonschema": "1.5.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lru-cache": "^7.14.1", "lru-cache": "^7.14.1",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"mirador": "^3.4.2", "mirador": "^3.4.3",
"mirador-dl-plugin": "^0.13.0", "mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.16.0", "mirador-share-plugin": "^0.16.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
@@ -67,6 +67,7 @@
"ng2-nouislider": "^2.0.0", "ng2-nouislider": "^2.0.0",
"ngx-infinite-scroll": "^16.0.0", "ngx-infinite-scroll": "^16.0.0",
"ngx-pagination": "6.0.3", "ngx-pagination": "6.0.3",
"ngx-skeleton-loader": "^9.0.0",
"ngx-ui-switch": "^14.1.0", "ngx-ui-switch": "^14.1.0",
"nouislider": "^15.7.1", "nouislider": "^15.7.1",
"orejime": "^2.3.1", "orejime": "^2.3.1",
@@ -99,7 +100,7 @@
"@types/grecaptcha": "^3.0.9", "@types/grecaptcha": "^3.0.9",
"@types/jasmine": "~3.6.0", "@types/jasmine": "~3.6.0",
"@types/js-cookie": "2.2.6", "@types/js-cookie": "2.2.6",
"@types/lodash": "^4.17.14", "@types/lodash": "^4.17.15",
"@types/node": "^14.14.9", "@types/node": "^14.14.9",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.18.0",
@@ -110,7 +111,7 @@
"copy-webpack-plugin": "^6.4.1", "copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cypress": "^13.17.0", "cypress": "^13.17.0",
"cypress-axe": "^1.5.0", "cypress-axe": "^1.6.0",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"eslint": "^8.39.0", "eslint": "^8.39.0",
"eslint-plugin-deprecation": "^1.4.1", "eslint-plugin-deprecation": "^1.4.1",
@@ -119,7 +120,7 @@
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-import-newlines": "^1.3.1", "eslint-plugin-import-newlines": "^1.3.1",
"eslint-plugin-jsdoc": "^45.0.0", "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-lodash": "^7.4.0",
"eslint-plugin-rxjs": "^5.0.3", "eslint-plugin-rxjs": "^5.0.3",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
@@ -137,12 +138,12 @@
"ng-mocks": "^14.13.2", "ng-mocks": "^14.13.2",
"ngx-mask": "14.2.4", "ngx-mask": "14.2.4",
"nodemon": "^2.0.22", "nodemon": "^2.0.22",
"postcss": "^8.4", "postcss": "^8.5",
"postcss-import": "^14.0.0", "postcss-import": "^14.0.0",
"postcss-loader": "^4.0.3", "postcss-loader": "^4.0.3",
"postcss-preset-env": "^7.4.2", "postcss-preset-env": "^7.4.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "~1.83.1", "sass": "~1.84.0",
"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",
@@ -3868,9 +3869,10 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.26.0", "version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@@ -6897,10 +6899,11 @@
"dev": true "dev": true
}, },
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.17.14", "version": "4.17.15",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==", "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/@types/mime": { "node_modules/@types/mime": {
"version": "1.3.5", "version": "1.3.5",
@@ -9920,9 +9923,9 @@
} }
}, },
"node_modules/core-js": { "node_modules/core-js": {
"version": "3.39.0", "version": "3.40.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz",
"integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==",
"hasInstallScript": true, "hasInstallScript": true,
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@@ -10080,10 +10083,11 @@
} }
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
"shebang-command": "^2.0.0", "shebang-command": "^2.0.0",
@@ -10328,16 +10332,17 @@
} }
}, },
"node_modules/cypress-axe": { "node_modules/cypress-axe": {
"version": "1.5.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/cypress-axe/-/cypress-axe-1.5.0.tgz", "resolved": "https://registry.npmjs.org/cypress-axe/-/cypress-axe-1.6.0.tgz",
"integrity": "sha512-Hy/owCjfj+25KMsecvDgo4fC/781ccL+e8p+UUYoadGVM2ogZF9XIKbiM6KI8Y3cEaSreymdD6ZzccbI2bY0lQ==", "integrity": "sha512-C/ij50G8eebBrl/WsGT7E+T/SFyIsRZ3Epx9cRTLrPL9Y1GcxlQGFoAVdtSFWRrHSCWXq9HC6iJQMaI89O9yvQ==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
"peerDependencies": { "peerDependencies": {
"axe-core": "^3 || ^4", "axe-core": "^3 || ^4",
"cypress": "^10 || ^11 || ^12 || ^13" "cypress": "^10 || ^11 || ^12 || ^13 || ^14"
} }
}, },
"node_modules/cypress/node_modules/ansi-styles": { "node_modules/cypress/node_modules/ansi-styles": {
@@ -11984,10 +11989,11 @@
} }
}, },
"node_modules/eslint-plugin-jsonc": { "node_modules/eslint-plugin-jsonc": {
"version": "2.18.2", "version": "2.19.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.18.2.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.19.1.tgz",
"integrity": "sha512-SDhJiSsWt3nItl/UuIv+ti4g3m4gpGkmnUJS9UWR3TrpyNsIcnJoBRD7Kof6cM4Rk3L0wrmY5Tm3z7ZPjR2uGg==", "integrity": "sha512-MmlAOaZK1+Lg7YoCZPGRjb88ZjT+ct/KTsvcsbZdBm+w8WMzGx+XEmexk0m40P1WV9G2rFV7X3klyRGRpFXEjA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"eslint-compat-utils": "^0.6.0", "eslint-compat-utils": "^0.6.0",
@@ -12486,9 +12492,9 @@
"dev": true "dev": true
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.21.1", "version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
@@ -12509,7 +12515,7 @@
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.10", "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.13.0", "qs": "6.13.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
@@ -12524,6 +12530,10 @@
}, },
"engines": { "engines": {
"node": ">= 0.10.0" "node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-rate-limit": { "node_modules/express-rate-limit": {
@@ -14474,9 +14484,10 @@
} }
}, },
"node_modules/isbot": { "node_modules/isbot": {
"version": "5.1.21", "version": "5.1.22",
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.21.tgz", "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.22.tgz",
"integrity": "sha512-0q3naRVpENL0ReKHeNcwn/G7BDynp0DqZUckKyFtM9+hmpnPqgm8+8wbjiVZ0XNhq1wPQV28/Pb8Snh5adeUHA==", "integrity": "sha512-RqCFY3cJy3c2y1I+rMn81cfzAR4XJwfPBC+M8kffUjbPzxApzyyv7Tbm1C/gXXq2dSCuD238pKFEWlQMTWsTFw==",
"license": "Unlicense",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -15016,9 +15027,9 @@
] ]
}, },
"node_modules/jsonschema": { "node_modules/jsonschema": {
"version": "1.4.1", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz",
"integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==",
"engines": { "engines": {
"node": "*" "node": "*"
} }
@@ -16618,9 +16629,9 @@
} }
}, },
"node_modules/mirador": { "node_modules/mirador": {
"version": "3.4.2", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/mirador/-/mirador-3.4.2.tgz", "resolved": "https://registry.npmjs.org/mirador/-/mirador-3.4.3.tgz",
"integrity": "sha512-Gd7G4NkXq6/qD/De5soYspSo9VykAzrGFunKqUI3x9WShoZP23pYIEPoC/96tvfk3KMv+UbAUxDp99Xeo7vnVQ==", "integrity": "sha512-yHoug0MHy4e9apykbbBhK+4CmbZS94zMxmugw2E2VX6iB0b2PKKY0JfYr/QfXh9P29YnWAbymaXJVpgbHVpTVw==",
"dependencies": { "dependencies": {
"@material-ui/core": "^4.12.3", "@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.9.1", "@material-ui/icons": "^4.9.1",
@@ -16941,6 +16952,19 @@
"@angular/core": ">=13.0.0" "@angular/core": ">=13.0.0"
} }
}, },
"node_modules/ngx-skeleton-loader": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/ngx-skeleton-loader/-/ngx-skeleton-loader-9.0.0.tgz",
"integrity": "sha512-aO4/V6oGdZGNcTjasTg/fwzJJYl/ZmNKgCukOEQdUK3GSFOZtB/3GGULMJuZ939hk3Hzqh1OBiLfIM1SqTfhqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0"
}
},
"node_modules/ngx-ui-switch": { "node_modules/ngx-ui-switch": {
"version": "14.1.0", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/ngx-ui-switch/-/ngx-ui-switch-14.1.0.tgz", "resolved": "https://registry.npmjs.org/ngx-ui-switch/-/ngx-ui-switch-14.1.0.tgz",
@@ -18188,9 +18212,9 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.10", "version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
}, },
"node_modules/path-type": { "node_modules/path-type": {
"version": "4.0.0", "version": "4.0.0",
@@ -18408,9 +18432,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.49", "version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -18426,7 +18450,7 @@
} }
], ],
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.8",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@@ -20477,10 +20501,11 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.83.1", "version": "1.84.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.83.1.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.84.0.tgz",
"integrity": "sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==", "integrity": "sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
"immutable": "^5.0.2", "immutable": "^5.0.2",

View File

@@ -108,7 +108,7 @@
"@angular/platform-server": "^17.3.12", "@angular/platform-server": "^17.3.12",
"@angular/router": "^17.3.12", "@angular/router": "^17.3.12",
"@angular/ssr": "^17.3.11", "@angular/ssr": "^17.3.11",
"@babel/runtime": "7.26.0", "@babel/runtime": "7.26.7",
"@kolkov/ngx-gallery": "^2.0.1", "@kolkov/ngx-gallery": "^2.0.1",
"@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",
@@ -126,27 +126,27 @@
"colors": "^1.4.0", "colors": "^1.4.0",
"compression": "^1.7.5", "compression": "^1.7.5",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"core-js": "^3.39.0", "core-js": "^3.40.0",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7", "date-fns-tz": "^1.3.7",
"deepmerge": "^4.3.1", "deepmerge": "^4.3.1",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^4.21.1", "express": "^4.21.2",
"express-rate-limit": "^5.1.3", "express-rate-limit": "^5.1.3",
"fast-json-patch": "^3.1.1", "fast-json-patch": "^3.1.1",
"filesize": "^6.1.0", "filesize": "^6.1.0",
"http-proxy-middleware": "^2.0.7", "http-proxy-middleware": "^2.0.7",
"http-terminator": "^3.2.0", "http-terminator": "^3.2.0",
"isbot": "^5.1.21", "isbot": "^5.1.22",
"js-cookie": "2.2.1", "js-cookie": "2.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"json5": "^2.2.3", "json5": "^2.2.3",
"jsonschema": "1.4.1", "jsonschema": "1.5.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lru-cache": "^7.14.1", "lru-cache": "^7.14.1",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"mirador": "^3.4.2", "mirador": "^3.4.3",
"mirador-dl-plugin": "^0.13.0", "mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.16.0", "mirador-share-plugin": "^0.16.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
@@ -154,6 +154,7 @@
"ng2-nouislider": "^2.0.0", "ng2-nouislider": "^2.0.0",
"ngx-infinite-scroll": "^16.0.0", "ngx-infinite-scroll": "^16.0.0",
"ngx-pagination": "6.0.3", "ngx-pagination": "6.0.3",
"ngx-skeleton-loader": "^9.0.0",
"ngx-ui-switch": "^14.1.0", "ngx-ui-switch": "^14.1.0",
"nouislider": "^15.7.1", "nouislider": "^15.7.1",
"orejime": "^2.3.1", "orejime": "^2.3.1",
@@ -186,7 +187,7 @@
"@types/grecaptcha": "^3.0.9", "@types/grecaptcha": "^3.0.9",
"@types/jasmine": "~3.6.0", "@types/jasmine": "~3.6.0",
"@types/js-cookie": "2.2.6", "@types/js-cookie": "2.2.6",
"@types/lodash": "^4.17.14", "@types/lodash": "^4.17.15",
"@types/node": "^14.14.9", "@types/node": "^14.14.9",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.18.0",
@@ -197,7 +198,7 @@
"copy-webpack-plugin": "^6.4.1", "copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cypress": "^13.17.0", "cypress": "^13.17.0",
"cypress-axe": "^1.5.0", "cypress-axe": "^1.6.0",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"eslint": "^8.39.0", "eslint": "^8.39.0",
"eslint-plugin-deprecation": "^1.4.1", "eslint-plugin-deprecation": "^1.4.1",
@@ -206,7 +207,7 @@
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-import-newlines": "^1.3.1", "eslint-plugin-import-newlines": "^1.3.1",
"eslint-plugin-jsdoc": "^45.0.0", "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-lodash": "^7.4.0",
"eslint-plugin-rxjs": "^5.0.3", "eslint-plugin-rxjs": "^5.0.3",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
@@ -224,12 +225,12 @@
"ng-mocks": "^14.13.2", "ng-mocks": "^14.13.2",
"ngx-mask": "14.2.4", "ngx-mask": "14.2.4",
"nodemon": "^2.0.22", "nodemon": "^2.0.22",
"postcss": "^8.4", "postcss": "^8.5",
"postcss-import": "^14.0.0", "postcss-import": "^14.0.0",
"postcss-loader": "^4.0.3", "postcss-loader": "^4.0.3",
"postcss-preset-env": "^7.4.2", "postcss-preset-env": "^7.4.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "~1.83.1", "sass": "~1.84.0",
"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",

View File

@@ -81,6 +81,9 @@ let anonymousCache: LRU<string, any>;
// extend environment with app config for server // extend environment with app config for server
extendEnvironmentWithAppConfig(environment, appConfig); extendEnvironmentWithAppConfig(environment, appConfig);
// 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. // The Express app is exported so that it can be used by serverless Functions.
export function app() { export function app() {
@@ -156,7 +159,7 @@ export function app() {
* Proxy the sitemaps * Proxy the sitemaps
*/ */
router.use('/sitemap**', createProxyMiddleware({ router.use('/sitemap**', createProxyMiddleware({
target: `${environment.rest.baseUrl}/sitemaps`, target: `${REST_BASE_URL}/sitemaps`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true, changeOrigin: true,
})); }));
@@ -165,7 +168,7 @@ export function app() {
* Proxy the linksets * Proxy the linksets
*/ */
router.use('/signposting**', createProxyMiddleware({ router.use('/signposting**', createProxyMiddleware({
target: `${environment.rest.baseUrl}`, target: `${REST_BASE_URL}`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true, changeOrigin: true,
})); }));
@@ -266,6 +269,11 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) {
}) })
.then((html) => { .then((html) => {
if (hasValue(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) // save server side rendered page to cache (if any are enabled)
saveToCache(req, html); saveToCache(req, html);
if (sendToUser) { if (sendToUser) {
@@ -623,7 +631,7 @@ function start() {
* The callback function to serve health check requests * The callback function to serve health check requests
*/ */
function healthCheck(req, res) { function healthCheck(req, res) {
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`; const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
axios.get(baseUrl) axios.get(baseUrl)
.then((response) => { .then((response) => {
res.status(response.status).send(response.data); res.status(response.status).send(response.data);

View File

@@ -10,7 +10,7 @@
<button class="btn btn-outline-primary mr-3" (click)="reset()"> <button class="btn btn-outline-primary mr-3" (click)="reset()">
{{ 'access-control-cancel' | translate }} {{ 'access-control-cancel' | translate }}
</button> </button>
<button class="btn btn-primary" [disabled]="!canExport()" (click)="submit()"> <button class="btn btn-primary" [dsBtnDisabled]="!canExport()" (click)="submit()">
{{ 'access-control-execute' | translate }} {{ 'access-control-execute' | translate }}
</button> </button>
</div> </div>

View File

@@ -14,6 +14,7 @@ import {
} from 'rxjs/operators'; } from 'rxjs/operators';
import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer'; import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { BulkAccessBrowseComponent } from './browse/bulk-access-browse.component'; import { BulkAccessBrowseComponent } from './browse/bulk-access-browse.component';
@@ -27,6 +28,7 @@ import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.com
TranslateModule, TranslateModule,
BulkAccessSettingsComponent, BulkAccessSettingsComponent,
BulkAccessBrowseComponent, BulkAccessBrowseComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -42,6 +42,7 @@ import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { FormBuilderService } from '../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock';
@@ -151,7 +152,7 @@ describe('EPeopleRegistryComponent', () => {
paginationService = new PaginationServiceStub(); paginationService = new PaginationServiceStub();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule.withRoutes([]), imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule.withRoutes([]),
TranslateModule.forRoot(), EPeopleRegistryComponent], TranslateModule.forRoot(), EPeopleRegistryComponent, BtnDisabledDirective],
providers: [ providers: [
{ provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },

View File

@@ -25,7 +25,7 @@
</button> </button>
</div> </div>
<div *ngIf="displayResetPassword" between class="btn-group"> <div *ngIf="displayResetPassword" between class="btn-group">
<button class="btn btn-primary" [disabled]="(canReset$ | async) !== true" type="button" (click)="resetPassword()"> <button class="btn btn-primary" [dsBtnDisabled]="(canReset$ | async) !== true" type="button" (click)="resetPassword()">
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}} <i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
</button> </button>
</div> </div>

View File

@@ -43,6 +43,7 @@ import { GroupDataService } from '../../../core/eperson/group-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../../core/eperson/models/eperson.model';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { FormComponent } from '../../../shared/form/form.component'; import { FormComponent } from '../../../shared/form/form.component';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
@@ -221,7 +222,7 @@ describe('EPersonFormComponent', () => {
route = new ActivatedRouteStub(); route = new ActivatedRouteStub();
router = new RouterStub(); router = new RouterStub();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BtnDisabledDirective, BrowserModule,
RouterModule.forRoot([]), RouterModule.forRoot([]),
TranslateModule.forRoot(), TranslateModule.forRoot(),
EPersonFormComponent, EPersonFormComponent,
@@ -516,7 +517,8 @@ describe('EPersonFormComponent', () => {
// ePersonDataServiceStub.activeEPerson = eperson; // ePersonDataServiceStub.activeEPerson = eperson;
spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204)); spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204));
const deleteButton = fixture.debugElement.query(By.css('.delete-button')); const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
expect(deleteButton.nativeElement.disabled).toBe(false); expect(deleteButton.nativeElement.getAttribute('aria-disabled')).toBeNull();
expect(deleteButton.nativeElement.classList.contains('disabled')).toBeFalse();
deleteButton.triggerEventHandler('click', null); deleteButton.triggerEventHandler('click', null);
fixture.detectChanges(); fixture.detectChanges();
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson); expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);

View File

@@ -65,6 +65,7 @@ import {
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { Registration } from '../../../core/shared/registration.model'; import { Registration } from '../../../core/shared/registration.model';
import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component'; import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
@@ -92,6 +93,7 @@ import { ValidateEmailNotTaken } from './validators/email-taken.validator';
PaginationComponent, PaginationComponent,
RouterLink, RouterLink,
HasNoValuePipe, HasNoValuePipe,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -35,14 +35,14 @@
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<button (click)="deleteMemberFromGroup(epersonDTO.eperson)" <button (click)="deleteMemberFromGroup(epersonDTO.eperson)"
*ngIf="epersonDTO.ableToDelete" *ngIf="epersonDTO.ableToDelete"
[disabled]="actionConfig.remove.disabled" [dsBtnDisabled]="actionConfig.remove.disabled"
[ngClass]="['btn btn-sm', actionConfig.remove.css]" [ngClass]="['btn btn-sm', actionConfig.remove.css]"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}"> title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
<i [ngClass]="actionConfig.remove.icon"></i> <i [ngClass]="actionConfig.remove.icon"></i>
</button> </button>
<button *ngIf="!epersonDTO.ableToDelete" <button *ngIf="!epersonDTO.ableToDelete"
(click)="addMemberToGroup(epersonDTO.eperson)" (click)="addMemberToGroup(epersonDTO.eperson)"
[disabled]="actionConfig.add.disabled" [dsBtnDisabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]" [ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}"> title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i> <i [ngClass]="actionConfig.add.icon"></i>
@@ -122,7 +122,7 @@
<td class="align-middle"> <td class="align-middle">
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<button (click)="addMemberToGroup(eperson)" <button (click)="addMemberToGroup(eperson)"
[disabled]="actionConfig.add.disabled" [dsBtnDisabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]" [ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}"> title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i> <i [ngClass]="actionConfig.add.icon"></i>

View File

@@ -54,6 +54,7 @@ import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import { ContextHelpDirective } from '../../../../shared/context-help.directive'; import { ContextHelpDirective } from '../../../../shared/context-help.directive';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
@@ -113,6 +114,7 @@ export interface EPersonListActionConfig {
RouterLink, RouterLink,
NgClass, NgClass,
NgForOf, NgForOf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -69,7 +69,7 @@
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
<button *ngSwitchCase="false" <button *ngSwitchCase="false"
[disabled]="true" [dsBtnDisabled]="true"
class="btn btn-outline-primary btn-sm btn-edit" class="btn btn-outline-primary btn-sm btn-edit"
placement="left" placement="left"
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate" [ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"

View File

@@ -50,6 +50,7 @@ import { RouteService } from '../../core/services/route.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { NoContent } from '../../core/shared/NoContent.model'; import { NoContent } from '../../core/shared/NoContent.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { import {
DSONameServiceMock, DSONameServiceMock,
UNDEFINED_NAME, UNDEFINED_NAME,
@@ -208,6 +209,7 @@ describe('GroupsRegistryComponent', () => {
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot(), TranslateModule.forRoot(),
GroupsRegistryComponent, GroupsRegistryComponent,
BtnDisabledDirective,
], ],
providers: [GroupsRegistryComponent, providers: [GroupsRegistryComponent,
{ provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: DSONameService, useValue: new DSONameServiceMock() },
@@ -278,7 +280,8 @@ describe('GroupsRegistryComponent', () => {
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit')); const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
expect(editButtonsFound.length).toEqual(2); expect(editButtonsFound.length).toEqual(2);
editButtonsFound.forEach((editButtonFound) => { editButtonsFound.forEach((editButtonFound) => {
expect(editButtonFound.nativeElement.disabled).toBeFalse(); expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBeNull();
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeFalse();
}); });
}); });
@@ -312,7 +315,8 @@ describe('GroupsRegistryComponent', () => {
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit')); const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
expect(editButtonsFound.length).toEqual(2); expect(editButtonsFound.length).toEqual(2);
editButtonsFound.forEach((editButtonFound) => { editButtonsFound.forEach((editButtonFound) => {
expect(editButtonFound.nativeElement.disabled).toBeFalse(); expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBeNull();
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeFalse();
}); });
}); });
}); });
@@ -331,7 +335,8 @@ describe('GroupsRegistryComponent', () => {
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit')); const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
expect(editButtonsFound.length).toEqual(2); expect(editButtonsFound.length).toEqual(2);
editButtonsFound.forEach((editButtonFound) => { editButtonsFound.forEach((editButtonFound) => {
expect(editButtonFound.nativeElement.disabled).toBeTrue(); expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeTrue();
}); });
}); });
}); });

View File

@@ -62,6 +62,7 @@ import {
getRemoteDataPayload, getRemoteDataPayload,
} from '../../core/shared/operators'; } from '../../core/shared/operators';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
@@ -84,6 +85,7 @@ import { followLink } from '../../shared/utils/follow-link-config.model';
NgSwitchCase, NgSwitchCase,
NgbTooltipModule, NgbTooltipModule,
NgForOf, NgForOf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -54,7 +54,7 @@
<div class="col-auto"> <div class="col-auto">
<button class="btn btn-light" (click)="addQueryPredicate()">+</button> <button class="btn btn-light" (click)="addQueryPredicate()">+</button>
&nbsp; &nbsp;
<button class="btn btn-light" [disabled]="deleteQueryPredicateDisabled()" (click)="deleteQueryPredicate(i)"></button> <button class="btn btn-light" [dsBtnDisabled]="deleteQueryPredicateDisabled()" (click)="deleteQueryPredicate(i)"></button>
</div> </div>
</div> </div>
</div> </div>
@@ -158,8 +158,8 @@
{{'admin.reports.commons.page' | translate}} {{ currentPage + 1 }} {{'admin.reports.commons.of' | translate}} {{ pageCount() }} {{'admin.reports.commons.page' | translate}} {{ currentPage + 1 }} {{'admin.reports.commons.of' | translate}} {{ pageCount() }}
</div> </div>
<div> <div>
<button id="prev" class="btn btn-light" (click)="prevPage()" [disabled]="!canNavigatePrevious()">{{'admin.reports.commons.previous-page' | translate}}</button> <button id="prev" class="btn btn-light" (click)="prevPage()" [dsBtnDisabled]="!canNavigatePrevious()">{{'admin.reports.commons.previous-page' | translate}}</button>
<button id="next" class="btn btn-light" (click)="nextPage()" [disabled]="!canNavigateNext()">{{'admin.reports.commons.next-page' | translate}}</button> <button id="next" class="btn btn-light" (click)="nextPage()" [dsBtnDisabled]="!canNavigateNext()">{{'admin.reports.commons.next-page' | translate}}</button>
<!-- <!--
<button id="export">{{'admin.reports.commons.export' | translate}}</button> <button id="export">{{'admin.reports.commons.export' | translate}}</button>
--> -->

View File

@@ -43,6 +43,7 @@ import { getFirstSucceededRemoteListPayload } from 'src/app/core/shared/operator
import { isEmpty } from 'src/app/shared/empty.util'; import { isEmpty } from 'src/app/shared/empty.util';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { FiltersComponent } from '../filters-section/filters-section.component'; import { FiltersComponent } from '../filters-section/filters-section.component';
import { FilteredItems } from './filtered-items-model'; import { FilteredItems } from './filtered-items-model';
import { OptionVO } from './option-vo.model'; import { OptionVO } from './option-vo.model';
@@ -64,6 +65,7 @@ import { QueryPredicate } from './query-predicate.model';
NgIf, NgIf,
NgForOf, NgForOf,
FiltersComponent, FiltersComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -85,7 +85,7 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
this.sidebarActiveBg$ = this.variableService.getVariable('--ds-admin-sidebar-active-bg'); this.sidebarActiveBg$ = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
this.isSidebarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID); this.isSidebarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID);
this.isSidebarPreviewCollapsed$ = this.menuService.isMenuPreviewCollapsed(this.menuID); this.isSidebarPreviewCollapsed$ = this.menuService.isMenuPreviewCollapsed(this.menuID);
this.isExpanded$ = combineLatestObservable([this.active, this.isSidebarCollapsed$, this.isSidebarPreviewCollapsed$]).pipe( this.isExpanded$ = combineLatestObservable([this.active$, this.isSidebarCollapsed$, this.isSidebarPreviewCollapsed$]).pipe(
map(([active, sidebarCollapsed, sidebarPreviewCollapsed]) => (active && (!sidebarCollapsed || !sidebarPreviewCollapsed))), map(([active, sidebarCollapsed, sidebarPreviewCollapsed]) => (active && (!sidebarCollapsed || !sidebarPreviewCollapsed))),
); );
} }

View File

@@ -54,6 +54,7 @@ import {
} from './app-routes'; } from './app-routes';
import { BROWSE_BY_DECORATOR_MAP } from './browse-by/browse-by-switcher/browse-by-decorator'; import { BROWSE_BY_DECORATOR_MAP } from './browse-by/browse-by-switcher/browse-by-decorator';
import { AuthInterceptor } from './core/auth/auth.interceptor'; import { AuthInterceptor } from './core/auth/auth.interceptor';
import { DspaceRestInterceptor } from './core/dspace-rest/dspace-rest.interceptor';
import { LocaleInterceptor } from './core/locale/locale.interceptor'; import { LocaleInterceptor } from './core/locale/locale.interceptor';
import { LogInterceptor } from './core/log/log.interceptor'; import { LogInterceptor } from './core/log/log.interceptor';
import { import {
@@ -148,6 +149,11 @@ export const commonAppConfig: ApplicationConfig = {
useClass: LogInterceptor, useClass: LogInterceptor,
multi: true, multi: true,
}, },
{
provide: HTTP_INTERCEPTORS,
useClass: DspaceRestInterceptor,
multi: true,
},
// register the dynamic matcher used by form. MUST be provided by the app module // register the dynamic matcher used by form. MUST be provided by the app module
...DYNAMIC_MATCHER_PROVIDERS, ...DYNAMIC_MATCHER_PROVIDERS,
provideCore(), provideCore(),

View File

@@ -1,6 +1,6 @@
<ng-container *ngVar="(bitstreamRD$ | async) as bitstreamRD"> <ng-container *ngVar="(bitstreamRD$ | async) as bitstreamRD">
<div class="container" *ngVar="(bitstreamFormatsRD$ | async) as formatsRD"> <div class="container">
<div class="row" *ngIf="bitstreamRD?.hasSucceeded && formatsRD?.hasSucceeded"> <div class="row" *ngIf="bitstreamRD?.hasSucceeded">
<div class="col-md-2"> <div class="col-md-2">
<ds-thumbnail [thumbnail]="bitstreamRD?.payload"></ds-thumbnail> <ds-thumbnail [thumbnail]="bitstreamRD?.payload"></ds-thumbnail>
</div> </div>
@@ -27,7 +27,7 @@
</div> </div>
</div> </div>
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error> <ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
<ds-loading *ngIf="!bitstreamRD || !formatsRD || bitstreamRD?.isLoading || formatsRD?.isLoading" <ds-loading *ngIf="!bitstreamRD || bitstreamRD?.isLoading"
message="{{'loading.bitstream' | translate}}"></ds-loading> message="{{'loading.bitstream' | translate}}"></ds-loading>
</div> </div>
</ng-container> </ng-container>

View File

@@ -261,7 +261,7 @@ describe('EditBitstreamPageComponent', () => {
}); });
it('should select the correct format', () => { it('should select the correct format', () => {
expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.id); expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.shortDescription);
}); });
it('should put the \"New Format\" input on invisible', () => { it('should put the \"New Format\" input on invisible', () => {
@@ -292,7 +292,13 @@ describe('EditBitstreamPageComponent', () => {
describe('when an unknown format is selected', () => { describe('when an unknown format is selected', () => {
beforeEach(() => { beforeEach(() => {
comp.updateNewFormatLayout(allFormats[0].id); comp.onChange({
model: {
id: 'selectedFormat',
value: allFormats[0],
},
});
comp.updateNewFormatLayout();
}); });
it('should remove the invisible class from the \"New Format\" input', () => { it('should remove the invisible class from the \"New Format\" input', () => {
@@ -394,9 +400,10 @@ describe('EditBitstreamPageComponent', () => {
describe('when selected format has changed', () => { describe('when selected format has changed', () => {
beforeEach(() => { beforeEach(() => {
comp.formGroup.patchValue({ comp.onChange({
formatContainer: { model: {
selectedFormat: allFormats[2].id, id: 'selectedFormat',
value: allFormats[2],
}, },
}); });
fixture.detectChanges(); fixture.detectChanges();

View File

@@ -21,7 +21,6 @@ import {
DynamicFormLayout, DynamicFormLayout,
DynamicFormService, DynamicFormService,
DynamicInputModel, DynamicInputModel,
DynamicSelectModel,
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { import {
TranslateModule, TranslateModule,
@@ -39,23 +38,24 @@ import {
filter, filter,
map, map,
switchMap, switchMap,
take,
tap, tap,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { FindAllDataImpl } from '../../core/data/base/find-all-data';
import { BitstreamDataService } from '../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../core/data/bitstream-data.service';
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service'; import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service'; import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Bitstream } from '../../core/shared/bitstream.model'; import { Bitstream } from '../../core/shared/bitstream.model';
import { BitstreamFormat } from '../../core/shared/bitstream-format.model'; import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
import { BITSTREAM_FORMAT } from '../../core/shared/bitstream-format.resource-type';
import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level'; import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level';
import { Bundle } from '../../core/shared/bundle.model'; import { Bundle } from '../../core/shared/bundle.model';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { Metadata } from '../../core/shared/metadata.utils'; import { Metadata } from '../../core/shared/metadata.utils';
import { import {
getAllSucceededRemoteDataPayload,
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
@@ -72,6 +72,7 @@ import { ErrorComponent } from '../../shared/error/error.component';
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model'; import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
import { DynamicScrollableDropdownModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
import { FormComponent } from '../../shared/form/form.component'; import { FormComponent } from '../../shared/form/form.component';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
@@ -109,12 +110,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
*/ */
bitstreamRD$: Observable<RemoteData<Bitstream>>; bitstreamRD$: Observable<RemoteData<Bitstream>>;
/**
* The formats their remote data observable
* Tracks changes and updates the view
*/
bitstreamFormatsRD$: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
/** /**
* The UUID of the primary bitstream for this bundle * The UUID of the primary bitstream for this bundle
*/ */
@@ -130,11 +125,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
*/ */
originalFormat: BitstreamFormat; originalFormat: BitstreamFormat;
/**
* A list of all available bitstream formats
*/
formats: BitstreamFormat[];
/** /**
* @type {string} Key prefix used to generate form messages * @type {string} Key prefix used to generate form messages
*/ */
@@ -178,7 +168,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
/** /**
* Options for fetching all bitstream formats * Options for fetching all bitstream formats
*/ */
findAllOptions = { elementsPerPage: 9999 }; findAllOptions = {
elementsPerPage: 20,
currentPage: 1,
};
/** /**
* The Dynamic Input Model for the file's name * The Dynamic Input Model for the file's name
@@ -218,9 +211,22 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
/** /**
* The Dynamic Input Model for the selected format * The Dynamic Input Model for the selected format
*/ */
selectedFormatModel = new DynamicSelectModel({ selectedFormatModel = new DynamicScrollableDropdownModel({
id: 'selectedFormat', id: 'selectedFormat',
name: 'selectedFormat', name: 'selectedFormat',
displayKey: 'shortDescription',
repeatable: false,
metadataFields: [],
submissionId: '',
hasSelectableMetadata: false,
resourceType: BITSTREAM_FORMAT,
formatFunction: (format: BitstreamFormat | string) => {
if (format instanceof BitstreamFormat) {
return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown ? this.translate.instant(this.KEY_PREFIX + 'selectedFormat.unknown') : format.shortDescription;
} else {
return format;
}
},
}); });
/** /**
@@ -438,6 +444,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
* @private * @private
*/ */
private bundle: Bundle; private bundle: Bundle;
/**
* The currently selected format
* @private
*/
private selectedFormat: BitstreamFormat;
constructor(private route: ActivatedRoute, constructor(private route: ActivatedRoute,
private router: Router, private router: Router,
@@ -463,18 +474,12 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
this.itemId = this.route.snapshot.queryParams.itemId; this.itemId = this.route.snapshot.queryParams.itemId;
this.entityType = this.route.snapshot.queryParams.entityType; this.entityType = this.route.snapshot.queryParams.entityType;
this.bitstreamRD$ = this.route.data.pipe(map((data: any) => data.bitstream)); this.bitstreamRD$ = this.route.data.pipe(map((data: any) => data.bitstream));
this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions);
const bitstream$ = this.bitstreamRD$.pipe( const bitstream$ = this.bitstreamRD$.pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
); );
const allFormats$ = this.bitstreamFormatsRD$.pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
);
const bundle$ = bitstream$.pipe( const bundle$ = bitstream$.pipe(
switchMap((bitstream: Bitstream) => bitstream.bundle), switchMap((bitstream: Bitstream) => bitstream.bundle),
getFirstSucceededRemoteDataPayload(), getFirstSucceededRemoteDataPayload(),
@@ -490,24 +495,31 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
switchMap((bundle: Bundle) => bundle.item), switchMap((bundle: Bundle) => bundle.item),
getFirstSucceededRemoteDataPayload(), getFirstSucceededRemoteDataPayload(),
); );
const format$ = bitstream$.pipe(
switchMap(bitstream => bitstream.format),
getFirstSucceededRemoteDataPayload(),
);
this.subs.push( this.subs.push(
observableCombineLatest( observableCombineLatest(
bitstream$, bitstream$,
allFormats$,
bundle$, bundle$,
primaryBitstream$, primaryBitstream$,
item$, item$,
).pipe() format$,
.subscribe(([bitstream, allFormats, bundle, primaryBitstream, item]) => { ).subscribe(([bitstream, bundle, primaryBitstream, item, format]) => {
this.bitstream = bitstream as Bitstream; this.bitstream = bitstream as Bitstream;
this.formats = allFormats.page; this.bundle = bundle;
this.bundle = bundle; this.selectedFormat = format;
// hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will // hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will
// be a success response, but empty // be a success response, but empty
this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null; this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null;
this.itemId = item.uuid; this.itemId = item.uuid;
this.setIiifStatus(this.bitstream); this.setIiifStatus(this.bitstream);
}), }),
format$.pipe(take(1)).subscribe(
(format) => this.originalFormat = format,
),
); );
this.subs.push( this.subs.push(
@@ -523,7 +535,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
*/ */
setForm() { setForm() {
this.formGroup = this.formService.createFormGroup(this.formModel); this.formGroup = this.formService.createFormGroup(this.formModel);
this.updateFormatModel();
this.updateForm(this.bitstream); this.updateForm(this.bitstream);
this.updateFieldTranslations(); this.updateFieldTranslations();
} }
@@ -542,6 +553,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
description: bitstream.firstMetadataValue('dc.description'), description: bitstream.firstMetadataValue('dc.description'),
}, },
formatContainer: { formatContainer: {
selectedFormat: this.selectedFormat.shortDescription,
newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined, newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined,
}, },
}); });
@@ -561,36 +573,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
}, },
}); });
} }
this.bitstream.format.pipe( this.updateNewFormatLayout();
getAllSucceededRemoteDataPayload(),
).subscribe((format: BitstreamFormat) => {
this.originalFormat = format;
this.formGroup.patchValue({
formatContainer: {
selectedFormat: format.id,
},
});
this.updateNewFormatLayout(format.id);
});
} }
/**
* Create the list of unknown format IDs an add options to the selectedFormatModel
*/
updateFormatModel() {
this.selectedFormatModel.options = this.formats.map((format: BitstreamFormat) =>
Object.assign({
value: format.id,
label: this.isUnknownFormat(format.id) ? this.translate.instant(this.KEY_PREFIX + 'selectedFormat.unknown') : format.shortDescription,
}));
}
/** /**
* Update the layout of the "Other Format" input depending on the selected format * Update the layout of the "Other Format" input depending on the selected format
* @param selectedId * @param selectedId
*/ */
updateNewFormatLayout(selectedId: string) { updateNewFormatLayout() {
if (this.isUnknownFormat(selectedId)) { if (this.isUnknownFormat()) {
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout; this.formLayout.newFormat.grid.host = this.newFormatBaseLayout;
} else { } else {
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout + ' invisible'; this.formLayout.newFormat.grid.host = this.newFormatBaseLayout + ' invisible';
@@ -601,9 +593,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
* Is the provided format (id) part of the list of unknown formats? * Is the provided format (id) part of the list of unknown formats?
* @param id * @param id
*/ */
isUnknownFormat(id: string): boolean { isUnknownFormat(): boolean {
const format = this.formats.find((f: BitstreamFormat) => f.id === id); return hasValue(this.selectedFormat) && this.selectedFormat.supportLevel === BitstreamFormatSupportLevel.Unknown;
return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown;
} }
/** /**
@@ -635,7 +626,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
onChange(event) { onChange(event) {
const model = event.model; const model = event.model;
if (model.id === this.selectedFormatModel.id) { if (model.id === this.selectedFormatModel.id) {
this.updateNewFormatLayout(model.value); this.selectedFormat = model.value;
this.updateNewFormatLayout();
} }
} }
@@ -645,8 +637,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
onSubmit() { onSubmit() {
const updatedValues = this.formGroup.getRawValue(); const updatedValues = this.formGroup.getRawValue();
const updatedBitstream = this.formToBitstream(updatedValues); const updatedBitstream = this.formToBitstream(updatedValues);
const selectedFormat = this.formats.find((f: BitstreamFormat) => f.id === updatedValues.formatContainer.selectedFormat); const isNewFormat = this.selectedFormat.id !== this.originalFormat.id;
const isNewFormat = selectedFormat.id !== this.originalFormat.id;
const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream; const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream;
const wasPrimary = this.primaryBitstreamUUID === this.bitstream.uuid; const wasPrimary = this.primaryBitstreamUUID === this.bitstream.uuid;
@@ -698,7 +689,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
bundle$ = observableOf(this.bundle); bundle$ = observableOf(this.bundle);
} }
if (isNewFormat) { if (isNewFormat) {
bitstream$ = this.bitstreamService.updateFormat(this.bitstream, selectedFormat).pipe( bitstream$ = this.bitstreamService.updateFormat(this.bitstream, this.selectedFormat).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
map((formatResponse: RemoteData<Bitstream>) => { map((formatResponse: RemoteData<Bitstream>) => {
if (hasValue(formatResponse) && formatResponse.hasFailed) { if (hasValue(formatResponse) && formatResponse.hasFailed) {
@@ -856,4 +847,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
.forEach((subscription) => subscription.unsubscribe()); .forEach((subscription) => subscription.unsubscribe());
} }
findAllFormatsServiceFactory() {
return () => this.bitstreamFormatService as any as FindAllDataImpl<BitstreamFormat>;
}
} }

View File

@@ -2,10 +2,13 @@ import { CommonModule } from '@angular/common';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
NO_ERRORS_SCHEMA, NO_ERRORS_SCHEMA,
PLATFORM_ID,
} from '@angular/core'; } from '@angular/core';
import { import {
ComponentFixture, ComponentFixture,
fakeAsync,
TestBed, TestBed,
tick,
waitForAsync, waitForAsync,
} from '@angular/core/testing'; } from '@angular/core/testing';
import { import {
@@ -26,6 +29,7 @@ import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-
import { SortDirection } from '../../core/cache/models/sort-options.model'; import { SortDirection } from '../../core/cache/models/sort-options.model';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { BrowseEntry } from '../../core/shared/browse-entry.model';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component'; import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component';
@@ -123,6 +127,7 @@ describe('BrowseByDateComponent', () => {
{ provide: ChangeDetectorRef, useValue: mockCdRef }, { provide: ChangeDetectorRef, useValue: mockCdRef },
{ provide: Store, useValue: {} }, { provide: Store, useValue: {} },
{ provide: APP_CONFIG, useValue: environment }, { provide: APP_CONFIG, useValue: environment },
{ provide: PLATFORM_ID, useValue: 'browser' },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}) })
@@ -172,4 +177,33 @@ describe('BrowseByDateComponent', () => {
//expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear()); //expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear());
expect(comp.startsWithOptions[0]).toEqual(1960); expect(comp.startsWithOptions[0]).toEqual(1960);
}); });
describe('when rendered in SSR', () => {
beforeEach(() => {
comp.platformId = 'server';
spyOn((comp as any).browseService, 'getBrowseItemsFor');
});
it('should not call getBrowseItemsFor on init', (done) => {
comp.ngOnInit();
expect((comp as any).browseService.getBrowseItemsFor).not.toHaveBeenCalled();
comp.loading$.subscribe((res) => {
expect(res).toBeFalsy();
done();
});
});
});
describe('when rendered in CSR', () => {
beforeEach(() => {
comp.platformId = 'browser';
spyOn((comp as any).browseService, 'getBrowseItemsFor').and.returnValue(createSuccessfulRemoteDataObject$(new BrowseEntry()));
});
it('should call getBrowseItemsFor on init', fakeAsync(() => {
comp.ngOnInit();
tick(100);
expect((comp as any).browseService.getBrowseItemsFor).toHaveBeenCalled();
}));
});
}); });

View File

@@ -1,5 +1,6 @@
import { import {
AsyncPipe, AsyncPipe,
isPlatformServer,
NgIf, NgIf,
} from '@angular/common'; } from '@angular/common';
import { import {
@@ -7,6 +8,7 @@ import {
Component, Component,
Inject, Inject,
OnInit, OnInit,
PLATFORM_ID,
} from '@angular/core'; } from '@angular/core';
import { import {
ActivatedRoute, ActivatedRoute,
@@ -17,10 +19,11 @@ import { TranslateModule } from '@ngx-translate/core';
import { import {
combineLatest as observableCombineLatest, combineLatest as observableCombineLatest,
Observable, Observable,
of as observableOf,
} from 'rxjs'; } from 'rxjs';
import { import {
distinctUntilChanged,
map, map,
take,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component'; import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component';
@@ -28,6 +31,7 @@ import {
APP_CONFIG, APP_CONFIG,
AppConfig, AppConfig,
} from '../../../config/app-config.interface'; } from '../../../config/app-config.interface';
import { environment } from '../../../environments/environment';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { BrowseService } from '../../core/browse/browse.service'; import { BrowseService } from '../../core/browse/browse.service';
import { import {
@@ -38,13 +42,7 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { ThemedComcolPageBrowseByComponent } from '../../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component';
import { ThemedComcolPageContentComponent } from '../../shared/comcol/comcol-page-content/themed-comcol-page-content.component';
import { ThemedComcolPageHandleComponent } from '../../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component';
import { ComcolPageHeaderComponent } from '../../shared/comcol/comcol-page-header/comcol-page-header.component';
import { ComcolPageLogoComponent } from '../../shared/comcol/comcol-page-logo/comcol-page-logo.component';
import { isValidDate } from '../../shared/date.util'; import { isValidDate } from '../../shared/date.util';
import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
import { import {
hasValue, hasValue,
isNotEmpty, isNotEmpty,
@@ -52,7 +50,6 @@ import {
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { StartsWithType } from '../../shared/starts-with/starts-with-type'; import { StartsWithType } from '../../shared/starts-with/starts-with-type';
import { VarDirective } from '../../shared/utils/var.directive';
import { import {
BrowseByMetadataComponent, BrowseByMetadataComponent,
browseParamsToOptions, browseParamsToOptions,
@@ -64,15 +61,8 @@ import {
templateUrl: '../browse-by-metadata/browse-by-metadata.component.html', templateUrl: '../browse-by-metadata/browse-by-metadata.component.html',
standalone: true, standalone: true,
imports: [ imports: [
VarDirective,
AsyncPipe, AsyncPipe,
ComcolPageHeaderComponent,
ComcolPageLogoComponent,
NgIf, NgIf,
ThemedComcolPageHandleComponent,
ThemedComcolPageContentComponent,
DsoEditMenuComponent,
ThemedComcolPageBrowseByComponent,
TranslateModule, TranslateModule,
ThemedLoadingComponent, ThemedLoadingComponent,
ThemedBrowseByComponent, ThemedBrowseByComponent,
@@ -99,27 +89,34 @@ export class BrowseByDateComponent extends BrowseByMetadataComponent implements
@Inject(APP_CONFIG) public appConfig: AppConfig, @Inject(APP_CONFIG) public appConfig: AppConfig,
public dsoNameService: DSONameService, public dsoNameService: DSONameService,
protected cdRef: ChangeDetectorRef, protected cdRef: ChangeDetectorRef,
@Inject(PLATFORM_ID) public platformId: any,
) { ) {
super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService); super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService, platformId);
} }
ngOnInit(): void { ngOnInit(): void {
if (!this.renderOnServerSide && !environment.ssr.enableBrowseComponent && isPlatformServer(this.platformId)) {
this.loading$ = observableOf(false);
return;
}
const sortConfig = new SortOptions('default', SortDirection.ASC); const sortConfig = new SortOptions('default', SortDirection.ASC);
this.startsWithType = StartsWithType.date; this.startsWithType = StartsWithType.date;
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
const routeParams$: Observable<Params> = observableCombineLatest([
this.route.params,
this.route.queryParams,
]).pipe(
map(([params, queryParams]: [Params, Params]) => Object.assign({}, params, queryParams)),
distinctUntilChanged((prev: Params, curr: Params) => prev.id === curr.id && prev.startsWith === curr.startsWith),
);
this.subs.push( this.subs.push(
observableCombineLatest( observableCombineLatest([
[ this.route.params.pipe(take(1)), routeParams$,
this.route.queryParams, this.scope$,
this.scope$, this.currentPagination$,
this.currentPagination$, this.currentSort$,
this.currentSort$, ]).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
]).pipe(
map(([routeParams, queryParams, scope, currentPage, currentSort]) => {
return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort];
}),
).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys; const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys;
this.browseId = params.id || this.defaultBrowseId; this.browseId = params.id || this.defaultBrowseId;
this.startsWith = +params.startsWith || params.startsWith; this.startsWith = +params.startsWith || params.startsWith;

View File

@@ -1,4 +1,4 @@
<section class="comcol-page-browse-section"> <section class="comcol-page-browse-section" *ngIf="(!ssrRenderingDisabled)">
<div class="browse-by-metadata w-100"> <div class="browse-by-metadata w-100">
<ds-browse-by *ngIf="(loading$ | async) !== true" class="col-xs-12 w-100" <ds-browse-by *ngIf="(loading$ | async) !== true" class="col-xs-12 w-100"
title="{{'browse.title' | translate:{ title="{{'browse.title' | translate:{

View File

@@ -1,8 +1,13 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import {
NO_ERRORS_SCHEMA,
PLATFORM_ID,
} from '@angular/core';
import { import {
ComponentFixture, ComponentFixture,
fakeAsync,
TestBed, TestBed,
tick,
waitForAsync, waitForAsync,
} from '@angular/core/testing'; } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@@ -147,6 +152,7 @@ describe('BrowseByMetadataComponent', () => {
{ provide: ThemeService, useValue: getMockThemeService() }, { provide: ThemeService, useValue: getMockThemeService() },
{ provide: SelectableListService, useValue: {} }, { provide: SelectableListService, useValue: {} },
{ provide: HostWindowService, useValue: {} }, { provide: HostWindowService, useValue: {} },
{ provide: PLATFORM_ID, useValue: 'browser' },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}) })
@@ -259,6 +265,35 @@ describe('BrowseByMetadataComponent', () => {
expect(result.fetchThumbnail).toBeTrue(); expect(result.fetchThumbnail).toBeTrue();
}); });
}); });
describe('when rendered in SSR', () => {
beforeEach(() => {
comp.ssrRenderingDisabled = true;
spyOn((comp as any).browseService, 'getBrowseEntriesFor').and.returnValue(createSuccessfulRemoteDataObject$(null));
});
it('should not call getBrowseEntriesFor on init', (done) => {
comp.ngOnInit();
expect((comp as any).browseService.getBrowseEntriesFor).not.toHaveBeenCalled();
comp.loading$.subscribe((res) => {
expect(res).toBeFalsy();
done();
});
});
});
describe('when rendered in CSR', () => {
beforeEach(() => {
comp.ssrRenderingDisabled = false;
spyOn((comp as any).browseService, 'getBrowseEntriesFor').and.returnValue(createSuccessfulRemoteDataObject$(new BrowseEntry()));
});
it('should call getBrowseEntriesFor on init', fakeAsync(() => {
comp.ngOnInit();
tick(100);
expect((comp as any).browseService.getBrowseEntriesFor).toHaveBeenCalled();
}));
});
}); });
export function toRemoteData(objects: any[]): Observable<RemoteData<PaginatedList<any>>> { export function toRemoteData(objects: any[]): Observable<RemoteData<PaginatedList<any>>> {

View File

@@ -1,5 +1,6 @@
import { import {
AsyncPipe, AsyncPipe,
isPlatformServer,
NgIf, NgIf,
} from '@angular/common'; } from '@angular/common';
import { import {
@@ -9,6 +10,8 @@ import {
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
PLATFORM_ID,
SimpleChanges,
} from '@angular/core'; } from '@angular/core';
import { import {
ActivatedRoute, ActivatedRoute,
@@ -24,8 +27,8 @@ import {
Subscription, Subscription,
} from 'rxjs'; } from 'rxjs';
import { import {
distinctUntilChanged,
map, map,
take,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component'; import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component';
@@ -33,6 +36,7 @@ import {
APP_CONFIG, APP_CONFIG,
AppConfig, AppConfig,
} from '../../../config/app-config.interface'; } from '../../../config/app-config.interface';
import { environment } from '../../../environments/environment';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { BrowseService } from '../../core/browse/browse.service'; import { BrowseService } from '../../core/browse/browse.service';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model'; import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
@@ -48,12 +52,6 @@ import { BrowseEntry } from '../../core/shared/browse-entry.model';
import { Context } from '../../core/shared/context.model'; import { Context } from '../../core/shared/context.model';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { getFirstSucceededRemoteData } from '../../core/shared/operators'; import { getFirstSucceededRemoteData } from '../../core/shared/operators';
import { ThemedComcolPageBrowseByComponent } from '../../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component';
import { ThemedComcolPageContentComponent } from '../../shared/comcol/comcol-page-content/themed-comcol-page-content.component';
import { ThemedComcolPageHandleComponent } from '../../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component';
import { ComcolPageHeaderComponent } from '../../shared/comcol/comcol-page-header/comcol-page-header.component';
import { ComcolPageLogoComponent } from '../../shared/comcol/comcol-page-logo/comcol-page-logo.component';
import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
import { import {
hasValue, hasValue,
isNotEmpty, isNotEmpty,
@@ -61,7 +59,6 @@ import {
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { StartsWithType } from '../../shared/starts-with/starts-with-type'; import { StartsWithType } from '../../shared/starts-with/starts-with-type';
import { VarDirective } from '../../shared/utils/var.directive';
import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type'; import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type';
export const BBM_PAGINATION_ID = 'bbm'; export const BBM_PAGINATION_ID = 'bbm';
@@ -71,15 +68,8 @@ export const BBM_PAGINATION_ID = 'bbm';
styleUrls: ['./browse-by-metadata.component.scss'], styleUrls: ['./browse-by-metadata.component.scss'],
templateUrl: './browse-by-metadata.component.html', templateUrl: './browse-by-metadata.component.html',
imports: [ imports: [
VarDirective,
AsyncPipe, AsyncPipe,
ComcolPageHeaderComponent,
ComcolPageLogoComponent,
NgIf, NgIf,
ThemedComcolPageHandleComponent,
ThemedComcolPageContentComponent,
DsoEditMenuComponent,
ThemedComcolPageBrowseByComponent,
TranslateModule, TranslateModule,
ThemedLoadingComponent, ThemedLoadingComponent,
ThemedBrowseByComponent, ThemedBrowseByComponent,
@@ -114,6 +104,11 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
*/ */
@Input() displayTitle = true; @Input() displayTitle = true;
/**
* Defines whether to fetch search results during SSR execution
*/
@Input() renderOnServerSide: boolean;
scope$: BehaviorSubject<string> = new BehaviorSubject(undefined); scope$: BehaviorSubject<string> = new BehaviorSubject(undefined);
/** /**
@@ -194,6 +189,10 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
* Observable determining if the loading animation needs to be shown * Observable determining if the loading animation needs to be shown
*/ */
loading$ = observableOf(true); loading$ = observableOf(true);
/**
* Whether this component should be rendered or not in SSR
*/
ssrRenderingDisabled = false;
public constructor(protected route: ActivatedRoute, public constructor(protected route: ActivatedRoute,
protected browseService: BrowseService, protected browseService: BrowseService,
@@ -202,6 +201,7 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
protected router: Router, protected router: Router,
@Inject(APP_CONFIG) public appConfig: AppConfig, @Inject(APP_CONFIG) public appConfig: AppConfig,
public dsoNameService: DSONameService, public dsoNameService: DSONameService,
@Inject(PLATFORM_ID) public platformId: any,
) { ) {
this.fetchThumbnails = this.appConfig.browseBy.showThumbnails; this.fetchThumbnails = this.appConfig.browseBy.showThumbnails;
this.paginationConfig = Object.assign(new PaginationComponentOptions(), { this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
@@ -209,27 +209,32 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
currentPage: 1, currentPage: 1,
pageSize: this.appConfig.browseBy.pageSize, pageSize: this.appConfig.browseBy.pageSize,
}); });
this.ssrRenderingDisabled = !this.renderOnServerSide && !environment.ssr.enableBrowseComponent && isPlatformServer(this.platformId);
} }
ngOnInit(): void { ngOnInit(): void {
if (this.ssrRenderingDisabled) {
this.loading$ = observableOf(false);
return;
}
const sortConfig = new SortOptions('default', SortDirection.ASC); const sortConfig = new SortOptions('default', SortDirection.ASC);
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
const routeParams$: Observable<Params> = observableCombineLatest([
this.route.params,
this.route.queryParams,
]).pipe(
map(([params, queryParams]: [Params, Params]) => Object.assign({}, params, queryParams)),
distinctUntilChanged((prev: Params, curr: Params) => prev.id === curr.id && prev.authority === curr.authority && prev.value === curr.value && prev.startsWith === curr.startsWith),
);
this.subs.push( this.subs.push(
observableCombineLatest( observableCombineLatest([
[ this.route.params.pipe(take(1)), routeParams$,
this.route.queryParams, this.scope$,
this.scope$, this.currentPagination$,
this.currentPagination$, this.currentSort$,
this.currentSort$, ]).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
]).pipe(
map(([routeParams, queryParams, scope, currentPage, currentSort]) => {
return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort];
}),
).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
this.browseId = params.id || this.defaultBrowseId; this.browseId = params.id || this.defaultBrowseId;
this.authority = params.authority; this.authority = params.authority;
@@ -257,8 +262,10 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
} }
ngOnChanges(): void { ngOnChanges(changes: SimpleChanges): void {
this.scope$.next(this.scope); if (hasValue(changes.scope)) {
this.scope$.next(this.scope);
}
} }
/** /**
@@ -336,7 +343,6 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
this.paginationService.clearPagination(this.paginationConfig.id); this.paginationService.clearPagination(this.paginationConfig.id);
} }
} }
/** /**

View File

@@ -5,7 +5,9 @@ import {
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { import {
ComponentFixture, ComponentFixture,
fakeAsync,
TestBed, TestBed,
tick,
waitForAsync, waitForAsync,
} from '@angular/core/testing'; } from '@angular/core/testing';
import { import {
@@ -23,6 +25,7 @@ import { BrowseService } from '../../core/browse/browse.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { ItemDataService } from '../../core/data/item-data.service'; import { ItemDataService } from '../../core/data/item-data.service';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { BrowseEntry } from '../../core/shared/browse-entry.model';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component'; import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component';
@@ -81,6 +84,7 @@ describe('BrowseByTitleComponent', () => {
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), { const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
params: observableOf({}), params: observableOf({}),
queryParams: observableOf({}),
data: observableOf({ metadata: 'title' }), data: observableOf({ metadata: 'title' }),
}); });
@@ -127,4 +131,35 @@ describe('BrowseByTitleComponent', () => {
expect(result.payload.page).toEqual(mockItems); expect(result.payload.page).toEqual(mockItems);
}); });
}); });
describe('when rendered in SSR', () => {
beforeEach(() => {
comp.platformId = 'server';
spyOn((comp as any).browseService, 'getBrowseItemsFor');
fixture.detectChanges();
});
it('should not call getBrowseItemsFor on init', (done) => {
comp.ngOnInit();
expect((comp as any).browseService.getBrowseItemsFor).not.toHaveBeenCalled();
comp.loading$.subscribe((res) => {
expect(res).toBeFalsy();
done();
});
});
});
describe('when rendered in CSR', () => {
beforeEach(() => {
comp.platformId = 'browser';
fixture.detectChanges();
spyOn((comp as any).browseService, 'getBrowseItemsFor').and.returnValue(createSuccessfulRemoteDataObject$(new BrowseEntry()));
});
it('should call getBrowseItemsFor on init', fakeAsync(() => {
comp.ngOnInit();
tick(100);
expect((comp as any).browseService.getBrowseItemsFor).toHaveBeenCalled();
}));
});
}); });

View File

@@ -1,5 +1,6 @@
import { import {
AsyncPipe, AsyncPipe,
isPlatformServer,
NgIf, NgIf,
} from '@angular/common'; } from '@angular/common';
import { import {
@@ -8,26 +9,24 @@ import {
} from '@angular/core'; } from '@angular/core';
import { Params } from '@angular/router'; import { Params } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { combineLatest as observableCombineLatest } from 'rxjs';
import { import {
combineLatest as observableCombineLatest,
Observable,
of as observableOf,
} from 'rxjs';
import {
distinctUntilChanged,
map, map,
take,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { environment } from '../../../environments/environment';
import { import {
SortDirection, SortDirection,
SortOptions, SortOptions,
} from '../../core/cache/models/sort-options.model'; } from '../../core/cache/models/sort-options.model';
import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component'; import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component';
import { ThemedComcolPageBrowseByComponent } from '../../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component';
import { ThemedComcolPageContentComponent } from '../../shared/comcol/comcol-page-content/themed-comcol-page-content.component';
import { ThemedComcolPageHandleComponent } from '../../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component';
import { ComcolPageHeaderComponent } from '../../shared/comcol/comcol-page-header/comcol-page-header.component';
import { ComcolPageLogoComponent } from '../../shared/comcol/comcol-page-logo/comcol-page-logo.component';
import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { VarDirective } from '../../shared/utils/var.directive';
import { import {
BrowseByMetadataComponent, BrowseByMetadataComponent,
browseParamsToOptions, browseParamsToOptions,
@@ -39,15 +38,8 @@ import {
templateUrl: '../browse-by-metadata/browse-by-metadata.component.html', templateUrl: '../browse-by-metadata/browse-by-metadata.component.html',
standalone: true, standalone: true,
imports: [ imports: [
VarDirective,
AsyncPipe, AsyncPipe,
ComcolPageHeaderComponent,
ComcolPageLogoComponent,
NgIf, NgIf,
ThemedComcolPageHandleComponent,
ThemedComcolPageContentComponent,
DsoEditMenuComponent,
ThemedComcolPageBrowseByComponent,
TranslateModule, TranslateModule,
ThemedLoadingComponent, ThemedLoadingComponent,
ThemedBrowseByComponent, ThemedBrowseByComponent,
@@ -59,21 +51,27 @@ import {
export class BrowseByTitleComponent extends BrowseByMetadataComponent implements OnInit { export class BrowseByTitleComponent extends BrowseByMetadataComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
if (!this.renderOnServerSide && !environment.ssr.enableBrowseComponent && isPlatformServer(this.platformId)) {
this.loading$ = observableOf(false);
return;
}
const sortConfig = new SortOptions('dc.title', SortDirection.ASC); const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
const routeParams$: Observable<Params> = observableCombineLatest([
this.route.params,
this.route.queryParams,
]).pipe(
map(([params, queryParams]: [Params, Params]) => Object.assign({}, params, queryParams)),
distinctUntilChanged((prev: Params, curr: Params) => prev.id === curr.id && prev.startsWith === curr.startsWith),
);
this.subs.push( this.subs.push(
observableCombineLatest( observableCombineLatest([
[ this.route.params.pipe(take(1)), routeParams$,
this.route.queryParams, this.scope$,
this.scope$, this.currentPagination$,
this.currentPagination$, this.currentSort$,
this.currentSort$, ]).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
]).pipe(
map(([routeParams, queryParams, scope, currentPage, currentSort]) => {
return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort];
}),
).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
this.startsWith = +params.startsWith || params.startsWith; this.startsWith = +params.startsWith || params.startsWith;
this.browseId = params.id || this.defaultBrowseId; this.browseId = params.id || this.defaultBrowseId;
this.updatePageWithItems(browseParamsToOptions(params, scope, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined); this.updatePageWithItems(browseParamsToOptions(params, scope, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined);

View File

@@ -6,10 +6,10 @@
<p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p> <p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p>
<div class="form-group row"> <div class="form-group row">
<div class="col text-right space-children-mr"> <div class="col text-right space-children-mr">
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)"> <button class="btn btn-outline-secondary" (click)="onCancel(dso)" [dsBtnDisabled]="(processing$ | async)">
<i class="fas fa-times"></i> {{'collection.delete.cancel' | translate}} <i class="fas fa-times"></i> {{'collection.delete.cancel' | translate}}
</button> </button>
<button class="btn btn-danger" (click)="onConfirm(dso)" [disabled]="(processing$ | async)"> <button class="btn btn-danger" (click)="onConfirm(dso)" [dsBtnDisabled]="(processing$ | async)">
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'collection.delete.processing' | translate}}</span> <span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'collection.delete.processing' | translate}}</span>
<span *ngIf="(processing$ | async) !== true"><i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}</span> <span *ngIf="(processing$ | async) !== true"><i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}</span>
</button> </button>

View File

@@ -15,6 +15,7 @@ import {
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component'; import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
@@ -31,6 +32,7 @@ import { VarDirective } from '../../shared/utils/var.directive';
AsyncPipe, AsyncPipe,
NgIf, NgIf,
VarDirective, VarDirective,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -19,32 +19,32 @@
</div> </div>
<button *ngIf="(testConfigRunning$ |async) !== true" class="btn btn-secondary" <button *ngIf="(testConfigRunning$ |async) !== true" class="btn btn-secondary"
[disabled]="!(isEnabled)" [dsBtnDisabled]="!(isEnabled)"
(click)="testConfiguration(contentSource)"> (click)="testConfiguration(contentSource)">
<span>{{'collection.source.controls.test.submit' | translate}}</span> <span>{{'collection.source.controls.test.submit' | translate}}</span>
</button> </button>
<button *ngIf="(testConfigRunning$ |async)" class="btn btn-secondary" <button *ngIf="(testConfigRunning$ |async)" class="btn btn-secondary"
[disabled]="true"> [dsBtnDisabled]="true">
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
<span>{{'collection.source.controls.test.running' | translate}}</span> <span>{{'collection.source.controls.test.running' | translate}}</span>
</button> </button>
<button *ngIf="(importRunning$ |async) !== true" class="btn btn-primary" <button *ngIf="(importRunning$ |async) !== true" class="btn btn-primary"
[disabled]="!(isEnabled)" [dsBtnDisabled]="!(isEnabled)"
(click)="importNow()"> (click)="importNow()">
<span class="d-none d-sm-inline">{{'collection.source.controls.import.submit' | translate}}</span> <span class="d-none d-sm-inline">{{'collection.source.controls.import.submit' | translate}}</span>
</button> </button>
<button *ngIf="(importRunning$ |async)" class="btn btn-primary" <button *ngIf="(importRunning$ |async)" class="btn btn-primary"
[disabled]="true"> [dsBtnDisabled]="true">
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
<span class="d-none d-sm-inline">{{'collection.source.controls.import.running' | translate}}</span> <span class="d-none d-sm-inline">{{'collection.source.controls.import.running' | translate}}</span>
</button> </button>
<button *ngIf="(reImportRunning$ |async) !== true" class="btn btn-primary" <button *ngIf="(reImportRunning$ |async) !== true" class="btn btn-primary"
[disabled]="!(isEnabled)" [dsBtnDisabled]="!(isEnabled)"
(click)="resetAndReimport()"> (click)="resetAndReimport()">
<span class="d-none d-sm-inline">&nbsp;{{'collection.source.controls.reset.submit' | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{'collection.source.controls.reset.submit' | translate}}</span>
</button> </button>
<button *ngIf="(reImportRunning$ |async)" class="btn btn-primary" <button *ngIf="(reImportRunning$ |async)" class="btn btn-primary"
[disabled]="true"> [dsBtnDisabled]="true">
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
<span class="d-none d-sm-inline">&nbsp;{{'collection.source.controls.reset.running' | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{'collection.source.controls.reset.running' | translate}}</span>
</button> </button>

View File

@@ -22,6 +22,7 @@ import { Collection } from '../../../../core/shared/collection.model';
import { ContentSource } from '../../../../core/shared/content-source.model'; import { ContentSource } from '../../../../core/shared/content-source.model';
import { ContentSourceSetSerializer } from '../../../../core/shared/content-source-set-serializer'; import { ContentSourceSetSerializer } from '../../../../core/shared/content-source-set-serializer';
import { Process } from '../../../../process-page/processes/process.model'; import { Process } from '../../../../process-page/processes/process.model';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
@@ -104,7 +105,7 @@ describe('CollectionSourceControlsComponent', () => {
requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring', 'setStaleByHrefSubstring']); requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring', 'setStaleByHrefSubstring']);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule, CollectionSourceControlsComponent, VarDirective], imports: [TranslateModule.forRoot(), RouterTestingModule, CollectionSourceControlsComponent, VarDirective, BtnDisabledDirective],
providers: [ providers: [
{ provide: ScriptDataService, useValue: scriptDataService }, { provide: ScriptDataService, useValue: scriptDataService },
{ provide: ProcessDataService, useValue: processDataService }, { provide: ProcessDataService, useValue: processDataService },
@@ -193,9 +194,10 @@ describe('CollectionSourceControlsComponent', () => {
const buttons = fixture.debugElement.queryAll(By.css('button')); const buttons = fixture.debugElement.queryAll(By.css('button'));
expect(buttons[0].nativeElement.disabled).toBeTrue(); buttons.forEach(button => {
expect(buttons[1].nativeElement.disabled).toBeTrue(); expect(button.nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(buttons[2].nativeElement.disabled).toBeTrue(); expect(button.nativeElement.classList.contains('disabled')).toBeTrue();
});
}); });
it('should be enabled when isEnabled is true', () => { it('should be enabled when isEnabled is true', () => {
comp.shouldShow = true; comp.shouldShow = true;
@@ -205,9 +207,10 @@ describe('CollectionSourceControlsComponent', () => {
const buttons = fixture.debugElement.queryAll(By.css('button')); const buttons = fixture.debugElement.queryAll(By.css('button'));
expect(buttons[0].nativeElement.disabled).toBeFalse(); buttons.forEach(button => {
expect(buttons[1].nativeElement.disabled).toBeFalse(); expect(button.nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(buttons[2].nativeElement.disabled).toBeFalse(); expect(button.nativeElement.classList.contains('disabled')).toBeFalse();
});
}); });
it('should call the corresponding button when clicked', () => { it('should call the corresponding button when clicked', () => {
spyOn(comp, 'testConfiguration'); spyOn(comp, 'testConfiguration');

View File

@@ -40,6 +40,7 @@ import {
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { Process } from '../../../../process-page/processes/process.model'; import { Process } from '../../../../process-page/processes/process.model';
import { ProcessStatus } from '../../../../process-page/processes/process-status.model'; import { ProcessStatus } from '../../../../process-page/processes/process-status.model';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import { hasValue } from '../../../../shared/empty.util'; import { hasValue } from '../../../../shared/empty.util';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { VarDirective } from '../../../../shared/utils/var.directive'; import { VarDirective } from '../../../../shared/utils/var.directive';
@@ -56,6 +57,7 @@ import { VarDirective } from '../../../../shared/utils/var.directive';
AsyncPipe, AsyncPipe,
NgIf, NgIf,
VarDirective, VarDirective,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -1,7 +1,7 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="d-inline-block float-right space-children-mr"> <div class="d-inline-block float-right space-children-mr">
<button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true" <button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
[disabled]="(hasChanges$ | async) !== true" [dsBtnDisabled]="(hasChanges$ | async) !== true"
(click)="discard()"><i (click)="discard()"><i
class="fas fa-times"></i> class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
@@ -12,7 +12,7 @@
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span>
</button> </button>
<button class="btn btn-primary" <button class="btn btn-primary"
[disabled]="(hasChanges$ | async) !== true || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)" [dsBtnDisabled]="(hasChanges$ | async) !== true || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
(click)="onSubmit()"><i (click)="onSubmit()"><i
class="fas fa-save"></i> class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>
@@ -45,7 +45,7 @@
<div class="col-12"> <div class="col-12">
<div class="d-inline-block float-right ml-1 space-children-mr"> <div class="d-inline-block float-right ml-1 space-children-mr">
<button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true" <button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
[disabled]="(hasChanges$ | async) !== true" [dsBtnDisabled]="(hasChanges$ | async) !== true"
(click)="discard()"><i (click)="discard()"><i
class="fas fa-times"></i> class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
@@ -56,7 +56,7 @@
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span>
</button> </button>
<button class="btn btn-primary" <button class="btn btn-primary"
[disabled]="(hasChanges$ | async) !== true || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)" [dsBtnDisabled]="(hasChanges$ | async) !== true || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
(click)="onSubmit()"><i (click)="onSubmit()"><i
class="fas fa-save"></i> class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>

View File

@@ -56,6 +56,7 @@ import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { import {
hasNoValue, hasNoValue,
hasValue, hasValue,
@@ -81,6 +82,7 @@ import { CollectionSourceControlsComponent } from './collection-source-controls/
ThemedLoadingComponent, ThemedLoadingComponent,
FormComponent, FormComponent,
CollectionSourceControlsComponent, CollectionSourceControlsComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -6,10 +6,10 @@
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p> <p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p>
<div class="form-group row"> <div class="form-group row">
<div class="col text-right space-children-mr"> <div class="col text-right space-children-mr">
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)"> <button class="btn btn-outline-secondary" (click)="onCancel(dso)" [dsBtnDisabled]="(processing$ | async)">
<i class="fas fa-times" aria-hidden="true"></i> {{'community.delete.cancel' | translate}} <i class="fas fa-times" aria-hidden="true"></i> {{'community.delete.cancel' | translate}}
</button> </button>
<button class="btn btn-danger" (click)="onConfirm(dso)" [disabled]="(processing$ | async)"> <button class="btn btn-danger" (click)="onConfirm(dso)" [dsBtnDisabled]="(processing$ | async)">
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin' aria-hidden="true"></i> {{'community.delete.processing' | translate}}</span> <span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin' aria-hidden="true"></i> {{'community.delete.processing' | translate}}</span>
<span *ngIf="(processing$ | async) !== true"><i class="fas fa-trash" aria-hidden="true"></i> {{'community.delete.confirm' | translate}}</span> <span *ngIf="(processing$ | async) !== true"><i class="fas fa-trash" aria-hidden="true"></i> {{'community.delete.confirm' | translate}}</span>
</button> </button>

View File

@@ -15,6 +15,7 @@ import {
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component'; import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
@@ -31,6 +32,7 @@ import { VarDirective } from '../../shared/utils/var.directive';
AsyncPipe, AsyncPipe,
VarDirective, VarDirective,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -129,12 +129,24 @@ export class AuthInterceptor implements HttpInterceptor {
*/ */
private sortAuthMethods(authMethodModels: AuthMethod[]): AuthMethod[] { private sortAuthMethods(authMethodModels: AuthMethod[]): AuthMethod[] {
const sortedAuthMethodModels: AuthMethod[] = []; const sortedAuthMethodModels: AuthMethod[] = [];
let passwordAuthFound = false;
let ldapAuthFound = false;
authMethodModels.forEach((method) => { authMethodModels.forEach((method) => {
if (method.authMethodType === AuthMethodType.Password) { if (method.authMethodType === AuthMethodType.Password) {
sortedAuthMethodModels.push(method); sortedAuthMethodModels.push(method);
passwordAuthFound = true;
}
if (method.authMethodType === AuthMethodType.Ldap) {
ldapAuthFound = true;
} }
}); });
// Using password authentication method to provide UI for LDAP authentication even if password auth is not present in server
if (ldapAuthFound && !(passwordAuthFound)) {
sortedAuthMethodModels.push(new AuthMethod(AuthMethodType.Password,0));
}
authMethodModels.forEach((method) => { authMethodModels.forEach((method) => {
if (method.authMethodType !== AuthMethodType.Password) { if (method.authMethodType !== AuthMethodType.Password) {
sortedAuthMethodModels.push(method); sortedAuthMethodModels.push(method);

View File

@@ -11,6 +11,7 @@ import {
from as observableFrom, from as observableFrom,
Observable, Observable,
of as observableOf, of as observableOf,
shareReplay,
} from 'rxjs'; } from 'rxjs';
import { import {
map, map,
@@ -288,6 +289,10 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
isNotEmptyOperator(), isNotEmptyOperator(),
take(1), take(1),
map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow)), map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow)),
shareReplay({
bufferSize: 1,
refCount: true,
}),
); );
const startTime: number = new Date().getTime(); const startTime: number = new Date().getTime();
@@ -343,6 +348,10 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
isNotEmptyOperator(), isNotEmptyOperator(),
take(1), take(1),
map((href: string) => this.buildHrefFromFindOptions(href, options, [], ...linksToFollow)), map((href: string) => this.buildHrefFromFindOptions(href, options, [], ...linksToFollow)),
shareReplay({
bufferSize: 1,
refCount: true,
}),
); );
const startTime: number = new Date().getTime(); const startTime: number = new Date().getTime();

View File

@@ -16,6 +16,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RequestParam } from '../cache/models/request-param.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { Bitstream } from '../shared/bitstream.model'; import { Bitstream } from '../shared/bitstream.model';
import { BitstreamFormat } from '../shared/bitstream-format.model'; import { BitstreamFormat } from '../shared/bitstream-format.model';
@@ -176,4 +177,30 @@ describe('BitstreamDataService', () => {
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream2-self'); expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream2-self');
}); });
}); });
describe('findByItemHandle', () => {
it('should encode the filename correctly in the search parameters', () => {
const handle = '123456789/1234';
const sequenceId = '5';
const filename = 'file with spaces.pdf';
const searchParams = [
new RequestParam('handle', handle),
new RequestParam('sequenceId', sequenceId),
new RequestParam('filename', filename),
];
const linksToFollow: FollowLinkConfig<Bitstream>[] = [];
spyOn(service as any, 'getSearchByHref').and.callThrough();
service.getSearchByHref('byItemHandle', { searchParams }, ...linksToFollow).subscribe((href) => {
expect(service.getSearchByHref).toHaveBeenCalledWith(
'byItemHandle',
{ searchParams },
...linksToFollow,
);
expect(href).toBe(`${url}/bitstreams/search/byItemHandle?handle=123456789%2F1234&sequenceId=5&filename=file%20with%20spaces.pdf`);
});
});
});
}); });

View File

@@ -241,11 +241,12 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
* no valid cached version. Defaults to true * no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re- * @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale * requested after the response becomes stale
* @param options the {@link FindListOptions} for the request
* @return {Observable<Bitstream | null>} * @return {Observable<Bitstream | null>}
* Return an observable that contains primary bitstream information or null * Return an observable that contains primary bitstream information or null
*/ */
public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable<Bitstream | null> { public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, options?: FindListOptions): Observable<Bitstream | null> {
return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, followLink('primaryBitstream')).pipe( return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, options, followLink('primaryBitstream')).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<Bundle>) => { switchMap((rd: RemoteData<Bundle>) => {
if (!rd.hasSucceeded) { if (!rd.hasSucceeded) {

View File

@@ -78,10 +78,14 @@ export class BundleDataService extends IdentifiableDataService<Bundle> implement
* requested after the response becomes stale * requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which * @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved * {@link HALLink}s should be automatically resolved
* @param options the {@link FindListOptions} for the request
*/ */
// TODO should be implemented rest side // TODO should be implemented rest side
findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<Bundle>> { findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, options?: FindListOptions, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<Bundle>> {
return this.findAllByItem(item, { elementsPerPage: 9999 }, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe( //Since we filter by bundleName where the pagination options are not indicated we need to load all the possible bundles.
// This is a workaround, in substitution of the previously recursive call with expand
const paginationOptions = options ?? { elementsPerPage: 9999 };
return this.findAllByItem(item, paginationOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
map((rd: RemoteData<PaginatedList<Bundle>>) => { map((rd: RemoteData<PaginatedList<Bundle>>) => {
if (hasValue(rd.payload) && hasValue(rd.payload.page)) { if (hasValue(rd.payload) && hasValue(rd.payload.page)) {
const matchingBundle = rd.payload.page.find((bundle: Bundle) => const matchingBundle = rd.payload.page.find((bundle: Bundle) =>

View File

@@ -0,0 +1,194 @@
import {
HTTP_INTERCEPTORS,
HttpClient,
} from '@angular/common/http';
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing';
import { PLATFORM_ID } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
APP_CONFIG,
AppConfig,
} from '../../../config/app-config.interface';
import { DspaceRestInterceptor } from './dspace-rest.interceptor';
import { DspaceRestService } from './dspace-rest.service';
describe('DspaceRestInterceptor', () => {
let httpMock: HttpTestingController;
let httpClient: HttpClient;
const appConfig: Partial<AppConfig> = {
rest: {
ssl: false,
host: 'localhost',
port: 8080,
nameSpace: '/server',
baseUrl: 'http://api.example.com/server',
},
};
const appConfigWithSSR: Partial<AppConfig> = {
rest: {
ssl: false,
host: 'localhost',
port: 8080,
nameSpace: '/server',
baseUrl: 'http://api.example.com/server',
ssrBaseUrl: 'http://ssr.example.com/server',
},
};
describe('When SSR base URL is not set ', () => {
describe('and it\'s in the browser', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
DspaceRestService,
{
provide: HTTP_INTERCEPTORS,
useClass: DspaceRestInterceptor,
multi: true,
},
{ provide: APP_CONFIG, useValue: appConfig },
{ provide: PLATFORM_ID, useValue: 'browser' },
],
});
httpMock = TestBed.inject(HttpTestingController);
httpClient = TestBed.inject(HttpClient);
});
it('should not modify the request', () => {
const url = 'http://api.example.com/server/items';
httpClient.get(url).subscribe((response) => {
expect(response).toBeTruthy();
});
const req = httpMock.expectOne(url);
expect(req.request.url).toBe(url);
req.flush({});
httpMock.verify();
});
});
describe('and it\'s in SSR mode', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
DspaceRestService,
{
provide: HTTP_INTERCEPTORS,
useClass: DspaceRestInterceptor,
multi: true,
},
{ provide: APP_CONFIG, useValue: appConfig },
{ provide: PLATFORM_ID, useValue: 'server' },
],
});
httpMock = TestBed.inject(HttpTestingController);
httpClient = TestBed.inject(HttpClient);
});
it('should not replace the base URL', () => {
const url = 'http://api.example.com/server/items';
httpClient.get(url).subscribe((response) => {
expect(response).toBeTruthy();
});
const req = httpMock.expectOne(url);
expect(req.request.url).toBe(url);
req.flush({});
httpMock.verify();
});
});
});
describe('When SSR base URL is set ', () => {
describe('and it\'s in the browser', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
DspaceRestService,
{
provide: HTTP_INTERCEPTORS,
useClass: DspaceRestInterceptor,
multi: true,
},
{ provide: APP_CONFIG, useValue: appConfigWithSSR },
{ provide: PLATFORM_ID, useValue: 'browser' },
],
});
httpMock = TestBed.inject(HttpTestingController);
httpClient = TestBed.inject(HttpClient);
});
it('should not modify the request', () => {
const url = 'http://api.example.com/server/items';
httpClient.get(url).subscribe((response) => {
expect(response).toBeTruthy();
});
const req = httpMock.expectOne(url);
expect(req.request.url).toBe(url);
req.flush({});
httpMock.verify();
});
});
describe('and it\'s in SSR mode', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
DspaceRestService,
{
provide: HTTP_INTERCEPTORS,
useClass: DspaceRestInterceptor,
multi: true,
},
{ provide: APP_CONFIG, useValue: appConfigWithSSR },
{ provide: PLATFORM_ID, useValue: 'server' },
],
});
httpMock = TestBed.inject(HttpTestingController);
httpClient = TestBed.inject(HttpClient);
});
it('should replace the base URL', () => {
const url = 'http://api.example.com/server/items';
const ssrBaseUrl = appConfigWithSSR.rest.ssrBaseUrl;
httpClient.get(url).subscribe((response) => {
expect(response).toBeTruthy();
});
const req = httpMock.expectOne(ssrBaseUrl + '/items');
expect(req.request.url).toBe(ssrBaseUrl + '/items');
req.flush({});
httpMock.verify();
});
it('should not replace any query param containing the base URL', () => {
const url = 'http://api.example.com/server/items?url=http://api.example.com/server/item/1';
const ssrBaseUrl = appConfigWithSSR.rest.ssrBaseUrl;
httpClient.get(url).subscribe((response) => {
expect(response).toBeTruthy();
});
const req = httpMock.expectOne(ssrBaseUrl + '/items?url=http://api.example.com/server/item/1');
expect(req.request.url).toBe(ssrBaseUrl + '/items?url=http://api.example.com/server/item/1');
req.flush({});
httpMock.verify();
});
});
});
});

View File

@@ -0,0 +1,52 @@
import { isPlatformBrowser } from '@angular/common';
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
} from '@angular/common/http';
import {
Inject,
Injectable,
PLATFORM_ID,
} from '@angular/core';
import { Observable } from 'rxjs';
import {
APP_CONFIG,
AppConfig,
} from '../../../config/app-config.interface';
import { isEmpty } from '../../shared/empty.util';
@Injectable()
/**
* This Interceptor is used to use the configured base URL for the request made during SSR execution
*/
export class DspaceRestInterceptor implements HttpInterceptor {
/**
* Contains the configured application base URL
* @protected
*/
protected baseUrl: string;
protected ssrBaseUrl: string;
constructor(
@Inject(APP_CONFIG) protected appConfig: AppConfig,
@Inject(PLATFORM_ID) private platformId: string,
) {
this.baseUrl = this.appConfig.rest.baseUrl;
this.ssrBaseUrl = this.appConfig.rest.ssrBaseUrl;
}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
if (isPlatformBrowser(this.platformId) || isEmpty(this.ssrBaseUrl) || this.baseUrl === this.ssrBaseUrl) {
return next.handle(request);
}
// Different SSR Base URL specified so replace it in the current request url
const url = request.url.replace(this.baseUrl, this.ssrBaseUrl);
const newRequest: HttpRequest<any> = request.clone({ url });
return next.handle(newRequest);
}
}

View File

@@ -50,6 +50,7 @@ import { coreSelector } from '../core.selectors';
import { CoreState } from '../core-state.model'; import { CoreState } from '../core-state.model';
import { BundleDataService } from '../data/bundle-data.service'; import { BundleDataService } from '../data/bundle-data.service';
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
import { FindListOptions } from '../data/find-list-options.model';
import { PaginatedList } from '../data/paginated-list.model'; import { PaginatedList } from '../data/paginated-list.model';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { RootDataService } from '../data/root-data.service'; import { RootDataService } from '../data/root-data.service';
@@ -331,6 +332,7 @@ export class HeadTagService {
'ORIGINAL', 'ORIGINAL',
true, true,
true, true,
new FindListOptions(),
followLink('primaryBitstream'), followLink('primaryBitstream'),
followLink('bitstreams', { followLink('bitstreams', {
findListOptions: { findListOptions: {

View File

@@ -1,5 +1,6 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { environment } from '../../../environments/environment.test';
import { ServerHardRedirectService } from './server-hard-redirect.service'; import { ServerHardRedirectService } from './server-hard-redirect.service';
describe('ServerHardRedirectService', () => { describe('ServerHardRedirectService', () => {
@@ -7,7 +8,7 @@ describe('ServerHardRedirectService', () => {
const mockRequest = jasmine.createSpyObj(['get']); const mockRequest = jasmine.createSpyObj(['get']);
const mockResponse = jasmine.createSpyObj(['redirect', 'end']); const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
const service: ServerHardRedirectService = new ServerHardRedirectService(mockRequest, mockResponse); let service: ServerHardRedirectService = new ServerHardRedirectService(environment, mockRequest, mockResponse);
const origin = 'https://test-host.com:4000'; const origin = 'https://test-host.com:4000';
beforeEach(() => { beforeEach(() => {
@@ -68,4 +69,23 @@ describe('ServerHardRedirectService', () => {
}); });
}); });
describe('when SSR base url is set', () => {
const redirect = 'https://private-url:4000/server/api/bitstreams/uuid';
const replacedUrl = 'https://public-url/server/api/bitstreams/uuid';
const environmentWithSSRUrl: any = { ...environment, ...{ ...environment.rest, rest: {
ssrBaseUrl: 'https://private-url:4000/server',
baseUrl: 'https://public-url/server',
} } };
service = new ServerHardRedirectService(environmentWithSSRUrl, mockRequest, mockResponse);
beforeEach(() => {
service.redirect(redirect);
});
it('should perform a 302 redirect', () => {
expect(mockResponse.redirect).toHaveBeenCalledWith(302, replacedUrl);
expect(mockResponse.end).toHaveBeenCalled();
});
});
}); });

View File

@@ -7,10 +7,15 @@ import {
Response, Response,
} from 'express'; } from 'express';
import {
APP_CONFIG,
AppConfig,
} from '../../../config/app-config.interface';
import { import {
REQUEST, REQUEST,
RESPONSE, RESPONSE,
} from '../../../express.tokens'; } from '../../../express.tokens';
import { isNotEmpty } from '../../shared/empty.util';
import { HardRedirectService } from './hard-redirect.service'; import { HardRedirectService } from './hard-redirect.service';
/** /**
@@ -20,6 +25,7 @@ import { HardRedirectService } from './hard-redirect.service';
export class ServerHardRedirectService extends HardRedirectService { export class ServerHardRedirectService extends HardRedirectService {
constructor( constructor(
@Inject(APP_CONFIG) protected appConfig: AppConfig,
@Inject(REQUEST) protected req: Request, @Inject(REQUEST) protected req: Request,
@Inject(RESPONSE) protected res: Response, @Inject(RESPONSE) protected res: Response,
) { ) {
@@ -35,17 +41,22 @@ export class ServerHardRedirectService extends HardRedirectService {
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect) * optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
*/ */
redirect(url: string, statusCode?: number) { redirect(url: string, statusCode?: number) {
if (url === this.req.url) { if (url === this.req.url) {
return; return;
} }
let redirectUrl = url;
// If redirect url contains SSR base url then replace with public base url
if (isNotEmpty(this.appConfig.rest.ssrBaseUrl) && this.appConfig.rest.baseUrl !== this.appConfig.rest.ssrBaseUrl) {
redirectUrl = url.replace(this.appConfig.rest.ssrBaseUrl, this.appConfig.rest.baseUrl);
}
if (this.res.finished) { if (this.res.finished) {
const req: any = this.req; const req: any = this.req;
req._r_count = (req._r_count || 0) + 1; req._r_count = (req._r_count || 0) + 1;
console.warn('Attempted to redirect on a finished response. From', console.warn('Attempted to redirect on a finished response. From',
this.req.url, 'to', url); this.req.url, 'to', redirectUrl);
if (req._r_count > 10) { if (req._r_count > 10) {
console.error('Detected a redirection loop. killing the nodejs process'); console.error('Detected a redirection loop. killing the nodejs process');
@@ -59,9 +70,9 @@ export class ServerHardRedirectService extends HardRedirectService {
status = 302; status = 302;
} }
console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`); console.info(`Redirecting from ${this.req.url} to ${redirectUrl} with ${status}`);
this.res.redirect(status, url); this.res.redirect(status, redirectUrl);
this.res.end(); this.res.end();
// I haven't found a way to correctly stop Angular rendering. // I haven't found a way to correctly stop Angular rendering.
// So we just let it end its work, though we have already closed // So we just let it end its work, though we have already closed

View File

@@ -77,29 +77,29 @@
<button class="btn btn-outline-primary btn-sm ng-star-inserted" data-test="metadata-edit-btn" *ngIf="!mdValue.editing" <button class="btn btn-outline-primary btn-sm ng-star-inserted" data-test="metadata-edit-btn" *ngIf="!mdValue.editing"
[title]="dsoType + '.edit.metadata.edit.buttons.edit' | translate" [title]="dsoType + '.edit.metadata.edit.buttons.edit' | translate"
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.edit' | translate }}" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.edit' | translate }}"
[disabled]="isVirtual || mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE || (saving$ | async)" (click)="edit.emit()"> [dsBtnDisabled]="isVirtual || mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE || (saving$ | async)" (click)="edit.emit()">
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
<button class="btn btn-outline-success btn-sm ng-star-inserted" data-test="metadata-confirm-btn" *ngIf="mdValue.editing" <button class="btn btn-outline-success btn-sm ng-star-inserted" data-test="metadata-confirm-btn" *ngIf="mdValue.editing"
[title]="dsoType + '.edit.metadata.edit.buttons.confirm' | translate" [title]="dsoType + '.edit.metadata.edit.buttons.confirm' | translate"
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.confirm' | translate }}" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.confirm' | translate }}"
[disabled]="isVirtual || (saving$ | async)" (click)="confirm.emit(true)"> [dsBtnDisabled]="isVirtual || (saving$ | async)" (click)="confirm.emit(true)">
<i class="fas fa-check fa-fw"></i> <i class="fas fa-check fa-fw"></i>
</button> </button>
<button class="btn btn-outline-danger btn-sm" data-test="metadata-remove-btn" <button class="btn btn-outline-danger btn-sm" data-test="metadata-remove-btn"
[title]="dsoType + '.edit.metadata.edit.buttons.remove' | translate" [title]="dsoType + '.edit.metadata.edit.buttons.remove' | translate"
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.remove' | translate }}" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.remove' | translate }}"
[disabled]="isVirtual || (mdValue.change && mdValue.change !== DsoEditMetadataChangeTypeEnum.ADD) || mdValue.editing || (saving$ | async)" (click)="remove.emit()"> [dsBtnDisabled]="isVirtual || (mdValue.change && mdValue.change !== DsoEditMetadataChangeTypeEnum.ADD) || mdValue.editing || (saving$ | async)" (click)="remove.emit()">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
<button class="btn btn-outline-warning btn-sm" data-test="metadata-undo-btn" <button class="btn btn-outline-warning btn-sm" data-test="metadata-undo-btn"
[title]="dsoType + '.edit.metadata.edit.buttons.undo' | translate" [title]="dsoType + '.edit.metadata.edit.buttons.undo' | translate"
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.undo' | translate }}" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.undo' | translate }}"
[disabled]="isVirtual || (!mdValue.change && mdValue.reordered) || (!mdValue.change && !mdValue.editing) || (saving$ | async)" (click)="undo.emit()"> [dsBtnDisabled]="isVirtual || (!mdValue.change && mdValue.reordered) || (!mdValue.change && !mdValue.editing) || (saving$ | async)" (click)="undo.emit()">
<i class="fas fa-undo-alt fa-fw"></i> <i class="fas fa-undo-alt fa-fw"></i>
</button> </button>
<button class="btn btn-outline-secondary ds-drag-handle btn-sm" data-test="metadata-drag-btn" *ngVar="(isOnlyValue || (saving$ | async)) as disabled" <button class="btn btn-outline-secondary ds-drag-handle btn-sm" data-test="metadata-drag-btn" *ngVar="(isOnlyValue || (saving$ | async)) as disabled"
cdkDragHandle [cdkDragHandleDisabled]="disabled" [ngClass]="{'disabled': disabled}" [disabled]="disabled" cdkDragHandle [cdkDragHandleDisabled]="disabled" [ngClass]="{'disabled': disabled}" [dsBtnDisabled]="disabled"
[title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate" [title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate"
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}"> ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}">
<i class="fas fa-grip-vertical fa-fw"></i> <i class="fas fa-grip-vertical fa-fw"></i>

View File

@@ -34,6 +34,7 @@ import {
VIRTUAL_METADATA_PREFIX, VIRTUAL_METADATA_PREFIX,
} from '../../../core/shared/metadata.models'; } from '../../../core/shared/metadata.models';
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component'; import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component';
import { DsDynamicScrollableDropdownComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; import { DsDynamicScrollableDropdownComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
import { ThemedTypeBadgeComponent } from '../../../shared/object-collection/shared/badges/type-badge/themed-type-badge.component'; import { ThemedTypeBadgeComponent } from '../../../shared/object-collection/shared/badges/type-badge/themed-type-badge.component';
@@ -188,6 +189,7 @@ describe('DsoEditMetadataValueComponent', () => {
RouterTestingModule.withRoutes([]), RouterTestingModule.withRoutes([]),
DsoEditMetadataValueComponent, DsoEditMetadataValueComponent,
VarDirective, VarDirective,
BtnDisabledDirective,
], ],
providers: [ providers: [
{ provide: RelationshipDataService, useValue: relationshipService }, { provide: RelationshipDataService, useValue: relationshipService },
@@ -524,7 +526,14 @@ describe('DsoEditMetadataValueComponent', () => {
}); });
it(`should${disabled ? ' ' : ' not '}be disabled`, () => { it(`should${disabled ? ' ' : ' not '}be disabled`, () => {
expect(btn.nativeElement.disabled).toBe(disabled); if (disabled) {
expect(btn.nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(btn.nativeElement.classList.contains('disabled')).toBeTrue();
} else {
// Can be null or false, depending on if button was ever disabled so just check not true
expect(btn.nativeElement.getAttribute('aria-disabled')).not.toBe('true');
expect(btn.nativeElement.classList.contains('disabled')).toBeFalse();
}
}); });
} else { } else {
it('should not exist', () => { it('should not exist', () => {

View File

@@ -67,6 +67,7 @@ import {
import { Vocabulary } from '../../../core/submission/vocabularies/models/vocabulary.model'; import { Vocabulary } from '../../../core/submission/vocabularies/models/vocabulary.model';
import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model'; import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { isNotEmpty } from '../../../shared/empty.util'; import { isNotEmpty } from '../../../shared/empty.util';
import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component'; import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component';
import { import {
@@ -94,7 +95,7 @@ import {
styleUrls: ['./dso-edit-metadata-value.component.scss', '../dso-edit-metadata-shared/dso-edit-metadata-cells.scss'], styleUrls: ['./dso-edit-metadata-value.component.scss', '../dso-edit-metadata-shared/dso-edit-metadata-cells.scss'],
templateUrl: './dso-edit-metadata-value.component.html', templateUrl: './dso-edit-metadata-value.component.html',
standalone: true, standalone: true,
imports: [VarDirective, CdkDrag, NgClass, NgIf, FormsModule, DebounceDirective, RouterLink, ThemedTypeBadgeComponent, NgbTooltipModule, CdkDragHandle, AsyncPipe, TranslateModule, DsDynamicScrollableDropdownComponent, DsDynamicOneboxComponent, AuthorityConfidenceStateDirective], imports: [VarDirective, CdkDrag, NgClass, NgIf, FormsModule, DebounceDirective, RouterLink, ThemedTypeBadgeComponent, NgbTooltipModule, CdkDragHandle, AsyncPipe, TranslateModule, DsDynamicScrollableDropdownComponent, DsDynamicOneboxComponent, AuthorityConfidenceStateDirective, BtnDisabledDirective],
}) })
/** /**
* Component displaying a single editable row for a metadata value * Component displaying a single editable row for a metadata value

View File

@@ -1,18 +1,18 @@
<div class="item-metadata" *ngIf="form"> <div class="item-metadata" *ngIf="form">
<div class="button-row top d-flex my-2 space-children-mr ml-gap"> <div class="button-row top d-flex my-2 space-children-mr ml-gap">
<button class="mr-auto btn btn-success" id="dso-add-btn" [disabled]="form.newValue || (saving$ | async)" <button class="mr-auto btn btn-success" id="dso-add-btn" [dsBtnDisabled]="form.newValue || (saving$ | async)"
[attr.aria-label]="dsoType + '.edit.metadata.add-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.add-button' | translate"
[title]="dsoType + '.edit.metadata.add-button' | translate" [title]="dsoType + '.edit.metadata.add-button' | translate"
(click)="add()"><i class="fas fa-plus" aria-hidden="true"></i> (click)="add()"><i class="fas fa-plus" aria-hidden="true"></i>
<span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.add-button' | translate }}</span> <span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.add-button' | translate }}</span>
</button> </button>
<button class="btn btn-warning ml-1" id="dso-reinstate-btn" *ngIf="isReinstatable" [disabled]="(saving$ | async)" <button class="btn btn-warning ml-1" id="dso-reinstate-btn" *ngIf="isReinstatable" [dsBtnDisabled]="(saving$ | async)"
[attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate"
[title]="dsoType + '.edit.metadata.reinstate-button' | translate" [title]="dsoType + '.edit.metadata.reinstate-button' | translate"
(click)="reinstate()"><i class="fas fa-undo-alt" aria-hidden="true"></i> (click)="reinstate()"><i class="fas fa-undo-alt" aria-hidden="true"></i>
<span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.reinstate-button' | translate }}</span> <span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.reinstate-button' | translate }}</span>
</button> </button>
<button class="btn btn-primary ml-1" id="dso-save-btn" [disabled]="!hasChanges || (saving$ | async)" <button class="btn btn-primary ml-1" id="dso-save-btn" [dsBtnDisabled]="!hasChanges || (saving$ | async)"
[attr.aria-label]="dsoType + '.edit.metadata.save-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.save-button' | translate"
[title]="dsoType + '.edit.metadata.save-button' | translate" [title]="dsoType + '.edit.metadata.save-button' | translate"
(click)="submit()"><i class="fas fa-save" aria-hidden="true"></i> (click)="submit()"><i class="fas fa-save" aria-hidden="true"></i>
@@ -21,7 +21,7 @@
<button class="btn btn-danger ml-1" id="dso-discard-btn" *ngIf="!isReinstatable" <button class="btn btn-danger ml-1" id="dso-discard-btn" *ngIf="!isReinstatable"
[attr.aria-label]="dsoType + '.edit.metadata.discard-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.discard-button' | translate"
[title]="dsoType + '.edit.metadata.discard-button' | translate" [title]="dsoType + '.edit.metadata.discard-button' | translate"
[disabled]="!hasChanges || (saving$ | async)" [dsBtnDisabled]="!hasChanges || (saving$ | async)"
(click)="discard()"><i class="fas fa-times" aria-hidden="true"></i> (click)="discard()"><i class="fas fa-times" aria-hidden="true"></i>
<span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.discard-button' | translate }}</span> <span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.discard-button' | translate }}</span>
</button> </button>
@@ -77,13 +77,13 @@
</div> </div>
<div class="button-row bottom d-inline-block w-100"> <div class="button-row bottom d-inline-block w-100">
<div class="mt-2 float-right space-children-mr ml-gap"> <div class="mt-2 float-right space-children-mr ml-gap">
<button class="btn btn-warning" *ngIf="isReinstatable" [disabled]="(saving$ | async)" <button class="btn btn-warning" *ngIf="isReinstatable" [dsBtnDisabled]="(saving$ | async)"
[attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate"
[title]="dsoType + '.edit.metadata.reinstate-button' | translate" [title]="dsoType + '.edit.metadata.reinstate-button' | translate"
(click)="reinstate()"> (click)="reinstate()">
<i class="fas fa-undo-alt" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.reinstate-button' | translate }} <i class="fas fa-undo-alt" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.reinstate-button' | translate }}
</button> </button>
<button class="btn btn-primary" [disabled]="!hasChanges || (saving$ | async)" <button class="btn btn-primary" [dsBtnDisabled]="!hasChanges || (saving$ | async)"
[attr.aria-label]="dsoType + '.edit.metadata.save-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.save-button' | translate"
[title]="dsoType + '.edit.metadata.save-button' | translate" [title]="dsoType + '.edit.metadata.save-button' | translate"
(click)="submit()"> (click)="submit()">
@@ -92,7 +92,7 @@
<button class="btn btn-danger" *ngIf="!isReinstatable" <button class="btn btn-danger" *ngIf="!isReinstatable"
[attr.aria-label]="dsoType + '.edit.metadata.discard-button' | translate" [attr.aria-label]="dsoType + '.edit.metadata.discard-button' | translate"
[title]="dsoType + '.edit.metadata.discard-button' | translate" [title]="dsoType + '.edit.metadata.discard-button' | translate"
[disabled]="!hasChanges || (saving$ | async)" [dsBtnDisabled]="!hasChanges || (saving$ | async)"
(click)="discard()"> (click)="discard()">
<i class="fas fa-times" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.discard-button' | translate }} <i class="fas fa-times" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.discard-button' | translate }}
</button> </button>

View File

@@ -22,6 +22,7 @@ import { Item } from '../../core/shared/item.model';
import { ITEM } from '../../core/shared/item.resource-type'; import { ITEM } from '../../core/shared/item.resource-type';
import { MetadataValue } from '../../core/shared/metadata.models'; import { MetadataValue } from '../../core/shared/metadata.models';
import { AlertComponent } from '../../shared/alert/alert.component'; import { AlertComponent } from '../../shared/alert/alert.component';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TestDataService } from '../../shared/testing/test-data-service.mock'; import { TestDataService } from '../../shared/testing/test-data-service.mock';
@@ -94,6 +95,7 @@ describe('DsoEditMetadataComponent', () => {
RouterTestingModule.withRoutes([]), RouterTestingModule.withRoutes([]),
DsoEditMetadataComponent, DsoEditMetadataComponent,
VarDirective, VarDirective,
BtnDisabledDirective,
], ],
providers: [ providers: [
{ provide: APP_DATA_SERVICES_MAP, useValue: mockDataServiceMap }, { provide: APP_DATA_SERVICES_MAP, useValue: mockDataServiceMap },
@@ -216,7 +218,13 @@ describe('DsoEditMetadataComponent', () => {
}); });
it(`should${disabled ? ' ' : ' not '}be disabled`, () => { it(`should${disabled ? ' ' : ' not '}be disabled`, () => {
expect(btn.nativeElement.disabled).toBe(disabled); if (disabled) {
expect(btn.nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(btn.nativeElement.classList.contains('disabled')).toBeTrue();
} else {
expect(btn.nativeElement.getAttribute('aria-disabled')).not.toBe('true');
expect(btn.nativeElement.classList.contains('disabled')).toBeFalse();
}
}); });
} else { } else {
it('should not exist', () => { it('should not exist', () => {

View File

@@ -47,6 +47,7 @@ import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { ResourceType } from '../../core/shared/resource-type'; import { ResourceType } from '../../core/shared/resource-type';
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 { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { import {
hasNoValue, hasNoValue,
hasValue, hasValue,
@@ -66,7 +67,7 @@ import { MetadataFieldSelectorComponent } from './metadata-field-selector/metada
styleUrls: ['./dso-edit-metadata.component.scss'], styleUrls: ['./dso-edit-metadata.component.scss'],
templateUrl: './dso-edit-metadata.component.html', templateUrl: './dso-edit-metadata.component.html',
standalone: true, standalone: true,
imports: [NgIf, DsoEditMetadataHeadersComponent, MetadataFieldSelectorComponent, DsoEditMetadataValueHeadersComponent, DsoEditMetadataValueComponent, NgFor, DsoEditMetadataFieldValuesComponent, AlertComponent, ThemedLoadingComponent, AsyncPipe, TranslateModule], imports: [NgIf, DsoEditMetadataHeadersComponent, MetadataFieldSelectorComponent, DsoEditMetadataValueHeadersComponent, DsoEditMetadataValueComponent, NgFor, DsoEditMetadataFieldValuesComponent, AlertComponent, ThemedLoadingComponent, AsyncPipe, TranslateModule, BtnDisabledDirective],
}) })
/** /**
* Component showing a table of all metadata on a DSpaceObject and options to modify them * Component showing a table of all metadata on a DSpaceObject and options to modify them

View File

@@ -28,7 +28,7 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<button <button
[disabled]="isInValid" [dsBtnDisabled]="isInValid"
class="btn btn-default btn-primary" class="btn btn-default btn-primary"
(click)="submit()">{{'forgot-password.form.submit' | translate}}</button> (click)="submit()">{{'forgot-password.form.submit' | translate}}</button>
</div> </div>

View File

@@ -29,6 +29,7 @@ import {
} from '../../core/shared/operators'; } from '../../core/shared/operators';
import { Registration } from '../../core/shared/registration.model'; import { Registration } from '../../core/shared/registration.model';
import { ProfilePageSecurityFormComponent } from '../../profile-page/profile-page-security-form/profile-page-security-form.component'; import { ProfilePageSecurityFormComponent } from '../../profile-page/profile-page-security-form/profile-page-security-form.component';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe'; import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe';
@@ -42,6 +43,7 @@ import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe';
ProfilePageSecurityFormComponent, ProfilePageSecurityFormComponent,
AsyncPipe, AsyncPipe,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -7,7 +7,7 @@
<div class="d-flex mt-4"> <div class="d-flex mt-4">
<button id="button-cancel" type="button" (click)="cancel()" class="btn btn-outline-secondary mr-auto">{{ 'info.end-user-agreement.buttons.cancel' | translate }}</button> <button id="button-cancel" type="button" (click)="cancel()" class="btn btn-outline-secondary mr-auto">{{ 'info.end-user-agreement.buttons.cancel' | translate }}</button>
<button id="button-save" type="submit" class="btn btn-primary" [disabled]="!accepted">{{ 'info.end-user-agreement.buttons.save' | translate }}</button> <button id="button-save" type="submit" class="btn btn-primary" [dsBtnDisabled]="!accepted">{{ 'info.end-user-agreement.buttons.save' | translate }}</button>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -16,6 +16,7 @@ import { of as observableOf } from 'rxjs';
import { LogOutAction } from '../../core/auth/auth.actions'; import { LogOutAction } from '../../core/auth/auth.actions';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service'; import { EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { EndUserAgreementComponent } from './end-user-agreement.component'; import { EndUserAgreementComponent } from './end-user-agreement.component';
@@ -57,7 +58,7 @@ describe('EndUserAgreementComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
init(); init();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), EndUserAgreementComponent], imports: [TranslateModule.forRoot(), EndUserAgreementComponent, BtnDisabledDirective],
providers: [ providers: [
{ provide: EndUserAgreementService, useValue: endUserAgreementService }, { provide: EndUserAgreementService, useValue: endUserAgreementService },
{ provide: NotificationsService, useValue: notificationsService }, { provide: NotificationsService, useValue: notificationsService },
@@ -95,7 +96,8 @@ describe('EndUserAgreementComponent', () => {
it('should disable the save button', () => { it('should disable the save button', () => {
const button = fixture.debugElement.query(By.css('#button-save')).nativeElement; const button = fixture.debugElement.query(By.css('#button-save')).nativeElement;
expect(button.disabled).toBeTruthy(); expect(button.getAttribute('aria-disabled')).toBe('true');
expect(button.classList.contains('disabled')).toBeTrue();
}); });
}); });

View File

@@ -23,6 +23,7 @@ import { AppState } from '../../app.reducer';
import { LogOutAction } from '../../core/auth/auth.actions'; import { LogOutAction } from '../../core/auth/auth.actions';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service'; import { EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { EndUserAgreementContentComponent } from './end-user-agreement-content/end-user-agreement-content.component'; import { EndUserAgreementContentComponent } from './end-user-agreement-content/end-user-agreement-content.component';
@@ -32,7 +33,7 @@ import { EndUserAgreementContentComponent } from './end-user-agreement-content/e
templateUrl: './end-user-agreement.component.html', templateUrl: './end-user-agreement.component.html',
styleUrls: ['./end-user-agreement.component.scss'], styleUrls: ['./end-user-agreement.component.scss'],
standalone: true, standalone: true,
imports: [EndUserAgreementContentComponent, FormsModule, TranslateModule], imports: [EndUserAgreementContentComponent, FormsModule, TranslateModule, BtnDisabledDirective],
}) })
/** /**
* Component displaying the End User Agreement and an option to accept it * Component displaying the End User Agreement and an option to accept it

View File

@@ -41,7 +41,7 @@
<div class="row mt-3"> <div class="row mt-3">
<div class="control-group col-sm-12 text-right"> <div class="control-group col-sm-12 text-right">
<button [disabled]="!feedbackForm.valid" class="btn btn-primary" name="submit" type="submit">{{ 'info.feedback.send' | translate }}</button> <button [dsBtnDisabled]="!feedbackForm.valid" class="btn btn-primary" name="submit" type="submit">{{ 'info.feedback.send' | translate }}</button>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -18,6 +18,7 @@ import { FeedbackDataService } from '../../../core/feedback/feedback-data.servic
import { Feedback } from '../../../core/feedback/models/feedback.model'; import { Feedback } from '../../../core/feedback/models/feedback.model';
import { RouteService } from '../../../core/services/route.service'; import { RouteService } from '../../../core/services/route.service';
import { NativeWindowService } from '../../../core/services/window.service'; import { NativeWindowService } from '../../../core/services/window.service';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref'; import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref';
import { RouterMock } from '../../../shared/mocks/router.mock'; import { RouterMock } from '../../../shared/mocks/router.mock';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
@@ -45,7 +46,7 @@ describe('FeedbackFormComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), FeedbackFormComponent], imports: [TranslateModule.forRoot(), FeedbackFormComponent, BtnDisabledDirective],
providers: [ providers: [
{ provide: RouteService, useValue: routeServiceStub }, { provide: RouteService, useValue: routeServiceStub },
{ provide: UntypedFormBuilder, useValue: new UntypedFormBuilder() }, { provide: UntypedFormBuilder, useValue: new UntypedFormBuilder() },
@@ -79,7 +80,8 @@ describe('FeedbackFormComponent', () => {
}); });
it('should have disabled button', () => { it('should have disabled button', () => {
expect(de.query(By.css('button')).nativeElement.disabled).toBeTrue(); expect(de.query(By.css('button')).nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(de.query(By.css('button')).nativeElement.classList.contains('disabled')).toBeTrue();
}); });
describe('when message is inserted', () => { describe('when message is inserted', () => {
@@ -90,7 +92,8 @@ describe('FeedbackFormComponent', () => {
}); });
it('should not have disabled button', () => { it('should not have disabled button', () => {
expect(de.query(By.css('button')).nativeElement.disabled).toBeFalse(); expect(de.query(By.css('button')).nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(de.query(By.css('button')).nativeElement.classList.contains('disabled')).toBeFalse();
}); });
it('on submit should call createFeedback of feedbackDataServiceStub service', () => { it('on submit should call createFeedback of feedbackDataServiceStub service', () => {

View File

@@ -30,6 +30,7 @@ import {
import { NoContent } from '../../../core/shared/NoContent.model'; import { NoContent } from '../../../core/shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { URLCombiner } from '../../../core/url-combiner/url-combiner'; import { URLCombiner } from '../../../core/url-combiner/url-combiner';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { ErrorComponent } from '../../../shared/error/error.component'; import { ErrorComponent } from '../../../shared/error/error.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
@@ -38,7 +39,7 @@ import { NotificationsService } from '../../../shared/notifications/notification
templateUrl: './feedback-form.component.html', templateUrl: './feedback-form.component.html',
styleUrls: ['./feedback-form.component.scss'], styleUrls: ['./feedback-form.component.scss'],
standalone: true, standalone: true,
imports: [FormsModule, ReactiveFormsModule, NgIf, ErrorComponent, TranslateModule], imports: [FormsModule, ReactiveFormsModule, NgIf, ErrorComponent, TranslateModule, BtnDisabledDirective],
}) })
/** /**
* Component displaying the contents of the Feedback Statement * Component displaying the contents of the Feedback Statement

View File

@@ -79,7 +79,7 @@
</a> </a>
<button <button
[disabled]="requestCopyForm.invalid" [dsBtnDisabled]="requestCopyForm.invalid"
class="btn btn-default btn-primary" class="btn btn-default btn-primary"
(click)="onSubmit()">{{'bitstream-request-a-copy.submit' | translate}}</button> (click)="onSubmit()">{{'bitstream-request-a-copy.submit' | translate}}</button>
</div> </div>

View File

@@ -55,6 +55,7 @@ import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { import {
hasValue, hasValue,
isNotEmpty, isNotEmpty,
@@ -71,6 +72,7 @@ import { getItemPageRoute } from '../../item-page-routing-paths';
AsyncPipe, AsyncPipe,
ReactiveFormsModule, ReactiveFormsModule,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -16,7 +16,7 @@
class="fas fa-undo-alt"></i> class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span>
</button> </button>
<button class="btn btn-primary" [disabled]="(hasChanges$ | async) !== true || submitting" <button class="btn btn-primary" [dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate" [attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
(click)="submit()"><i (click)="submit()"><i
class="fas fa-save"></i> class="fas fa-save"></i>
@@ -24,7 +24,7 @@
</button> </button>
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true" <button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate" [attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
[disabled]="(hasChanges$ | async) !== true || submitting" [dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
(click)="discard()"><i (click)="discard()"><i
class="fas fa-times"></i> class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span>
@@ -39,6 +39,9 @@
[isFirstTable]="isFirst" [isFirstTable]="isFirst"
aria-describedby="reorder-description"> aria-describedby="reorder-description">
</ds-item-edit-bitstream-bundle> </ds-item-edit-bitstream-bundle>
<div class="d-flex justify-content-center" *ngIf="showLoadMoreLink$ | async">
<button class="btn btn-link my-3" (click)="loadBundles()"> {{'item.edit.bitstreams.load-more.link' | translate}}</button>
</div>
</div> </div>
<div *ngIf="bundles?.length === 0" <div *ngIf="bundles?.length === 0"
class="alert alert-info w-100 d-inline-block mt-4" role="alert"> class="alert alert-info w-100 d-inline-block mt-4" role="alert">
@@ -54,7 +57,7 @@
class="fas fa-undo-alt"></i> class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span>
</button> </button>
<button class="btn btn-primary" [disabled]="(hasChanges$ | async) !== true || submitting" <button class="btn btn-primary" [dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate" [attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
(click)="submit()"><i (click)="submit()"><i
class="fas fa-save"></i> class="fas fa-save"></i>
@@ -62,7 +65,7 @@
</button> </button>
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true" <button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate" [attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
[disabled]="(hasChanges$ | async) !== true || submitting" [dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
(click)="discard()"><i (click)="discard()"><i
class="fas fa-times"></i> class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span>

View File

@@ -1,4 +1,9 @@
import { CommonModule } from '@angular/common'; import {
AsyncPipe,
CommonModule,
NgForOf,
NgIf,
} from '@angular/common';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
@@ -15,16 +20,22 @@ import {
TranslateModule, TranslateModule,
TranslateService, TranslateService,
} from '@ngx-translate/core'; } from '@ngx-translate/core';
import { Operation } from 'fast-json-patch';
import { import {
BehaviorSubject,
combineLatest, combineLatest,
Observable, Observable,
Subscription, Subscription,
} from 'rxjs'; } from 'rxjs';
import { import {
filter,
map, map,
switchMap, switchMap,
take, take,
tap,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { AlertComponent } from 'src/app/shared/alert/alert.component';
import { AlertType } from 'src/app/shared/alert/alert-type';
import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
@@ -40,10 +51,14 @@ import {
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { AlertComponent } from '../../../shared/alert/alert.component'; import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { AlertType } from '../../../shared/alert/alert-type'; import {
hasValue,
isNotEmpty,
} from '../../../shared/empty.util';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes';
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
import { ObjectValuesPipe } from '../../../shared/utils/object-values-pipe'; import { ObjectValuesPipe } from '../../../shared/utils/object-values-pipe';
@@ -58,12 +73,16 @@ import { ItemEditBitstreamBundleComponent } from './item-edit-bitstream-bundle/i
templateUrl: './item-bitstreams.component.html', templateUrl: './item-bitstreams.component.html',
imports: [ imports: [
CommonModule, CommonModule,
AsyncPipe,
TranslateModule, TranslateModule,
ItemEditBitstreamBundleComponent, ItemEditBitstreamBundleComponent,
RouterLink, RouterLink,
NgIf,
VarDirective, VarDirective,
NgForOf,
ThemedLoadingComponent, ThemedLoadingComponent,
AlertComponent, AlertComponent,
BtnDisabledDirective,
], ],
providers: [ObjectValuesPipe], providers: [ObjectValuesPipe],
standalone: true, standalone: true,
@@ -77,9 +96,18 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
protected readonly AlertType = AlertType; protected readonly AlertType = AlertType;
/** /**
* The currently listed bundles * All bundles for the current item
*/ */
bundles$: Observable<Bundle[]>; private bundlesSubject = new BehaviorSubject<Bundle[]>([]);
/**
* The page options to use for fetching the bundles
*/
bundlesOptions: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'bundles-pagination-options',
currentPage: 1,
pageSize: 10,
});
/** /**
* The bootstrap sizes used for the columns within this table * The bootstrap sizes used for the columns within this table
@@ -98,6 +126,18 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
*/ */
itemUpdateSubscription: Subscription; itemUpdateSubscription: Subscription;
/**
* The flag indicating to show the load more link
*/
showLoadMoreLink$: BehaviorSubject<boolean> = new BehaviorSubject(true);
/**
* The list of bundles for the current item as an observable
*/
get bundles$(): Observable<Bundle[]> {
return this.bundlesSubject.asObservable();
}
/** /**
* An observable which emits a boolean which represents whether the service is currently handling a 'move' request * An observable which emits a boolean which represents whether the service is currently handling a 'move' request
*/ */
@@ -127,14 +167,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
* Actions to perform after the item has been initialized * Actions to perform after the item has been initialized
*/ */
postItemInit(): void { postItemInit(): void {
const bundlesOptions = this.itemBitstreamsService.getInitialBundlesPaginationOptions(); this.loadBundles(1);
this.isProcessingMoveRequest = this.itemBitstreamsService.getPerformingMoveRequest$();
this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({ pagination: bundlesOptions })).pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
map((bundlePage: PaginatedList<Bundle>) => bundlePage.page),
);
} }
/** /**
@@ -199,6 +232,26 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
this.notificationsPrefix = 'item.edit.bitstreams.notifications.'; this.notificationsPrefix = 'item.edit.bitstreams.notifications.';
} }
/**
* Load bundles for the current item
* @param currentPage The current page to load
*/
loadBundles(currentPage?: number) {
this.bundlesOptions = Object.assign(new PaginationComponentOptions(), this.bundlesOptions, {
currentPage: currentPage || this.bundlesOptions.currentPage + 1,
});
this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({ pagination: this.bundlesOptions })).pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
tap((bundlesPL: PaginatedList<Bundle>) =>
this.showLoadMoreLink$.next(bundlesPL.pageInfo.currentPage < bundlesPL.pageInfo.totalPages),
),
map((bundlePage: PaginatedList<Bundle>) => bundlePage.page),
).subscribe((bundles: Bundle[]) => {
this.bundlesSubject.next([...this.bundlesSubject.getValue(), ...bundles]);
});
}
/** /**
* Submit the current changes * Submit the current changes
@@ -208,7 +261,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
submit() { submit() {
this.submitting = true; this.submitting = true;
const removedResponses$ = this.itemBitstreamsService.removeMarkedBitstreams(this.bundles$); const removedResponses$ = this.itemBitstreamsService.removeMarkedBitstreams(this.bundles$.pipe(take(1)));
// Perform the setup actions from above in order and display notifications // Perform the setup actions from above in order and display notifications
removedResponses$.subscribe((responses: RemoteData<NoContent>) => { removedResponses$.subscribe((responses: RemoteData<NoContent>) => {
@@ -217,6 +270,56 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
}); });
} }
/**
* A bitstream was dropped in a new location. Send out a Move Patch request to the REST API, display notifications,
* refresh the bundle's cache (so the lists can properly reload) and call the event's callback function (which will
* navigate the user to the correct page)
* @param bundle The bundle to send patch requests to
* @param event The event containing the index the bitstream came from and was dropped to
*/
dropBitstream(bundle: Bundle, event: any) {
this.zone.runOutsideAngular(() => {
if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) {
const moveOperation = {
op: 'move',
from: `/_links/bitstreams/${event.fromIndex}/href`,
path: `/_links/bitstreams/${event.toIndex}/href`,
} as Operation;
this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RemoteData<Bundle>) => {
this.zone.run(() => {
this.displayNotifications('item.edit.bitstreams.notifications.move', [response]);
// Remove all cached requests from this bundle and call the event's callback when the requests are cleared
this.requestService.removeByHrefSubstring(bundle.self).pipe(
filter((isCached) => isCached),
take(1),
).subscribe(() => event.finish());
});
});
}
});
}
/**
* Display notifications
* - Error notification for each failed response with their message
* - Success notification in case there's at least one successful response
* @param key The i18n key for the notification messages
* @param responses The returned responses to display notifications for
*/
displayNotifications(key: string, responses: RemoteData<any>[]) {
if (isNotEmpty(responses)) {
const failedResponses = responses.filter((response: RemoteData<Bundle>) => hasValue(response) && response.hasFailed);
const successfulResponses = responses.filter((response: RemoteData<Bundle>) => hasValue(response) && response.hasSucceeded);
failedResponses.forEach((response: RemoteData<Bundle>) => {
this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage);
});
if (successfulResponses.length > 0) {
this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`));
}
}
}
/** /**
* Request the object updates service to discard all current changes to this item * Request the object updates service to discard all current changes to this item
* Shows a notification to remind the user that they can undo this * Shows a notification to remind the user that they can undo this

View File

@@ -113,13 +113,13 @@
title="{{'item.edit.bitstreams.edit.buttons.edit' | translate}}"> title="{{'item.edit.bitstreams.edit.buttons.edit' | translate}}">
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
<button [disabled]="!canRemove(update)" (click)="remove(entry.bitstream)" <button [dsBtnDisabled]="!canRemove(update)" (click)="remove(entry.bitstream)"
[attr.aria-label]=" 'item. edit bitstreams.edit.buttons.remove' | translate" [attr.aria-label]=" 'item. edit bitstreams.edit.buttons.remove' | translate"
class="btn btn-outline-danger btn-sm" class="btn btn-outline-danger btn-sm"
title="{{'item.edit.bitstreams.edit.buttons.remove' | translate}}"> title="{{'item.edit.bitstreams.edit.buttons.remove' | translate}}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
<button [disabled]="!canUndo(update)" (click)="undo(entry.bitstream)" <button [dsBtnDisabled]="!canUndo(update)" (click)="undo(entry.bitstream)"
[attr.aria-label]="'item.edit.bitstreams.edit.buttons.undo' | translate" [attr.aria-label]="'item.edit.bitstreams.edit.buttons.undo' | translate"
class="btn btn-outline-warning btn-sm" class="btn btn-outline-warning btn-sm"
title="{{'item.edit.bitstreams.edit.buttons.undo' | translate}}"> title="{{'item.edit.bitstreams.edit.buttons.undo' | translate}}">

View File

@@ -49,6 +49,7 @@ import {
getAllSucceededRemoteData, getAllSucceededRemoteData,
paginatedListToArray, paginatedListToArray,
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import { import {
hasNoValue, hasNoValue,
hasValue, hasValue,
@@ -83,6 +84,7 @@ import {
NgbDropdownModule, NgbDropdownModule,
CdkDrag, CdkDrag,
BrowserOnlyPipe, BrowserOnlyPipe,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -86,10 +86,10 @@
</ng-container> </ng-container>
<div class="space-children-mr"> <div class="space-children-mr">
<button [disabled]="isDeleting$ | async" (click)="performAction()" <button [dsBtnDisabled]="isDeleting$ | async" (click)="performAction()"
class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}} class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
</button> </button>
<button [disabled]="isDeleting$ | async" [routerLink]="[itemPageRoute, 'edit']" <button [dsBtnDisabled]="isDeleting$ | async" [routerLink]="[itemPageRoute, 'edit']"
class="btn btn-outline-secondary cancel"> class="btn btn-outline-secondary cancel">
{{cancelMessage| translate}} {{cancelMessage| translate}}
</button> </button>

View File

@@ -56,6 +56,7 @@ import {
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { ViewMode } from '../../../core/shared/view-mode.model'; import { ViewMode } from '../../../core/shared/view-mode.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { import {
hasValue, hasValue,
isNotEmpty, isNotEmpty,
@@ -109,6 +110,7 @@ class RelationshipDTO {
VarDirective, VarDirective,
NgForOf, NgForOf,
RouterLink, RouterLink,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -40,7 +40,7 @@
<button [routerLink]="[(itemPageRoute$ | async), 'edit']" class="btn btn-outline-secondary"> <button [routerLink]="[(itemPageRoute$ | async), 'edit']" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {{'item.edit.move.cancel' | translate}} <i class="fas fa-arrow-left"></i> {{'item.edit.move.cancel' | translate}}
</button> </button>
<button class="btn btn-primary" [disabled]="!canMove" (click)="moveToCollection()"> <button class="btn btn-primary" [dsBtnDisabled]="!canMove" (click)="moveToCollection()">
<span *ngIf="!processing"> <span *ngIf="!processing">
<i class="fas fa-save"></i> {{'item.edit.move.save-button' | translate}} <i class="fas fa-save"></i> {{'item.edit.move.save-button' | translate}}
</span> </span>
@@ -48,7 +48,7 @@
<i class="fas fa-circle-notch fa-spin"></i> {{'item.edit.move.processing' | translate}} <i class="fas fa-circle-notch fa-spin"></i> {{'item.edit.move.processing' | translate}}
</span> </span>
</button> </button>
<button class="btn btn-danger" [disabled]="!canSubmit" (click)="discard()"> <button class="btn btn-danger" [dsBtnDisabled]="!canSubmit" (click)="discard()">
<i class="fas fa-times"></i> {{"item.edit.move.discard-button" | translate}} <i class="fas fa-times"></i> {{"item.edit.move.discard-button" | translate}}
</button> </button>
</div> </div>

View File

@@ -37,6 +37,7 @@ import {
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { SearchService } from '../../../core/shared/search/search.service'; import { SearchService } from '../../../core/shared/search/search.service';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { AuthorizedCollectionSelectorComponent } from '../../../shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component'; import { AuthorizedCollectionSelectorComponent } from '../../../shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
@@ -56,6 +57,7 @@ import {
AsyncPipe, AsyncPipe,
AuthorizedCollectionSelectorComponent, AuthorizedCollectionSelectorComponent,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -5,12 +5,12 @@
</div> </div>
<div class="col-12 col-md-9 float-left action-button"> <div class="col-12 col-md-9 float-left action-button">
<span *ngIf="operation.authorized"> <span *ngIf="operation.authorized">
<button class="btn btn-outline-primary" [disabled]="operation.disabled" [routerLink]="operation.operationUrl" [attr.aria-label]="'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate"> <button class="btn btn-outline-primary" [dsBtnDisabled]="operation.disabled" [routerLink]="operation.operationUrl" [attr.aria-label]="'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate">
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}} {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
</button> </button>
</span> </span>
<span *ngIf="!operation.authorized" [ngbTooltip]="'item.edit.tabs.status.buttons.unauthorized' | translate"> <span *ngIf="!operation.authorized" [ngbTooltip]="'item.edit.tabs.status.buttons.unauthorized' | translate">
<button class="btn btn-outline-primary" [disabled]="true" [attr.aria-label]="'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate"> <button class="btn btn-outline-primary" [dsBtnDisabled]="true" [attr.aria-label]="'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate">
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}} {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
</button> </button>
</span> </span>

View File

@@ -6,6 +6,7 @@ import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { ItemOperationComponent } from './item-operation.component'; import { ItemOperationComponent } from './item-operation.component';
import { ItemOperation } from './itemOperation.model'; import { ItemOperation } from './itemOperation.model';
@@ -17,7 +18,7 @@ describe('ItemOperationComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ItemOperationComponent], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ItemOperationComponent, BtnDisabledDirective],
}).compileComponents(); }).compileComponents();
})); }));
@@ -43,7 +44,8 @@ describe('ItemOperationComponent', () => {
const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement; const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement;
expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label'); expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
const button = fixture.debugElement.query(By.css('button')).nativeElement; const button = fixture.debugElement.query(By.css('button')).nativeElement;
expect(button.disabled).toBeTrue(); expect(button.getAttribute('aria-disabled')).toBe('true');
expect(button.classList.contains('disabled')).toBeTrue();
expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
}); });
}); });

View File

@@ -7,6 +7,7 @@ import { RouterLink } from '@angular/router';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { ItemOperation } from './itemOperation.model'; import { ItemOperation } from './itemOperation.model';
@Component({ @Component({
@@ -17,6 +18,7 @@ import { ItemOperation } from './itemOperation.model';
RouterLink, RouterLink,
NgbTooltipModule, NgbTooltipModule,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -1,6 +1,6 @@
<h2 class="h4"> <h2 class="h4">
{{relationshipMessageKey$ | async | translate}} {{relationshipMessageKey$ | async | translate}}
<button class="ml-2 btn btn-success" [disabled]="(hasChanges | async)" (click)="openLookup()"> <button class="ml-2 btn btn-success" [dsBtnDisabled]="(hasChanges | async)" (click)="openLookup()">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.relationships.edit.buttons.add" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.relationships.edit.buttons.add" | translate}}</span>
</button> </button>

View File

@@ -388,7 +388,8 @@ describe('EditRelationshipListComponent', () => {
comp.hasChanges = observableOf(true); comp.hasChanges = observableOf(true);
fixture.detectChanges(); fixture.detectChanges();
const element = de.query(By.css('.btn-success')); const element = de.query(By.css('.btn-success'));
expect(element.nativeElement?.disabled).toBeTrue(); expect(element.nativeElement?.getAttribute('aria-disabled')).toBe('true');
expect(element.nativeElement?.classList.contains('disabled')).toBeTrue();
}); });
}); });

View File

@@ -65,6 +65,7 @@ import {
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import { import {
hasNoValue, hasNoValue,
hasValue, hasValue,
@@ -100,6 +101,7 @@ import { EditRelationshipComponent } from '../edit-relationship/edit-relationshi
TranslateModule, TranslateModule,
NgClass, NgClass,
ThemedLoadingComponent, ThemedLoadingComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -9,12 +9,12 @@
</div> </div>
<div class="col-2"> <div class="col-2">
<div class="btn-group relationship-action-buttons"> <div class="btn-group relationship-action-buttons">
<button [disabled]="!canRemove()" (click)="openVirtualMetadataModal(virtualMetadataModal)" <button [dsBtnDisabled]="!canRemove()" (click)="openVirtualMetadataModal(virtualMetadataModal)"
class="btn btn-outline-danger btn-sm" class="btn btn-outline-danger btn-sm"
title="{{'item.edit.metadata.edit.buttons.remove' | translate}}"> title="{{'item.edit.metadata.edit.buttons.remove' | translate}}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
<button [disabled]="!canUndo()" (click)="undo()" <button [dsBtnDisabled]="!canUndo()" (click)="undo()"
class="btn btn-outline-warning btn-sm" class="btn btn-outline-warning btn-sm"
title="{{'item.edit.metadata.edit.buttons.undo' | translate}}"> title="{{'item.edit.metadata.edit.buttons.undo' | translate}}">
<i class="fas fa-undo-alt fa-fw"></i> <i class="fas fa-undo-alt fa-fw"></i>

View File

@@ -37,6 +37,7 @@ import {
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { ViewMode } from '../../../../core/shared/view-mode.model'; import { ViewMode } from '../../../../core/shared/view-mode.model';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import { import {
hasValue, hasValue,
isNotEmpty, isNotEmpty,
@@ -54,6 +55,7 @@ import { VirtualMetadataComponent } from '../../virtual-metadata/virtual-metadat
NgIf, NgIf,
TranslateModule, TranslateModule,
VirtualMetadataComponent, VirtualMetadataComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -35,7 +35,7 @@
<ng-template #buttons> <ng-template #buttons>
<div class="d-flex space-children-mr justify-content-end"> <div class="d-flex space-children-mr justify-content-end">
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true" <button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
[disabled]="(hasChanges$ | async) !== true" [dsBtnDisabled]="(hasChanges$ | async) !== true"
(click)="discard()"> (click)="discard()">
<i aria-hidden="true" class="fas fa-times"></i> <i aria-hidden="true" class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{ 'item.edit.metadata.discard-button' | translate }}</span> <span class="d-none d-sm-inline">&nbsp;{{ 'item.edit.metadata.discard-button' | translate }}</span>
@@ -45,7 +45,7 @@
<span class="d-none d-sm-inline">&nbsp;{{ 'item.edit.metadata.reinstate-button' | translate }}</span> <span class="d-none d-sm-inline">&nbsp;{{ 'item.edit.metadata.reinstate-button' | translate }}</span>
</button> </button>
<button class="btn btn-primary" <button class="btn btn-primary"
[disabled]="(hasChanges$ | async) !== true || (isSaving$ | async) === true" [dsBtnDisabled]="(hasChanges$ | async) !== true || (isSaving$ | async) === true"
(click)="submit()"> (click)="submit()">
<span *ngIf="isSaving$ | async" aria-hidden="true" class="spinner-border spinner-border-sm" role="status"></span> <span *ngIf="isSaving$ | async" aria-hidden="true" class="spinner-border spinner-border-sm" role="status"></span>
<i *ngIf="(isSaving$ | async) !== true" aria-hidden="true" class="fas fa-save"></i> <i *ngIf="(isSaving$ | async) !== true" aria-hidden="true" class="fas fa-save"></i>

View File

@@ -42,6 +42,7 @@ import {
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
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 { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
@@ -67,6 +68,7 @@ import { EditRelationshipListWrapperComponent } from './edit-relationship-list-w
TranslateModule, TranslateModule,
VarDirective, VarDirective,
EditRelationshipListWrapperComponent, EditRelationshipListWrapperComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -19,7 +19,7 @@
<div class="buttons" *ngIf="medias?.length > 1"> <div class="buttons" *ngIf="medias?.length > 1">
<button <button
class="btn btn-primary previous" class="btn btn-primary previous"
[disabled]="currentIndex === 0" [dsBtnDisabled]="currentIndex === 0"
(click)="prevMedia()" (click)="prevMedia()"
> >
{{ "media-viewer.previous" | translate }} {{ "media-viewer.previous" | translate }}
@@ -27,7 +27,7 @@
<button <button
class="btn btn-primary next" class="btn btn-primary next"
[disabled]="currentIndex === medias.length - 1" [dsBtnDisabled]="currentIndex === medias.length - 1"
(click)="nextMedia()" (click)="nextMedia()"
> >
{{ "media-viewer.next" | translate }} {{ "media-viewer.next" | translate }}

View File

@@ -12,6 +12,7 @@ import { Bitstream } from 'src/app/core/shared/bitstream.model';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model'; import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { CaptionInfo } from './caption-info'; import { CaptionInfo } from './caption-info';
import { languageHelper } from './language-helper'; import { languageHelper } from './language-helper';
@@ -27,6 +28,7 @@ import { languageHelper } from './language-helper';
NgbDropdownModule, NgbDropdownModule,
TranslateModule, TranslateModule,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -48,7 +48,7 @@
<div class="row" *ngIf="(ownerCanDisconnectProfileFromOrcid() | async)" data-test="unlinkOwner"> <div class="row" *ngIf="(ownerCanDisconnectProfileFromOrcid() | async)" data-test="unlinkOwner">
<div class="col"> <div class="col">
<button type="submit" class="btn btn-danger float-right" (click)="unlinkOrcid()" <button type="submit" class="btn btn-danger float-right" (click)="unlinkOrcid()"
[disabled]="(unlinkProcessing | async)"> [dsBtnDisabled]="(unlinkProcessing | async)">
<span *ngIf="(unlinkProcessing | async) !== true"><i <span *ngIf="(unlinkProcessing | async) !== true"><i
class="fas fa-unlink"></i> {{ 'person.page.orcid.unlink' | translate }}</span> class="fas fa-unlink"></i> {{ 'person.page.orcid.unlink' | translate }}</span>
<span *ngIf="(unlinkProcessing | async)"><i <span *ngIf="(unlinkProcessing | async)"><i

View File

@@ -34,6 +34,7 @@ import {
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { AlertComponent } from '../../../shared/alert/alert.component'; import { AlertComponent } from '../../../shared/alert/alert.component';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { createFailedRemoteDataObjectFromError$ } from '../../../shared/remote-data.utils'; import { createFailedRemoteDataObjectFromError$ } from '../../../shared/remote-data.utils';
@@ -47,6 +48,7 @@ import { createFailedRemoteDataObjectFromError$ } from '../../../shared/remote-d
NgIf, NgIf,
NgForOf, NgForOf,
AlertComponent, AlertComponent,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -7,12 +7,12 @@
<ds-loading *ngIf="(i + 1) === objects.length && (itemsRD || i > 0) && !(itemsRD?.hasSucceeded && itemsRD?.payload && itemsRD?.payload?.page?.length > 0)" message="{{'loading.default' | translate}}"></ds-loading> <ds-loading *ngIf="(i + 1) === objects.length && (itemsRD || i > 0) && !(itemsRD?.hasSucceeded && itemsRD?.payload && itemsRD?.payload?.page?.length > 0)" message="{{'loading.default' | translate}}"></ds-loading>
<div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && itemsRD?.payload?.page?.length > 0"> <div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && itemsRD?.payload?.page?.length > 0">
<div *ngIf="itemsRD?.payload?.totalPages > objects.length" class="float-left" id="view-more"> <div *ngIf="itemsRD?.payload?.totalPages > objects.length" class="float-left" id="view-more">
<button class="btn btn-link btn-link-inline" (click)="increase()">{{'item.page.related-items.view-more' | <button class="btn btn-link btn-link-inline text-capitalize" (click)="increase()">{{'item.page.related-items.view-more' |
translate:{ amount: (itemsRD?.payload?.totalElements - (incrementBy * objects.length) < incrementBy) ? itemsRD?.payload?.totalElements - (incrementBy * objects.length) : incrementBy } }}</button> translate:{ amount: (itemsRD?.payload?.totalElements - (incrementBy * objects.length) < incrementBy) ? itemsRD?.payload?.totalElements - (incrementBy * objects.length) : incrementBy } }} {{label}}</button>
</div> </div>
<div *ngIf="objects.length > 1" class="float-right" id="view-less"> <div *ngIf="objects.length > 1" class="float-right" id="view-less">
<button class="btn btn-link btn-link-inline" (click)="decrease()">{{'item.page.related-items.view-less' | <button class="btn btn-link btn-link-inline text-capitalize" (click)="decrease()">{{'item.page.related-items.view-less' |
translate:{ amount: itemsRD?.payload?.page?.length } }}</button> translate:{ amount: itemsRD?.payload?.page?.length } }} {{label}}</button>
</div> </div>
</div> </div>
</ng-container> </ng-container>

View File

@@ -31,7 +31,7 @@
<!--CREATE--> <!--CREATE-->
<ng-container *ngIf="canCreateVersion$ | async"> <ng-container *ngIf="canCreateVersion$ | async">
<button class="btn btn-outline-primary btn-sm version-row-element-create" <button class="btn btn-outline-primary btn-sm version-row-element-create"
[disabled]="isAnyBeingEdited() || hasDraftVersion" [dsBtnDisabled]="isAnyBeingEdited() || hasDraftVersion"
(click)="createNewVersion(version)" (click)="createNewVersion(version)"
title="{{createVersionTitle | translate }}"> title="{{createVersionTitle | translate }}">
<i class="fas fa-code-branch fa-fw"></i> <i class="fas fa-code-branch fa-fw"></i>
@@ -41,7 +41,7 @@
<ng-container *ngIf="canDeleteVersion$ | async"> <ng-container *ngIf="canDeleteVersion$ | async">
<button class="btn btn-sm version-row-element-delete" <button class="btn btn-sm version-row-element-delete"
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'" [ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
[disabled]="isAnyBeingEdited()" [dsBtnDisabled]="isAnyBeingEdited()"
(click)="deleteVersion(version, version.id === itemVersion.id)" (click)="deleteVersion(version, version.id === itemVersion.id)"
title="{{'item.version.history.table.action.deleteVersion' | translate}}"> title="{{'item.version.history.table.action.deleteVersion' | translate}}">
<i class="fas fa-trash fa-fw"></i> <i class="fas fa-trash fa-fw"></i>

View File

@@ -49,6 +49,7 @@ import { VersionHistory } from '../../../core/shared/version-history.model';
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model'; import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service'; import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service'; import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { import {
getItemEditVersionhistoryRoute, getItemEditVersionhistoryRoute,
@@ -67,6 +68,7 @@ import { ItemVersionsSummaryModalComponent } from '../item-versions-summary-moda
TranslateModule, TranslateModule,
NgClass, NgClass,
NgIf, NgIf,
BtnDisabledDirective,
], ],
templateUrl: './item-versions-row-element-version.component.html', templateUrl: './item-versions-row-element-version.component.html',
styleUrl: './item-versions-row-element-version.component.scss', styleUrl: './item-versions-row-element-version.component.scss',

View File

@@ -67,7 +67,7 @@
<ng-template #notThisBeingEdited> <ng-template #notThisBeingEdited>
<!--EDIT--> <!--EDIT-->
<button class="btn btn-outline-primary btn-sm version-row-element-edit" <button class="btn btn-outline-primary btn-sm version-row-element-edit"
[disabled]="isAnyBeingEdited()" [dsBtnDisabled]="isAnyBeingEdited()"
(click)="enableVersionEditing(versionDTO.version)" (click)="enableVersionEditing(versionDTO.version)"
title="{{'item.version.history.table.action.editSummary' | translate}}"> title="{{'item.version.history.table.action.editSummary' | translate}}">
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>

View File

@@ -42,6 +42,7 @@ import { VersionHistory } from '../../core/shared/version-history.model';
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-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 { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { PaginationComponent } from '../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
@@ -158,7 +159,7 @@ describe('ItemVersionsComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterModule.forRoot([]), CommonModule, FormsModule, ReactiveFormsModule, BrowserModule, ItemVersionsComponent, VarDirective], imports: [TranslateModule.forRoot(), RouterModule.forRoot([]), CommonModule, FormsModule, ReactiveFormsModule, BrowserModule, ItemVersionsComponent, VarDirective, BtnDisabledDirective],
providers: [ providers: [
{ provide: PaginationService, useValue: new PaginationServiceStub() }, { provide: PaginationService, useValue: new PaginationServiceStub() },
{ provide: UntypedFormBuilder, useValue: new UntypedFormBuilder() }, { provide: UntypedFormBuilder, useValue: new UntypedFormBuilder() },
@@ -234,8 +235,9 @@ describe('ItemVersionsComponent', () => {
it('should not disable the delete button', () => { it('should not disable the delete button', () => {
const deleteButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.version-row-element-delete')); const deleteButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.version-row-element-delete'));
expect(deleteButtons.length).not.toBe(0); expect(deleteButtons.length).not.toBe(0);
deleteButtons.forEach((btn: DebugElement) => { deleteButtons.forEach((btn) => {
expect(btn.nativeElement.disabled).toBe(false); expect(btn.nativeElement.getAttribute('aria-disabled')).toBe('false');
expect(btn.nativeElement.classList.contains('disabled')).toBeFalse();
}); });
}); });

View File

@@ -48,6 +48,7 @@ import { Version } from '../../core/shared/version.model';
import { VersionHistory } from '../../core/shared/version-history.model'; import { VersionHistory } from '../../core/shared/version-history.model';
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 { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { import {
hasValue, hasValue,
hasValueOperator, hasValueOperator,
@@ -85,6 +86,7 @@ interface VersionDTO {
NgIf, NgIf,
PaginationComponent, PaginationComponent,
TranslateModule, TranslateModule,
BtnDisabledDirective,
], ],
}) })

View File

@@ -169,6 +169,7 @@ export class MenuResolverService {
this.createExportMenuSections(); this.createExportMenuSections();
this.createImportMenuSections(); this.createImportMenuSections();
this.createAccessControlMenuSections(); this.createAccessControlMenuSections();
this.createReportMenuSections();
return this.waitForMenu$(MenuID.ADMIN); return this.waitForMenu$(MenuID.ADMIN);
} }

View File

@@ -1,6 +1,6 @@
<div class="add" *ngIf="(moreThanOne$ | async) !== true"> <div class="add" *ngIf="(moreThanOne$ | async) !== true">
<button class="btn btn-lg btn-outline-primary mt-1 ml-2" <button class="btn btn-lg btn-outline-primary mt-1 ml-2"
[attr.aria-label]="'mydspace.new-submission-external' | translate" [disabled]="(initialized$ | async) !== true" [attr.aria-label]="'mydspace.new-submission-external' | translate" [dsBtnDisabled]="(initialized$ | async) !== true"
(click)="openPage(singleEntity)" role="button" (click)="openPage(singleEntity)" role="button"
title="{{'mydspace.new-submission-external' | translate}}"> title="{{'mydspace.new-submission-external' | translate}}">
<i class="fa fa-file-import" aria-hidden="true"></i> <i class="fa fa-file-import" aria-hidden="true"></i>
@@ -10,7 +10,7 @@
ngbDropdown ngbDropdown
*ngIf="(moreThanOne$ | async)"> *ngIf="(moreThanOne$ | async)">
<button class="btn btn-lg btn-outline-primary mt-1 ml-2" id="dropdownImport" ngbDropdownToggle <button class="btn btn-lg btn-outline-primary mt-1 ml-2" id="dropdownImport" ngbDropdownToggle
type="button" [disabled]="(initialized$ | async) !== true" type="button" [dsBtnDisabled]="(initialized$ | async) !== true"
[attr.aria-label]="'mydspace.new-submission-external' | translate" [attr.aria-label]="'mydspace.new-submission-external' | translate"
[attr.data-test]="'import-dropdown' | dsBrowserOnly" [attr.data-test]="'import-dropdown' | dsBrowserOnly"
title="{{'mydspace.new-submission-external' | translate}}"> title="{{'mydspace.new-submission-external' | translate}}">

View File

@@ -27,6 +27,7 @@ import { FindListOptions } from '../../../core/data/find-list-options.model';
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 { ItemType } from '../../../core/shared/item-relationships/item-type.model'; import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { EntityDropdownComponent } from '../../../shared/entity-dropdown/entity-dropdown.component'; import { EntityDropdownComponent } from '../../../shared/entity-dropdown/entity-dropdown.component';
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe'; import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
@@ -45,6 +46,7 @@ import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
TranslateModule, TranslateModule,
BrowserOnlyPipe, BrowserOnlyPipe,
NgIf, NgIf,
BtnDisabledDirective,
], ],
standalone: true, standalone: true,
}) })

Some files were not shown because too many files have changed in this diff Show More