mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'dspace/main' into accessibility-settings-main
# Conflicts: # cypress/support/e2e.ts # src/app/core/auth/auth.service.ts # src/app/footer/footer.component.html
This commit is contained in:
@@ -160,6 +160,9 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"@angular-eslint/prefer-standalone": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
"@angular-eslint/no-attribute-decorator": "error",
|
"@angular-eslint/no-attribute-decorator": "error",
|
||||||
"@angular-eslint/no-output-native": "warn",
|
"@angular-eslint/no-output-native": "warn",
|
||||||
"@angular-eslint/no-output-on-prefix": "warn",
|
"@angular-eslint/no-output-on-prefix": "warn",
|
||||||
|
33
.github/workflows/build.yml
vendored
33
.github/workflows/build.yml
vendored
@@ -29,6 +29,8 @@ jobs:
|
|||||||
DSPACE_CACHE_SERVERSIDE_ANONYMOUSCACHE_MAX: 0
|
DSPACE_CACHE_SERVERSIDE_ANONYMOUSCACHE_MAX: 0
|
||||||
# Tell Cypress to run e2e tests using the same UI URL
|
# Tell Cypress to run e2e tests using the same UI URL
|
||||||
CYPRESS_BASE_URL: http://127.0.0.1:4000
|
CYPRESS_BASE_URL: http://127.0.0.1:4000
|
||||||
|
# Disable the cookie consent banner in e2e tests to avoid errors because of elements hidden by it
|
||||||
|
DSPACE_INFO_ENABLECOOKIECONSENTPOPUP: false
|
||||||
# When Chrome version is specified, we pin to a specific version of Chrome
|
# When Chrome version is specified, we pin to a specific version of Chrome
|
||||||
# Comment this out to use the latest release
|
# Comment this out to use the latest release
|
||||||
#CHROME_VERSION: "90.0.4430.212-1"
|
#CHROME_VERSION: "90.0.4430.212-1"
|
||||||
@@ -268,6 +270,37 @@ jobs:
|
|||||||
echo "$result"
|
echo "$result"
|
||||||
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental & Architectural Phenomenology Vol. 28, No. 1"
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental & Architectural Phenomenology Vol. 28, No. 1"
|
||||||
|
|
||||||
|
# Verify 301 Handle redirect behavior
|
||||||
|
# Note: /handle/123456789/260 is the same test Publication used by our e2e tests
|
||||||
|
- name: Verify 301 redirect from '/handle' URLs
|
||||||
|
run: |
|
||||||
|
result=$(wget --server-response --quiet http://127.0.0.1:4000/handle/123456789/260 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result"
|
||||||
|
[[ "$result" -eq "301" ]]
|
||||||
|
|
||||||
|
# Verify 403 error code behavior
|
||||||
|
- name: Verify 403 error code from '/403'
|
||||||
|
run: |
|
||||||
|
result=$(wget --server-response --quiet http://127.0.0.1:4000/403 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result"
|
||||||
|
[[ "$result" -eq "403" ]]
|
||||||
|
|
||||||
|
# Verify 404 error code behavior
|
||||||
|
- name: Verify 404 error code from '/404' and on invalid pages
|
||||||
|
run: |
|
||||||
|
result=$(wget --server-response --quiet http://127.0.0.1:4000/404 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result"
|
||||||
|
result2=$(wget --server-response --quiet http://127.0.0.1:4000/invalidurl 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result2"
|
||||||
|
[[ "$result" -eq "404" && "$result2" -eq "404" ]]
|
||||||
|
|
||||||
|
# Verify 500 error code behavior
|
||||||
|
- name: Verify 500 error code from '/500'
|
||||||
|
run: |
|
||||||
|
result=$(wget --server-response --quiet http://127.0.0.1:4000/500 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result"
|
||||||
|
[[ "$result" -eq "500" ]]
|
||||||
|
|
||||||
- name: Stop running app
|
- name: Stop running app
|
||||||
run: kill -9 $(lsof -t -i:4000)
|
run: kill -9 $(lsof -t -i:4000)
|
||||||
|
|
||||||
|
@@ -23,10 +23,24 @@ ssr:
|
|||||||
# Determining which styles are critical is a relatively expensive operation; this option is
|
# Determining which styles are critical is a relatively expensive operation; this option is
|
||||||
# disabled (false) by default to boost server performance at the expense of loading smoothness.
|
# 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.
|
# Patterns to be run as regexes against the path of the page to check if SSR is allowed.
|
||||||
# NOTE: The "/handle/" path ensures Handle redirects work via SSR. The "/reload/" path ensures
|
# If the path match any of the regexes it will be served directly in CSR.
|
||||||
# hard refreshes (e.g. after login) trigger SSR while fully reloading the page.
|
# By default, excludes community and collection browse, global browse, global search, community list, statistics and various administrative tools.
|
||||||
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/', '/reload/' ]
|
excludePathPatterns:
|
||||||
|
- pattern: "^/communities/[a-f0-9-]{36}/browse(/.*)?$",
|
||||||
|
flag: "i"
|
||||||
|
- pattern: "^/collections/[a-f0-9-]{36}/browse(/.*)?$"
|
||||||
|
flag: "i"
|
||||||
|
- pattern: "^/browse/"
|
||||||
|
- pattern: "^/search$"
|
||||||
|
- pattern: "^/community-list$"
|
||||||
|
- pattern: "^/admin/"
|
||||||
|
- pattern: "^/processes/?"
|
||||||
|
- pattern: "^/notifications/"
|
||||||
|
- pattern: "^/statistics/?"
|
||||||
|
- pattern: "^/access-control/"
|
||||||
|
- pattern: "^/health$"
|
||||||
|
|
||||||
# Whether to enable rendering of Search component on SSR.
|
# 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 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.
|
# If set to false the component will not be included in the HTML returned from the server side rendering.
|
||||||
@@ -453,6 +467,8 @@ info:
|
|||||||
enableEndUserAgreement: true
|
enableEndUserAgreement: true
|
||||||
enablePrivacyStatement: true
|
enablePrivacyStatement: true
|
||||||
enableCOARNotifySupport: true
|
enableCOARNotifySupport: true
|
||||||
|
# Whether to show the cookie consent popup and the cookie settings footer link or not.
|
||||||
|
enableCookieConsentPopup: true
|
||||||
|
|
||||||
# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
||||||
# display in supported metadata fields. By default, only dc.description.abstract is supported.
|
# display in supported metadata fields. By default, only dc.description.abstract is supported.
|
||||||
|
@@ -15,24 +15,24 @@ describe('Header', () => {
|
|||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
|
|
||||||
// Click the language switcher (globe) in header
|
// Click the language switcher (globe) in header
|
||||||
cy.get('a[data-test="lang-switch"]').click();
|
cy.get('button[data-test="lang-switch"]').click();
|
||||||
// Click on the "Deusch" language in dropdown
|
// Click on the "Deusch" language in dropdown
|
||||||
cy.get('#language-menu-list li').contains('Deutsch').click();
|
cy.get('#language-menu-list div[role="option"]').contains('Deutsch').click();
|
||||||
|
|
||||||
// HTML "lang" attribute should switch to "de"
|
// HTML "lang" attribute should switch to "de"
|
||||||
cy.get('html').invoke('attr', 'lang').should('eq', 'de');
|
cy.get('html').invoke('attr', 'lang').should('eq', 'de');
|
||||||
|
|
||||||
// Login menu should now be in German
|
// Login menu should now be in German
|
||||||
cy.get('a[data-test="login-menu"]').contains('Anmelden');
|
cy.get('[data-test="login-menu"]').contains('Anmelden');
|
||||||
|
|
||||||
// Change back to English from language switcher
|
// Change back to English from language switcher
|
||||||
cy.get('a[data-test="lang-switch"]').click();
|
cy.get('button[data-test="lang-switch"]').click();
|
||||||
cy.get('#language-menu-list li').contains('English').click();
|
cy.get('#language-menu-list div[role="option"]').contains('English').click();
|
||||||
|
|
||||||
// HTML "lang" attribute should switch to "en"
|
// HTML "lang" attribute should switch to "en"
|
||||||
cy.get('html').invoke('attr', 'lang').should('eq', 'en');
|
cy.get('html').invoke('attr', 'lang').should('eq', 'en');
|
||||||
|
|
||||||
// Login menu should now be in English
|
// Login menu should now be in English
|
||||||
cy.get('a[data-test="login-menu"]').contains('Log In');
|
cy.get('[data-test="login-menu"]').contains('Log In');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -26,6 +26,12 @@ describe('Homepage', () => {
|
|||||||
// Wait for homepage tag to appear
|
// Wait for homepage tag to appear
|
||||||
cy.get('ds-home-page').should('be.visible');
|
cy.get('ds-home-page').should('be.visible');
|
||||||
|
|
||||||
|
// Wait for at least one loading component to show up
|
||||||
|
cy.get('ds-loading').should('exist');
|
||||||
|
|
||||||
|
// Wait until all loading components have disappeared
|
||||||
|
cy.get('ds-loading').should('not.exist');
|
||||||
|
|
||||||
// Analyze <ds-home-page> for accessibility issues
|
// Analyze <ds-home-page> for accessibility issues
|
||||||
testA11y('ds-home-page');
|
testA11y('ds-home-page');
|
||||||
});
|
});
|
||||||
|
@@ -54,12 +54,9 @@ before(() => {
|
|||||||
|
|
||||||
// Runs once before the first test in each "block"
|
// Runs once before the first test in each "block"
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Pre-agree to all Orejime cookies by setting the orejime-* cookies
|
// Pre-agree to all Orejime cookies by setting the orejime-anonymous cookie
|
||||||
// This just ensures it doesn't get in the way of matching other objects in the page.
|
// This just ensures it doesn't get in the way of matching other objects in the page.
|
||||||
const cookieContent = '{"authentication":true,"preferences":true,"acknowledgement":true,"google-analytics":true,"matomo":true,"google-recaptcha":true,"accessibility":true}';
|
cy.setCookie('orejime-anonymous', '{"authentication":true,"preferences":true,"acknowledgement":true,"google-analytics":true,"correlation-id":true,"accessibility":true}');
|
||||||
cy.setCookie('orejime-anonymous', cookieContent);
|
|
||||||
cy.setCookie(`orejime-${Cypress.env('DSPACE_TEST_ADMIN_USER_UUID')}`, cookieContent);
|
|
||||||
cy.setCookie(`orejime-${Cypress.env('DSPACE_TEST_SUBMIT_USER_UUID')}`, cookieContent);
|
|
||||||
|
|
||||||
// Remove any CSRF cookies saved from prior tests
|
// Remove any CSRF cookies saved from prior tests
|
||||||
cy.clearCookie(DSPACE_XSRF_COOKIE);
|
cy.clearCookie(DSPACE_XSRF_COOKIE);
|
||||||
|
30
package-lock.json
generated
30
package-lock.json
generated
@@ -42,7 +42,7 @@
|
|||||||
"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.41.0",
|
"core-js": "^3.42.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",
|
||||||
@@ -150,12 +150,12 @@
|
|||||||
"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.87.0",
|
"sass": "~1.88.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",
|
||||||
"typescript": "~5.4.5",
|
"typescript": "~5.4.5",
|
||||||
"webpack": "5.99.7",
|
"webpack": "5.99.8",
|
||||||
"webpack-cli": "^5.1.4",
|
"webpack-cli": "^5.1.4",
|
||||||
"webpack-dev-server": "^4.15.1"
|
"webpack-dev-server": "^4.15.1"
|
||||||
}
|
}
|
||||||
@@ -9108,9 +9108,9 @@
|
|||||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
||||||
},
|
},
|
||||||
"node_modules/bootstrap": {
|
"node_modules/bootstrap": {
|
||||||
"version": "5.3.5",
|
"version": "5.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
|
||||||
"integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==",
|
"integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -10198,9 +10198,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/core-js": {
|
"node_modules/core-js": {
|
||||||
"version": "3.41.0",
|
"version": "3.42.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz",
|
||||||
"integrity": "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==",
|
"integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -20677,9 +20677,9 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.87.0",
|
"version": "1.88.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz",
|
||||||
"integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==",
|
"integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -22772,9 +22772,9 @@
|
|||||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||||
},
|
},
|
||||||
"node_modules/webpack": {
|
"node_modules/webpack": {
|
||||||
"version": "5.99.7",
|
"version": "5.99.8",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
|
||||||
"integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==",
|
"integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@@ -124,7 +124,7 @@
|
|||||||
"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.41.0",
|
"core-js": "^3.42.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",
|
||||||
@@ -232,12 +232,12 @@
|
|||||||
"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.87.0",
|
"sass": "~1.88.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",
|
||||||
"typescript": "~5.4.5",
|
"typescript": "~5.4.5",
|
||||||
"webpack": "5.99.7",
|
"webpack": "5.99.8",
|
||||||
"webpack-cli": "^5.1.4",
|
"webpack-cli": "^5.1.4",
|
||||||
"webpack-dev-server": "^4.15.1"
|
"webpack-dev-server": "^4.15.1"
|
||||||
}
|
}
|
||||||
|
18
server.ts
18
server.ts
@@ -58,6 +58,7 @@ import {
|
|||||||
REQUEST,
|
REQUEST,
|
||||||
RESPONSE,
|
RESPONSE,
|
||||||
} from './src/express.tokens';
|
} from './src/express.tokens';
|
||||||
|
import { SsrExcludePatterns } from "./src/config/ssr-config.interface";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set path for the browser application's dist folder
|
* Set path for the browser application's dist folder
|
||||||
@@ -221,7 +222,7 @@ export function app() {
|
|||||||
* The callback function to serve server side angular
|
* The callback function to serve server side angular
|
||||||
*/
|
*/
|
||||||
function ngApp(req, res, next) {
|
function ngApp(req, res, next) {
|
||||||
if (environment.ssr.enabled && req.method === 'GET' && (req.path === '/' || environment.ssr.paths.some(pathPrefix => req.path.startsWith(pathPrefix)))) {
|
if (environment.ssr.enabled && req.method === 'GET' && (req.path === '/' || !isExcludedFromSsr(req.path, environment.ssr.excludePathPatterns))) {
|
||||||
// Render the page to user via SSR (server side rendering)
|
// Render the page to user via SSR (server side rendering)
|
||||||
serverSideRender(req, res, next);
|
serverSideRender(req, res, next);
|
||||||
} else {
|
} else {
|
||||||
@@ -627,6 +628,21 @@ function start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if SSR should be skipped for path
|
||||||
|
*
|
||||||
|
* @param path
|
||||||
|
* @param excludePathPattern
|
||||||
|
*/
|
||||||
|
function isExcludedFromSsr(path: string, excludePathPattern: SsrExcludePatterns[]): boolean {
|
||||||
|
const patterns = excludePathPattern.map(p =>
|
||||||
|
new RegExp(p.pattern, p.flag || '')
|
||||||
|
);
|
||||||
|
return patterns.some((regex) => {
|
||||||
|
return regex.test(path)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The callback function to serve health check requests
|
* The callback function to serve health check requests
|
||||||
*/
|
*/
|
||||||
|
@@ -20,6 +20,7 @@ import { BrowseByPageComponent } from './browse-by-page.component';
|
|||||||
@Component({
|
@Component({
|
||||||
// eslint-disable-next-line @angular-eslint/component-selector
|
// eslint-disable-next-line @angular-eslint/component-selector
|
||||||
selector: '',
|
selector: '',
|
||||||
|
standalone: true,
|
||||||
template: '<span id="BrowseByTestComponent"></span>',
|
template: '<span id="BrowseByTestComponent"></span>',
|
||||||
})
|
})
|
||||||
class BrowseByTestComponent {
|
class BrowseByTestComponent {
|
||||||
|
@@ -20,6 +20,7 @@ import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
|
|||||||
@Component({
|
@Component({
|
||||||
// eslint-disable-next-line @angular-eslint/component-selector
|
// eslint-disable-next-line @angular-eslint/component-selector
|
||||||
selector: '',
|
selector: '',
|
||||||
|
standalone: true,
|
||||||
template: '<span id="BrowseByTestComponent"></span>',
|
template: '<span id="BrowseByTestComponent"></span>',
|
||||||
})
|
})
|
||||||
class BrowseByTestComponent {
|
class BrowseByTestComponent {
|
||||||
|
@@ -257,6 +257,16 @@ export class AuthService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authenticated user id from the store
|
||||||
|
* @returns {User}
|
||||||
|
*/
|
||||||
|
public getAuthenticatedUserIdFromStore(): Observable<string> {
|
||||||
|
return this.store.pipe(
|
||||||
|
select(getAuthenticatedUserId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable which emits the currently authenticated user from the store,
|
* Returns an observable which emits the currently authenticated user from the store,
|
||||||
* or null if the user is not authenticated.
|
* or null if the user is not authenticated.
|
||||||
|
@@ -10,12 +10,18 @@ import {
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { StoreModule } from '@ngrx/store';
|
import { StoreModule } from '@ngrx/store';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
appReducers,
|
appReducers,
|
||||||
storeModuleConfig,
|
storeModuleConfig,
|
||||||
} from '../../app.reducer';
|
} from '../../app.reducer';
|
||||||
import { CorrelationIdService } from '../../correlation-id/correlation-id.service';
|
import { CorrelationIdService } from '../../correlation-id/correlation-id.service';
|
||||||
|
import { OrejimeService } from '../../shared/cookies/orejime.service';
|
||||||
|
import {
|
||||||
|
CORRELATION_ID_COOKIE,
|
||||||
|
CORRELATION_ID_OREJIME_KEY,
|
||||||
|
} from '../../shared/cookies/orejime-configuration';
|
||||||
import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
|
import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
|
||||||
import { RouterStub } from '../../shared/testing/router.stub';
|
import { RouterStub } from '../../shared/testing/router.stub';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
@@ -40,6 +46,8 @@ describe('LogInterceptor', () => {
|
|||||||
const mockStatusCode = 200;
|
const mockStatusCode = 200;
|
||||||
const mockStatusText = 'SUCCESS';
|
const mockStatusText = 'SUCCESS';
|
||||||
|
|
||||||
|
const mockOrejimeService = jasmine.createSpyObj('OrejimeService', ['getSavedPreferences']);
|
||||||
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -56,6 +64,7 @@ describe('LogInterceptor', () => {
|
|||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
{ provide: CorrelationIdService, useClass: CorrelationIdService },
|
{ provide: CorrelationIdService, useClass: CorrelationIdService },
|
||||||
{ provide: UUIDService, useClass: UUIDService },
|
{ provide: UUIDService, useClass: UUIDService },
|
||||||
|
{ provide: OrejimeService, useValue: mockOrejimeService },
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
],
|
],
|
||||||
@@ -66,12 +75,14 @@ describe('LogInterceptor', () => {
|
|||||||
cookieService = TestBed.inject(CookieService);
|
cookieService = TestBed.inject(CookieService);
|
||||||
correlationIdService = TestBed.inject(CorrelationIdService);
|
correlationIdService = TestBed.inject(CorrelationIdService);
|
||||||
|
|
||||||
cookieService.set('CORRELATION-ID','123455');
|
cookieService.set(CORRELATION_ID_COOKIE,'123455');
|
||||||
correlationIdService.initCorrelationId();
|
correlationIdService.setCorrelationId();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('headers should be set', (done) => {
|
it('headers should be set when cookie is accepted', (done) => {
|
||||||
|
mockOrejimeService.getSavedPreferences.and.returnValue(of({ [CORRELATION_ID_OREJIME_KEY]: true }));
|
||||||
|
|
||||||
service.request(RestRequestMethod.POST, 'server/api/core/items', 'test', { withCredentials: false }).subscribe((response) => {
|
service.request(RestRequestMethod.POST, 'server/api/core/items', 'test', { withCredentials: false }).subscribe((response) => {
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
done();
|
done();
|
||||||
@@ -83,7 +94,23 @@ describe('LogInterceptor', () => {
|
|||||||
expect(httpRequest.request.headers.has('X-REFERRER')).toBeTrue();
|
expect(httpRequest.request.headers.has('X-REFERRER')).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('headers should have the right values', (done) => {
|
it('headers should not be set when cookie is declined', (done) => {
|
||||||
|
mockOrejimeService.getSavedPreferences.and.returnValue(of({ [CORRELATION_ID_OREJIME_KEY]: false }));
|
||||||
|
|
||||||
|
service.request(RestRequestMethod.POST, 'server/api/core/items', 'test', { withCredentials: false }).subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
const httpRequest = httpMock.expectOne('server/api/core/items');
|
||||||
|
httpRequest.flush(mockPayload, { status: mockStatusCode, statusText: mockStatusText });
|
||||||
|
expect(httpRequest.request.headers.has('X-CORRELATION-ID')).toBeFalse();
|
||||||
|
expect(httpRequest.request.headers.has('X-REFERRER')).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('headers should have the right values when cookie is accepted', (done) => {
|
||||||
|
mockOrejimeService.getSavedPreferences.and.returnValue(of({ [CORRELATION_ID_OREJIME_KEY]: true }));
|
||||||
|
|
||||||
service.request(RestRequestMethod.POST, 'server/api/core/items', 'test', { withCredentials: false }).subscribe((response) => {
|
service.request(RestRequestMethod.POST, 'server/api/core/items', 'test', { withCredentials: false }).subscribe((response) => {
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
done();
|
done();
|
||||||
|
@@ -7,9 +7,15 @@ import {
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { CorrelationIdService } from '../../correlation-id/correlation-id.service';
|
import { CorrelationIdService } from '../../correlation-id/correlation-id.service';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { OrejimeService } from '../../shared/cookies/orejime.service';
|
||||||
|
import { CORRELATION_ID_OREJIME_KEY } from '../../shared/cookies/orejime-configuration';
|
||||||
|
import {
|
||||||
|
hasValue,
|
||||||
|
isEmpty,
|
||||||
|
} from '../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log Interceptor intercepting Http Requests & Responses to
|
* Log Interceptor intercepting Http Requests & Responses to
|
||||||
@@ -19,22 +25,37 @@ import { hasValue } from '../../shared/empty.util';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class LogInterceptor implements HttpInterceptor {
|
export class LogInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
constructor(private cidService: CorrelationIdService, private router: Router) {}
|
constructor(
|
||||||
|
private cidService: CorrelationIdService,
|
||||||
|
private router: Router,
|
||||||
|
private orejimeService: OrejimeService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
return this.orejimeService.getSavedPreferences().pipe(
|
||||||
// Get the correlation id for the user from the store
|
switchMap(preferences => {
|
||||||
const correlationId = this.cidService.getCorrelationId();
|
// Check if the user has declined correlation id tracking
|
||||||
|
const correlationDeclined =
|
||||||
|
isEmpty(preferences) ||
|
||||||
|
isEmpty(preferences[CORRELATION_ID_OREJIME_KEY]) ||
|
||||||
|
!preferences[CORRELATION_ID_OREJIME_KEY];
|
||||||
|
|
||||||
// Add headers from the intercepted request
|
// Add headers from the intercepted request
|
||||||
let headers = request.headers;
|
let headers = request.headers;
|
||||||
|
if (!correlationDeclined) {
|
||||||
|
// Get the correlation id for the user from the store
|
||||||
|
const correlationId = this.cidService.getCorrelationId();
|
||||||
if (hasValue(correlationId)) {
|
if (hasValue(correlationId)) {
|
||||||
headers = headers.append('X-CORRELATION-ID', correlationId);
|
headers = headers.append('X-CORRELATION-ID', correlationId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
headers = headers.append('X-REFERRER', this.router.url);
|
headers = headers.append('X-REFERRER', this.router.url);
|
||||||
|
|
||||||
// Add new headers to the intercepted request
|
// Add new headers to the intercepted request
|
||||||
request = request.clone({ withCredentials: true, headers: headers });
|
request = request.clone({ withCredentials: true, headers: headers });
|
||||||
return next.handle(request);
|
return next.handle(request);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { getTestScheduler } from 'jasmine-marbles';
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { of } from 'rxjs';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model';
|
import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model';
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
SubmissionRequest,
|
SubmissionRequest,
|
||||||
} from '../data/request.models';
|
} from '../data/request.models';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { RequestEntry } from '../data/request-entry.model';
|
||||||
import { SubmissionRestService } from './submission-rest.service';
|
import { SubmissionRestService } from './submission-rest.service';
|
||||||
|
|
||||||
describe('SubmissionRestService test suite', () => {
|
describe('SubmissionRestService test suite', () => {
|
||||||
@@ -38,7 +40,9 @@ describe('SubmissionRestService test suite', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService(of(Object.assign(new RequestEntry(), {
|
||||||
|
request: new SubmissionRequest('mock-request-uuid', 'mock-request-href'),
|
||||||
|
})));
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
halService = new HALEndpointServiceStub(resourceEndpointURL);
|
halService = new HALEndpointServiceStub(resourceEndpointURL);
|
||||||
@@ -62,7 +66,7 @@ describe('SubmissionRestService test suite', () => {
|
|||||||
scheduler.schedule(() => service.getDataById(resourceEndpoint, resourceScope).subscribe());
|
scheduler.schedule(() => service.getDataById(resourceEndpoint, resourceScope).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(requestService.send).toHaveBeenCalledWith(expected);
|
expect(requestService.send).toHaveBeenCalledWith(expected, false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,15 +1,20 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import {
|
||||||
|
Observable,
|
||||||
|
skipWhile,
|
||||||
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
filter,
|
filter,
|
||||||
map,
|
map,
|
||||||
mergeMap,
|
mergeMap,
|
||||||
|
switchMap,
|
||||||
tap,
|
tap,
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
hasValue,
|
hasValue,
|
||||||
|
hasValueOperator,
|
||||||
isNotEmpty,
|
isNotEmpty,
|
||||||
} from '../../shared/empty.util';
|
} from '../../shared/empty.util';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
@@ -25,7 +30,6 @@ import {
|
|||||||
} from '../data/request.models';
|
} from '../data/request.models';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { RequestError } from '../data/request-error.model';
|
import { RequestError } from '../data/request-error.model';
|
||||||
import { RestRequest } from '../data/rest-request.model';
|
|
||||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
@@ -33,6 +37,23 @@ import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-respon
|
|||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { SubmissionResponse } from './submission-response.model';
|
import { SubmissionResponse } from './submission-response.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the first emitting payload's dataDefinition, or throw an error if the request failed
|
||||||
|
*/
|
||||||
|
export const getFirstDataDefinition = () =>
|
||||||
|
(source: Observable<RemoteData<SubmissionResponse>>): Observable<SubmitDataResponseDefinitionObject> =>
|
||||||
|
source.pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((response: RemoteData<SubmissionResponse>) => {
|
||||||
|
if (response.hasFailed) {
|
||||||
|
throw new ErrorResponse({ statusText: response.errorMessage, statusCode: response.statusCode } as RequestError);
|
||||||
|
} else {
|
||||||
|
return hasValue(response?.payload?.dataDefinition) ? response.payload.dataDefinition : [response.payload];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The service handling all submission REST requests
|
* The service handling all submission REST requests
|
||||||
*/
|
*/
|
||||||
@@ -56,15 +77,7 @@ export class SubmissionRestService {
|
|||||||
*/
|
*/
|
||||||
protected fetchRequest(requestId: string): Observable<SubmitDataResponseDefinitionObject> {
|
protected fetchRequest(requestId: string): Observable<SubmitDataResponseDefinitionObject> {
|
||||||
return this.rdbService.buildFromRequestUUID<SubmissionResponse>(requestId).pipe(
|
return this.rdbService.buildFromRequestUUID<SubmissionResponse>(requestId).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstDataDefinition(),
|
||||||
map((response: RemoteData<SubmissionResponse>) => {
|
|
||||||
if (response.hasFailed) {
|
|
||||||
throw new ErrorResponse({ statusText: response.errorMessage, statusCode: response.statusCode } as RequestError);
|
|
||||||
} else {
|
|
||||||
return hasValue(response.payload) ? response.payload.dataDefinition : response.payload;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,21 +129,52 @@ export class SubmissionRestService {
|
|||||||
* The endpoint link name
|
* The endpoint link name
|
||||||
* @param id
|
* @param id
|
||||||
* The submission Object to retrieve
|
* The submission Object to retrieve
|
||||||
|
* @param useCachedVersionIfAvailable
|
||||||
|
* If this is true, the request will only be sent if there's no valid & cached version. Defaults to false
|
||||||
* @return Observable<SubmitDataResponseDefinitionObject>
|
* @return Observable<SubmitDataResponseDefinitionObject>
|
||||||
* server response
|
* server response
|
||||||
*/
|
*/
|
||||||
public getDataById(linkName: string, id: string): Observable<SubmitDataResponseDefinitionObject> {
|
public getDataById(linkName: string, id: string, useCachedVersionIfAvailable = false): Observable<SubmitDataResponseDefinitionObject> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
|
||||||
return this.halService.getEndpoint(linkName).pipe(
|
return this.halService.getEndpoint(linkName).pipe(
|
||||||
map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, id)),
|
map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, id)),
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
map((endpointURL: string) => new SubmissionRequest(requestId, endpointURL)),
|
mergeMap((endpointURL: string) => {
|
||||||
tap((request: RestRequest) => {
|
this.sendGetDataRequest(endpointURL, useCachedVersionIfAvailable);
|
||||||
this.requestService.send(request);
|
const startTime: number = new Date().getTime();
|
||||||
|
return this.requestService.getByHref(endpointURL).pipe(
|
||||||
|
map((requestEntry) => requestEntry?.request?.uuid),
|
||||||
|
hasValueOperator(),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap((requestId) => this.rdbService.buildFromRequestUUID<SubmissionResponse>(requestId)),
|
||||||
|
// This skip ensures that if a stale object is present in the cache when you do a
|
||||||
|
// call it isn't immediately returned, but we wait until the remote data for the new request
|
||||||
|
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
||||||
|
// cached completed object
|
||||||
|
skipWhile((rd: RemoteData<SubmissionResponse>) => rd.isStale || (!useCachedVersionIfAvailable && rd.lastUpdated < startTime)),
|
||||||
|
tap((rd: RemoteData<SubmissionResponse>) => {
|
||||||
|
if (hasValue(rd) && rd.isStale) {
|
||||||
|
this.sendGetDataRequest(endpointURL, useCachedVersionIfAvailable);
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
mergeMap(() => this.fetchRequest(requestId)),
|
);
|
||||||
distinctUntilChanged());
|
}),
|
||||||
|
getFirstDataDefinition(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a GET SubmissionRequest
|
||||||
|
*
|
||||||
|
* @param href
|
||||||
|
* Endpoint URL of the submission data
|
||||||
|
* @param useCachedVersionIfAvailable
|
||||||
|
* If this is true, the request will only be sent if there's no valid & cached version. Defaults to false
|
||||||
|
*/
|
||||||
|
private sendGetDataRequest(href: string, useCachedVersionIfAvailable = false) {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
const request = new SubmissionRequest(requestId, href);
|
||||||
|
this.requestService.send(request, useCachedVersionIfAvailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -4,6 +4,7 @@ import {
|
|||||||
StoreModule,
|
StoreModule,
|
||||||
} from '@ngrx/store';
|
} from '@ngrx/store';
|
||||||
import { MockStore } from '@ngrx/store/testing';
|
import { MockStore } from '@ngrx/store/testing';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
appReducers,
|
appReducers,
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
storeModuleConfig,
|
storeModuleConfig,
|
||||||
} from '../app.reducer';
|
} from '../app.reducer';
|
||||||
import { UUIDService } from '../core/shared/uuid.service';
|
import { UUIDService } from '../core/shared/uuid.service';
|
||||||
|
import { CORRELATION_ID_COOKIE } from '../shared/cookies/orejime-configuration';
|
||||||
import { CookieServiceMock } from '../shared/mocks/cookie.service.mock';
|
import { CookieServiceMock } from '../shared/mocks/cookie.service.mock';
|
||||||
import { SetCorrelationIdAction } from './correlation-id.actions';
|
import { SetCorrelationIdAction } from './correlation-id.actions';
|
||||||
import { CorrelationIdService } from './correlation-id.service';
|
import { CorrelationIdService } from './correlation-id.service';
|
||||||
@@ -34,7 +36,13 @@ describe('CorrelationIdService', () => {
|
|||||||
cookieService = new CookieServiceMock();
|
cookieService = new CookieServiceMock();
|
||||||
uuidService = new UUIDService();
|
uuidService = new UUIDService();
|
||||||
store = TestBed.inject(Store) as MockStore<AppState>;
|
store = TestBed.inject(Store) as MockStore<AppState>;
|
||||||
service = new CorrelationIdService(cookieService, uuidService, store);
|
const mockOrejimeService = {
|
||||||
|
getSavedPreferences: () => of({ CORRELATION_ID_OREJIME_KEY: true }),
|
||||||
|
initialize: jasmine.createSpy('initialize'),
|
||||||
|
showSettings: jasmine.createSpy('showSettings'),
|
||||||
|
};
|
||||||
|
|
||||||
|
service = new CorrelationIdService(cookieService, uuidService, store, mockOrejimeService, { nativeWindow: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getCorrelationId', () => {
|
describe('getCorrelationId', () => {
|
||||||
@@ -46,45 +54,45 @@ describe('CorrelationIdService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('initCorrelationId', () => {
|
describe('setCorrelationId', () => {
|
||||||
const cookieCID = 'cookie CID';
|
const cookieCID = 'cookie CID';
|
||||||
const storeCID = 'store CID';
|
const storeCID = 'store CID';
|
||||||
|
|
||||||
it('should set cookie and store values to a newly generated value if neither ex', () => {
|
it('should set cookie and store values to a newly generated value if neither ex', () => {
|
||||||
service.initCorrelationId();
|
service.setCorrelationId();
|
||||||
|
|
||||||
expect(cookieService.get('CORRELATION-ID')).toBeTruthy();
|
expect(cookieService.get(CORRELATION_ID_COOKIE)).toBeTruthy();
|
||||||
expect(service.getCorrelationId()).toBeTruthy();
|
expect(service.getCorrelationId()).toBeTruthy();
|
||||||
expect(cookieService.get('CORRELATION-ID')).toEqual(service.getCorrelationId());
|
expect(cookieService.get(CORRELATION_ID_COOKIE)).toEqual(service.getCorrelationId());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set store value to cookie value if present', () => {
|
it('should set store value to cookie value if present', () => {
|
||||||
expect(service.getCorrelationId()).toBe(null);
|
expect(service.getCorrelationId()).toBe(null);
|
||||||
|
|
||||||
cookieService.set('CORRELATION-ID', cookieCID);
|
cookieService.set(CORRELATION_ID_COOKIE, cookieCID);
|
||||||
|
|
||||||
service.initCorrelationId();
|
service.setCorrelationId();
|
||||||
|
|
||||||
expect(cookieService.get('CORRELATION-ID')).toBe(cookieCID);
|
expect(cookieService.get(CORRELATION_ID_COOKIE)).toBe(cookieCID);
|
||||||
expect(service.getCorrelationId()).toBe(cookieCID);
|
expect(service.getCorrelationId()).toBe(cookieCID);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set cookie value to store value if present', () => {
|
it('should set cookie value to store value if present', () => {
|
||||||
store.dispatch(new SetCorrelationIdAction(storeCID));
|
store.dispatch(new SetCorrelationIdAction(storeCID));
|
||||||
|
|
||||||
service.initCorrelationId();
|
service.setCorrelationId();
|
||||||
|
|
||||||
expect(cookieService.get('CORRELATION-ID')).toBe(storeCID);
|
expect(cookieService.get(CORRELATION_ID_COOKIE)).toBe(storeCID);
|
||||||
expect(service.getCorrelationId()).toBe(storeCID);
|
expect(service.getCorrelationId()).toBe(storeCID);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set store value to cookie value if both are present', () => {
|
it('should set store value to cookie value if both are present', () => {
|
||||||
cookieService.set('CORRELATION-ID', cookieCID);
|
cookieService.set(CORRELATION_ID_COOKIE, cookieCID);
|
||||||
store.dispatch(new SetCorrelationIdAction(storeCID));
|
store.dispatch(new SetCorrelationIdAction(storeCID));
|
||||||
|
|
||||||
service.initCorrelationId();
|
service.setCorrelationId();
|
||||||
|
|
||||||
expect(cookieService.get('CORRELATION-ID')).toBe(cookieCID);
|
expect(cookieService.get(CORRELATION_ID_COOKIE)).toBe(cookieCID);
|
||||||
expect(service.getCorrelationId()).toBe(cookieCID);
|
expect(service.getCorrelationId()).toBe(cookieCID);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import {
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
select,
|
select,
|
||||||
Store,
|
Store,
|
||||||
@@ -7,8 +10,20 @@ import { take } from 'rxjs/operators';
|
|||||||
|
|
||||||
import { AppState } from '../app.reducer';
|
import { AppState } from '../app.reducer';
|
||||||
import { CookieService } from '../core/services/cookie.service';
|
import { CookieService } from '../core/services/cookie.service';
|
||||||
|
import {
|
||||||
|
NativeWindowRef,
|
||||||
|
NativeWindowService,
|
||||||
|
} from '../core/services/window.service';
|
||||||
import { UUIDService } from '../core/shared/uuid.service';
|
import { UUIDService } from '../core/shared/uuid.service';
|
||||||
import { isEmpty } from '../shared/empty.util';
|
import { OrejimeService } from '../shared/cookies/orejime.service';
|
||||||
|
import {
|
||||||
|
CORRELATION_ID_COOKIE,
|
||||||
|
CORRELATION_ID_OREJIME_KEY,
|
||||||
|
} from '../shared/cookies/orejime-configuration';
|
||||||
|
import {
|
||||||
|
hasValue,
|
||||||
|
isEmpty,
|
||||||
|
} from '../shared/empty.util';
|
||||||
import { SetCorrelationIdAction } from './correlation-id.actions';
|
import { SetCorrelationIdAction } from './correlation-id.actions';
|
||||||
import { correlationIdSelector } from './correlation-id.selector';
|
import { correlationIdSelector } from './correlation-id.selector';
|
||||||
|
|
||||||
@@ -24,15 +39,32 @@ export class CorrelationIdService {
|
|||||||
protected cookieService: CookieService,
|
protected cookieService: CookieService,
|
||||||
protected uuidService: UUIDService,
|
protected uuidService: UUIDService,
|
||||||
protected store: Store<AppState>,
|
protected store: Store<AppState>,
|
||||||
|
protected orejimeService: OrejimeService,
|
||||||
|
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||||
) {
|
) {
|
||||||
|
if (this._window?.nativeWindow) {
|
||||||
|
this._window.nativeWindow.initCorrelationId = () => this.initCorrelationId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the correlation id is allowed to be set, then set it
|
||||||
|
*/
|
||||||
|
initCorrelationId(): void {
|
||||||
|
this.orejimeService?.getSavedPreferences().subscribe(preferences => {
|
||||||
|
if (hasValue(preferences) && preferences[CORRELATION_ID_OREJIME_KEY]) {
|
||||||
|
this.setCorrelationId();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the correlation id based on the cookie or the ngrx store
|
* Initialize the correlation id based on the cookie or the ngrx store
|
||||||
*/
|
*/
|
||||||
initCorrelationId(): void {
|
setCorrelationId(): void {
|
||||||
// first see of there's a cookie with a correlation-id
|
// first see of there's a cookie with a correlation-id
|
||||||
let correlationId = this.cookieService.get('CORRELATION-ID');
|
let correlationId = this.cookieService.get(CORRELATION_ID_COOKIE);
|
||||||
|
|
||||||
// if there isn't see if there's an ID in the store
|
// if there isn't see if there's an ID in the store
|
||||||
if (isEmpty(correlationId)) {
|
if (isEmpty(correlationId)) {
|
||||||
@@ -46,7 +78,7 @@ export class CorrelationIdService {
|
|||||||
|
|
||||||
// Store the correct id both in the store and as a cookie to ensure they're in sync
|
// Store the correct id both in the store and as a cookie to ensure they're in sync
|
||||||
this.store.dispatch(new SetCorrelationIdAction(correlationId));
|
this.store.dispatch(new SetCorrelationIdAction(correlationId));
|
||||||
this.cookieService.set('CORRELATION-ID', correlationId);
|
this.cookieService.set(CORRELATION_ID_COOKIE, correlationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -116,6 +116,14 @@ describe('MetadataFieldSelectorComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should sort the fields by name to ensure the one without a qualifier is first', () => {
|
||||||
|
component.mdField = 'dc.relation';
|
||||||
|
|
||||||
|
component.validate();
|
||||||
|
|
||||||
|
expect(registryService.queryMetadataFields).toHaveBeenCalledWith('dc.relation', { elementsPerPage: 20, sort: new SortOptions('fieldName', SortDirection.ASC), currentPage: 1 }, true, false, followLink('schema'));
|
||||||
|
});
|
||||||
|
|
||||||
describe('when querying the metadata fields returns an error response', () => {
|
describe('when querying the metadata fields returns an error response', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(registryService.queryMetadataFields as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Failed'));
|
(registryService.queryMetadataFields as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Failed'));
|
||||||
|
@@ -43,6 +43,7 @@ import {
|
|||||||
SortDirection,
|
SortDirection,
|
||||||
SortOptions,
|
SortOptions,
|
||||||
} from '../../../core/cache/models/sort-options.model';
|
} from '../../../core/cache/models/sort-options.model';
|
||||||
|
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||||
import { RegistryService } from '../../../core/registry/registry.service';
|
import { RegistryService } from '../../../core/registry/registry.service';
|
||||||
import {
|
import {
|
||||||
getAllSucceededRemoteData,
|
getAllSucceededRemoteData,
|
||||||
@@ -153,7 +154,10 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
|
|||||||
/**
|
/**
|
||||||
* Default page option for this feature
|
* Default page option for this feature
|
||||||
*/
|
*/
|
||||||
pageOptions = { elementsPerPage: 20, sort: new SortOptions('fieldName', SortDirection.ASC) };
|
pageOptions: FindListOptions = {
|
||||||
|
elementsPerPage: 20,
|
||||||
|
sort: new SortOptions('fieldName', SortDirection.ASC),
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
constructor(protected registryService: RegistryService,
|
constructor(protected registryService: RegistryService,
|
||||||
@@ -209,7 +213,7 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
|
|||||||
* Upon subscribing to the returned observable, the showInvalid flag is updated accordingly to show the feedback under the input
|
* Upon subscribing to the returned observable, the showInvalid flag is updated accordingly to show the feedback under the input
|
||||||
*/
|
*/
|
||||||
validate(): Observable<boolean> {
|
validate(): Observable<boolean> {
|
||||||
return this.registryService.queryMetadataFields(this.mdField, null, true, false, followLink('schema')).pipe(
|
return this.registryService.queryMetadataFields(this.mdField, Object.assign({}, this.pageOptions, { currentPage: 1 }), true, false, followLink('schema')).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
switchMap((rd) => {
|
switchMap((rd) => {
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
@@ -263,9 +267,7 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
|
|||||||
* @param useCache Whether or not to use the cache
|
* @param useCache Whether or not to use the cache
|
||||||
*/
|
*/
|
||||||
search(query: string, page: number, useCache: boolean = true) {
|
search(query: string, page: number, useCache: boolean = true) {
|
||||||
return this.registryService.queryMetadataFields(query,{
|
return this.registryService.queryMetadataFields(query, Object.assign({}, this.pageOptions, { currentPage: page }), useCache, false, followLink('schema'))
|
||||||
elementsPerPage: this.pageOptions.elementsPerPage, sort: this.pageOptions.sort,
|
|
||||||
currentPage: page }, useCache, false, followLink('schema'))
|
|
||||||
.pipe(
|
.pipe(
|
||||||
getAllSucceededRemoteData(),
|
getAllSucceededRemoteData(),
|
||||||
metadataFieldsToString(),
|
metadataFieldsToString(),
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<a
|
<a
|
||||||
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
|
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<a
|
<a
|
||||||
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
|
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<a
|
<a
|
||||||
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
|
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
@if (linkType !== linkTypes.None) {
|
@if (linkType !== linkTypes.None) {
|
||||||
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
||||||
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" role="link" tabindex="0">
|
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" tabindex="-1">
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
@if (linkType !== linkTypes.None) {
|
@if (linkType !== linkTypes.None) {
|
||||||
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
||||||
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" role="link" tabindex="0">
|
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" tabindex="-1">
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<div class="col-3 col-md-2">
|
<div class="col-3 col-md-2">
|
||||||
@if (linkType !== linkTypes.None) {
|
@if (linkType !== linkTypes.None) {
|
||||||
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
||||||
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" role="link" tabindex="0">
|
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" tabindex="-1">
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -36,13 +36,13 @@
|
|||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isJournalVolumeOfIssue'"
|
[relationType]="'isJournalVolumeOfIssue'"
|
||||||
[label]="'relationships.isSingleVolumeOf' | translate">
|
[label]="'item.page.journal-volume' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-related-items
|
<ds-related-items
|
||||||
class="mb-1 mt-1"
|
class="mb-1 mt-1"
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isPublicationOfJournalIssue'"
|
[relationType]="'isPublicationOfJournalIssue'"
|
||||||
[label]="'relationships.isPublicationOfJournalIssue' | translate">
|
[label]="'item.page.articles' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
[fields]="['dc.description']"
|
[fields]="['dc.description']"
|
||||||
|
@@ -24,12 +24,12 @@
|
|||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isJournalOfVolume'"
|
[relationType]="'isJournalOfVolume'"
|
||||||
[label]="'relationships.isSingleJournalOf' | translate">
|
[label]="'item.page.journal' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isIssueOfJournalVolume'"
|
[relationType]="'isIssueOfJournalVolume'"
|
||||||
[label]="'relationships.isIssueOf' | translate">
|
[label]="'item.page.journal-issues' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
[fields]="['dc.description']"
|
[fields]="['dc.description']"
|
||||||
|
@@ -28,7 +28,7 @@
|
|||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isVolumeOfJournal'"
|
[relationType]="'isVolumeOfJournal'"
|
||||||
[label]="'relationships.isVolumeOf' | translate">
|
[label]="'item.page.journal-volumes' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-generic-item-page-field class="item-page-fields" [item]="object"
|
<ds-generic-item-page-field class="item-page-fields" [item]="object"
|
||||||
[fields]="['dc.description']"
|
[fields]="['dc.description']"
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<a
|
<a
|
||||||
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
|
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<a
|
<a
|
||||||
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
|
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<a
|
<a
|
||||||
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
|
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
@if (linkType !== linkTypes.None) {
|
@if (linkType !== linkTypes.None) {
|
||||||
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
||||||
[routerLink]="[itemPageRoute]" class="dont-break-out" role="link" tabindex="0">
|
[routerLink]="[itemPageRoute]" class="dont-break-out" tabindex="-1">
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
|
||||||
[defaultImage]="'assets/images/orgunit-placeholder.svg'"
|
[defaultImage]="'assets/images/orgunit-placeholder.svg'"
|
||||||
[alt]="'thumbnail.orgunit.alt'"
|
[alt]="'thumbnail.orgunit.alt'"
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
@if (linkType !== linkTypes.None) {
|
@if (linkType !== linkTypes.None) {
|
||||||
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
||||||
[routerLink]="[itemPageRoute]" class="dont-break-out" role="link" tabindex="0">
|
[routerLink]="[itemPageRoute]" class="dont-break-out" tabindex="-1">
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
|
||||||
[defaultImage]="'assets/images/person-placeholder.svg'"
|
[defaultImage]="'assets/images/person-placeholder.svg'"
|
||||||
[alt]="'thumbnail.person.alt'"
|
[alt]="'thumbnail.person.alt'"
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
@if (linkType !== linkTypes.None) {
|
@if (linkType !== linkTypes.None) {
|
||||||
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||||
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
||||||
[routerLink]="[itemPageRoute]" class="dont-break-out" role="link" tabindex="0">
|
[routerLink]="[itemPageRoute]" class="dont-break-out" tabindex="-1">
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
|
||||||
[defaultImage]="'assets/images/project-placeholder.svg'"
|
[defaultImage]="'assets/images/project-placeholder.svg'"
|
||||||
[alt]="'thumbnail.project.alt'"
|
[alt]="'thumbnail.project.alt'"
|
||||||
|
@@ -49,7 +49,7 @@
|
|||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isPublicationOfOrgUnit'"
|
[relationType]="'isPublicationOfOrgUnit'"
|
||||||
[label]="'relationships.isPublicationOf' | translate">
|
[label]="'item.page.publications' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
[fields]="['dc.description']"
|
[fields]="['dc.description']"
|
||||||
|
@@ -28,12 +28,12 @@
|
|||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isProjectOfPerson'"
|
[relationType]="'isProjectOfPerson'"
|
||||||
[label]="'relationships.isProjectOf' | translate">
|
[label]="'item.page.projects' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isOrgUnitOfPerson'"
|
[relationType]="'isOrgUnitOfPerson'"
|
||||||
[label]="'relationships.isOrgUnitOf' | translate">
|
[label]="'item.page.org-units' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
[fields]="['person.jobTitle']"
|
[fields]="['person.jobTitle']"
|
||||||
|
@@ -43,17 +43,17 @@
|
|||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isPersonOfProject'"
|
[relationType]="'isPersonOfProject'"
|
||||||
[label]="'relationships.isPersonOf' | translate">
|
[label]="'item.page.authors' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isPublicationOfProject'"
|
[relationType]="'isPublicationOfProject'"
|
||||||
[label]="'relationships.isPublicationOf' | translate">
|
[label]="'item.page.publications' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isOrgUnitOfProject'"
|
[relationType]="'isOrgUnitOfProject'"
|
||||||
[label]="'relationships.isOrgUnitOf' | translate">
|
[label]="'item.page.org-units' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
[fields]="['dc.description']"
|
[fields]="['dc.description']"
|
||||||
|
@@ -49,8 +49,8 @@
|
|||||||
<!-- Grid container -->
|
<!-- Grid container -->
|
||||||
|
|
||||||
<!-- Copyright -->
|
<!-- Copyright -->
|
||||||
<div class="bottom-footer p-1 d-flex justify-content-center align-items-center text-white">
|
<div class="bottom-footer p-1 d-flex flex-column flex-md-row justify-content-center align-items-center text-white">
|
||||||
<div class="content-container">
|
<div class="content-container align-self-center">
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
<a class="text-white"
|
<a class="text-white"
|
||||||
href="http://www.dspace.org/" role="link" tabindex="0">{{ 'footer.link.dspace' | translate}}</a>
|
href="http://www.dspace.org/" role="link" tabindex="0">{{ 'footer.link.dspace' | translate}}</a>
|
||||||
@@ -59,11 +59,13 @@
|
|||||||
href="https://www.lyrasis.org/" role="link" tabindex="0">{{ 'footer.link.lyrasis' | translate}}</a>
|
href="https://www.lyrasis.org/" role="link" tabindex="0">{{ 'footer.link.lyrasis' | translate}}</a>
|
||||||
</p>
|
</p>
|
||||||
<ul class="footer-info list-unstyled d-flex justify-content-center mb-0">
|
<ul class="footer-info list-unstyled d-flex justify-content-center mb-0">
|
||||||
|
@if (showCookieSettings) {
|
||||||
<li>
|
<li>
|
||||||
<button class="btn btn-link text-white" type="button" (click)="showCookieSettings()" role="button" tabindex="0">
|
<button class="btn btn-link text-white" type="button" (click)="openCookieSettings()" role="button" tabindex="0">
|
||||||
{{ 'footer.link.cookies' | translate}}
|
{{ 'footer.link.cookies' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
}
|
||||||
<li>
|
<li>
|
||||||
<a class="text-white"
|
<a class="text-white"
|
||||||
routerLink="info/accessibility">{{ 'footer.link.accessibility' | translate }}</a>
|
routerLink="info/accessibility">{{ 'footer.link.accessibility' | translate }}</a>
|
||||||
@@ -89,7 +91,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@if (coarLdnEnabled$ | async) {
|
@if (coarLdnEnabled$ | async) {
|
||||||
<div class="notify-enabled text-white">
|
<div class="notify-enabled text-white align-self-end">
|
||||||
<a class="coar-notify-support-route" routerLink="info/coar-notify-support" role="link" tabindex="0">
|
<a class="coar-notify-support-route" routerLink="info/coar-notify-support" role="link" tabindex="0">
|
||||||
<img class="n-coar" src="assets/images/n-coar.svg" [attr.alt]="'menu.header.image.logo' | translate" />
|
<img class="n-coar" src="assets/images/n-coar.svg" [attr.alt]="'menu.header.image.logo' | translate" />
|
||||||
{{ 'footer.link.coar-notify-support' | translate }}
|
{{ 'footer.link.coar-notify-support' | translate }}
|
||||||
|
@@ -23,9 +23,8 @@
|
|||||||
|
|
||||||
.bottom-footer {
|
.bottom-footer {
|
||||||
.notify-enabled {
|
.notify-enabled {
|
||||||
position: absolute;
|
position: relative;
|
||||||
bottom: 4px;
|
margin-top: 4px;
|
||||||
right: 0;
|
|
||||||
|
|
||||||
.coar-notify-support-route {
|
.coar-notify-support-route {
|
||||||
padding: 0 calc(var(--bs-spacer) / 2);
|
padding: 0 calc(var(--bs-spacer) / 2);
|
||||||
@@ -37,7 +36,11 @@
|
|||||||
margin-bottom: 8.5px;
|
margin-bottom: 8.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
margin-top: 20px;
|
@media screen and (min-width: map-get($grid-breakpoints, md)) {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ul {
|
ul {
|
||||||
li {
|
li {
|
||||||
|
@@ -63,21 +63,21 @@ describe('Footer component', () => {
|
|||||||
expect(comp.showEndUserAgreement).toBe(environment.info.enableEndUserAgreement);
|
expect(comp.showEndUserAgreement).toBe(environment.info.enableEndUserAgreement);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('showCookieSettings', () => {
|
describe('openCookieSettings', () => {
|
||||||
it('should call cookies.showSettings() if cookies is defined', () => {
|
it('should call cookies.showSettings() if cookies is defined', () => {
|
||||||
const cookies = jasmine.createSpyObj('cookies', ['showSettings']);
|
const cookies = jasmine.createSpyObj('cookies', ['showSettings']);
|
||||||
comp.cookies = cookies;
|
comp.cookies = cookies;
|
||||||
comp.showCookieSettings();
|
comp.openCookieSettings();
|
||||||
expect(cookies.showSettings).toHaveBeenCalled();
|
expect(cookies.showSettings).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call cookies.showSettings() if cookies is undefined', () => {
|
it('should not call cookies.showSettings() if cookies is undefined', () => {
|
||||||
comp.cookies = undefined;
|
comp.cookies = undefined;
|
||||||
expect(() => comp.showCookieSettings()).not.toThrow();
|
expect(() => comp.openCookieSettings()).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false', () => {
|
it('should return false', () => {
|
||||||
expect(comp.showCookieSettings()).toBeFalse();
|
expect(comp.openCookieSettings()).toBeFalse();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -39,6 +39,7 @@ export class FooterComponent implements OnInit {
|
|||||||
* A boolean representing if to show or not the top footer container
|
* A boolean representing if to show or not the top footer container
|
||||||
*/
|
*/
|
||||||
showTopFooter = false;
|
showTopFooter = false;
|
||||||
|
showCookieSettings = false;
|
||||||
showPrivacyPolicy: boolean;
|
showPrivacyPolicy: boolean;
|
||||||
showEndUserAgreement: boolean;
|
showEndUserAgreement: boolean;
|
||||||
showSendFeedback$: Observable<boolean>;
|
showSendFeedback$: Observable<boolean>;
|
||||||
@@ -53,14 +54,15 @@ export class FooterComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.showCookieSettings = this.appConfig.info.enableCookieConsentPopup;
|
||||||
this.showPrivacyPolicy = this.appConfig.info.enablePrivacyStatement;
|
this.showPrivacyPolicy = this.appConfig.info.enablePrivacyStatement;
|
||||||
this.showEndUserAgreement = this.appConfig.info.enableEndUserAgreement;
|
this.showEndUserAgreement = this.appConfig.info.enableEndUserAgreement;
|
||||||
this.coarLdnEnabled$ = this.appConfig.info.enableCOARNotifySupport ? this.notifyInfoService.isCoarConfigEnabled() : observableOf(false);
|
this.coarLdnEnabled$ = this.appConfig.info.enableCOARNotifySupport ? this.notifyInfoService.isCoarConfigEnabled() : observableOf(false);
|
||||||
this.showSendFeedback$ = this.authorizationService.isAuthorized(FeatureID.CanSendFeedback);
|
this.showSendFeedback$ = this.authorizationService.isAuthorized(FeatureID.CanSendFeedback);
|
||||||
}
|
}
|
||||||
|
|
||||||
showCookieSettings() {
|
openCookieSettings() {
|
||||||
if (hasValue(this.cookies)) {
|
if (hasValue(this.cookies) && this.cookies.showSettings instanceof Function) {
|
||||||
this.cookies.showSettings();
|
this.cookies.showSettings();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@@ -5,12 +5,14 @@
|
|||||||
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/>
|
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<nav role="navigation" [attr.aria-label]="'nav.user.description' | translate" class="navbar navbar-light navbar-expand-md flex-shrink-0 px-0">
|
<div class="navbar navbar-light navbar-expand-md flex-shrink-0 px-0">
|
||||||
<ds-search-navbar></ds-search-navbar>
|
<ds-search-navbar></ds-search-navbar>
|
||||||
|
<div role="toolbar" [attr.aria-label]="'nav.user.description' | translate">
|
||||||
<ds-lang-switch></ds-lang-switch>
|
<ds-lang-switch></ds-lang-switch>
|
||||||
<ds-context-help-toggle></ds-context-help-toggle>
|
<ds-context-help-toggle></ds-context-help-toggle>
|
||||||
<ds-auth-nav-menu></ds-auth-nav-menu>
|
<ds-auth-nav-menu></ds-auth-nav-menu>
|
||||||
<ds-impersonate-navbar></ds-impersonate-navbar>
|
<ds-impersonate-navbar></ds-impersonate-navbar>
|
||||||
|
</div>
|
||||||
@if (isMobile$ | async) {
|
@if (isMobile$ | async) {
|
||||||
<div class="ps-2">
|
<div class="ps-2">
|
||||||
<button class="navbar-toggler px-0" type="button" (click)="toggleNavbar()"
|
<button class="navbar-toggler px-0" type="button" (click)="toggleNavbar()"
|
||||||
@@ -20,7 +22,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</nav>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar, div[role="toolbar"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: calc(var(--bs-spacer) / 3);
|
gap: calc(var(--bs-spacer) / 3);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@@ -3,13 +3,17 @@
|
|||||||
<div class="mt-4" [ngClass]="placeholderFontClass" @fadeIn>
|
<div class="mt-4" [ngClass]="placeholderFontClass" @fadeIn>
|
||||||
<div class="d-flex flex-row border-bottom mb-4 pb-4"></div>
|
<div class="d-flex flex-row border-bottom mb-4 pb-4"></div>
|
||||||
<h2> {{ 'home.recent-submissions.head' | translate }}</h2>
|
<h2> {{ 'home.recent-submissions.head' | translate }}</h2>
|
||||||
|
<ul class="list-unstyled m-0 p-0">
|
||||||
@for (item of itemRD?.payload?.page; track item) {
|
@for (item of itemRD?.payload?.page; track item) {
|
||||||
<div class="my-4">
|
<li class="my-4">
|
||||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode" class="pb-4">
|
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode" class="pb-4">
|
||||||
</ds-listable-object-component-loader>
|
</ds-listable-object-component-loader>
|
||||||
</div>
|
</li>
|
||||||
}
|
}
|
||||||
<button (click)="onLoadMore()" class="btn btn-primary search-button mt-4 float-start ng-tns-c290-40" role="button" tabindex="0"> {{'vocabulary-treeview.load-more' | translate }} ...</button>
|
</ul>
|
||||||
|
<button (click)="onLoadMore()" class="btn btn-primary search-button mt-4 float-start" role="link" tabindex="0">
|
||||||
|
{{ 'vocabulary-treeview.load-more' | translate }} ...
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (itemRD?.hasFailed) {
|
@if (itemRD?.hasFailed) {
|
||||||
|
@@ -153,6 +153,8 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
relationshipMessageKey$: Observable<string>;
|
relationshipMessageKey$: Observable<string>;
|
||||||
|
|
||||||
|
currentEntityType$: Observable<ItemType>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list ID to save selected entities under
|
* The list ID to save selected entities under
|
||||||
*/
|
*/
|
||||||
@@ -222,20 +224,12 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
public getRelationshipMessageKey(): Observable<string> {
|
public getRelationshipMessageKey(): Observable<string> {
|
||||||
return observableCombineLatest([
|
return observableCombineLatest([
|
||||||
|
this.currentEntityType$,
|
||||||
this.getLabel(),
|
this.getLabel(),
|
||||||
this.relatedEntityType$,
|
this.relatedEntityType$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([label, relatedEntityType]) => {
|
map(([currentEntityType, label, relatedEntityType]: [ItemType, string, ItemType]) => {
|
||||||
if (hasValue(label) && label.indexOf('is') > -1 && label.indexOf('Of') > -1) {
|
return `relationships.${currentEntityType.label}.${label}.${relatedEntityType.label}`;
|
||||||
const relationshipLabel = `${label.substring(2, label.indexOf('Of'))}`;
|
|
||||||
if (relationshipLabel !== relatedEntityType.label) {
|
|
||||||
return `relationships.is${relationshipLabel}Of.${relatedEntityType.label}`;
|
|
||||||
} else {
|
|
||||||
return `relationships.is${relationshipLabel}Of`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -469,6 +463,17 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.currentEntityType$ = this.relationshipLeftAndRightType$.pipe(
|
||||||
|
map(([leftType, rightType]: [ItemType, ItemType]) => {
|
||||||
|
if (leftType.uuid === this.itemType.uuid) {
|
||||||
|
return leftType;
|
||||||
|
} else {
|
||||||
|
return rightType;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
hasValueOperator(),
|
||||||
|
);
|
||||||
|
|
||||||
this.relatedEntityType$.pipe(
|
this.relatedEntityType$.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
).subscribe(
|
).subscribe(
|
||||||
|
@@ -2,14 +2,12 @@
|
|||||||
<div *ngVar="(originals$ | async)?.payload as originals">
|
<div *ngVar="(originals$ | async)?.payload as originals">
|
||||||
@if (hasValuesInBundle(originals)) {
|
@if (hasValuesInBundle(originals)) {
|
||||||
<div>
|
<div>
|
||||||
<h3 class="h5 simple-view-element-header">{{"item.page.filesection.original.bundle" | translate}}</h3>
|
<h3 class="h5 simple-view-element-header">
|
||||||
|
{{ "item.page.filesection.original.bundle" | translate }}
|
||||||
|
</h3>
|
||||||
@if (originals?.page?.length > 0) {
|
@if (originals?.page?.length > 0) {
|
||||||
<ds-pagination
|
<ds-pagination [hideGear]="true" [hidePagerWhenSinglePage]="true" [paginationOptions]="originalOptions"
|
||||||
[hideGear]="true"
|
[collectionSize]="originals?.totalElements" [retainScrollPosition]="true">
|
||||||
[hidePagerWhenSinglePage]="true"
|
|
||||||
[paginationOptions]="originalOptions"
|
|
||||||
[collectionSize]="originals?.totalElements"
|
|
||||||
[retainScrollPosition]="true">
|
|
||||||
@for (file of originals?.page; track file) {
|
@for (file of originals?.page; track file) {
|
||||||
<div class="file-section row mb-3">
|
<div class="file-section row mb-3">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
@@ -17,21 +15,35 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-7">
|
<div class="col-7">
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-md-4">{{"item.page.filesection.name" | translate}}</dt>
|
<dt class="col-md-4">
|
||||||
|
{{ "item.page.filesection.name" | translate }}
|
||||||
|
</dt>
|
||||||
<dd class="col-md-8">{{ dsoNameService.getName(file) }}</dd>
|
<dd class="col-md-8">{{ dsoNameService.getName(file) }}</dd>
|
||||||
<dt class="col-md-4">{{"item.page.filesection.size" | translate}}</dt>
|
<dt class="col-md-4">
|
||||||
<dd class="col-md-8">{{(file.sizeBytes) | dsFileSize }}</dd>
|
{{ "item.page.filesection.size" | translate }}
|
||||||
<dt class="col-md-4">{{"item.page.filesection.format" | translate}}</dt>
|
</dt>
|
||||||
<dd class="col-md-8">{{(file.format | async)?.payload?.description}}</dd>
|
<dd class="col-md-8">{{ file.sizeBytes | dsFileSize }}</dd>
|
||||||
|
<dt class="col-md-4">
|
||||||
|
{{ "item.page.filesection.format" | translate }}
|
||||||
|
</dt>
|
||||||
|
<dd class="col-md-8">
|
||||||
|
{{ (file.format | async)?.payload?.description }}
|
||||||
|
</dd>
|
||||||
@if (file.hasMetadata('dc.description')) {
|
@if (file.hasMetadata('dc.description')) {
|
||||||
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
<dt class="col-md-4">
|
||||||
<dd class="col-md-8">{{file.firstMetadataValue("dc.description")}}</dd>
|
{{ "item.page.filesection.description" | translate }}
|
||||||
|
</dt>
|
||||||
|
<dd class="col-md-8">
|
||||||
|
{{ file.firstMetadataValue("dc.description") }}
|
||||||
|
</dd>
|
||||||
}
|
}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<ds-file-download-link [bitstream]="file" [item]="item">
|
<ds-file-download-link [showIcon]="true" [bitstream]="file" [item]="item" cssClasses="btn btn-outline-primary btn-download">
|
||||||
|
<span class="d-none d-md-inline">
|
||||||
{{ "item.page.filesection.download" | translate }}
|
{{ "item.page.filesection.download" | translate }}
|
||||||
|
</span>
|
||||||
</ds-file-download-link>
|
</ds-file-download-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,14 +56,12 @@
|
|||||||
<div *ngVar="(licenses$ | async)?.payload as licenses">
|
<div *ngVar="(licenses$ | async)?.payload as licenses">
|
||||||
@if (hasValuesInBundle(licenses)) {
|
@if (hasValuesInBundle(licenses)) {
|
||||||
<div>
|
<div>
|
||||||
<h3 class="h5 simple-view-element-header">{{"item.page.filesection.license.bundle" | translate}}</h3>
|
<h3 class="h5 simple-view-element-header">
|
||||||
|
{{ "item.page.filesection.license.bundle" | translate }}
|
||||||
|
</h3>
|
||||||
@if (licenses?.page?.length > 0) {
|
@if (licenses?.page?.length > 0) {
|
||||||
<ds-pagination
|
<ds-pagination [hideGear]="true" [hidePagerWhenSinglePage]="true" [paginationOptions]="licenseOptions"
|
||||||
[hideGear]="true"
|
[collectionSize]="licenses?.totalElements" [retainScrollPosition]="true">
|
||||||
[hidePagerWhenSinglePage]="true"
|
|
||||||
[paginationOptions]="licenseOptions"
|
|
||||||
[collectionSize]="licenses?.totalElements"
|
|
||||||
[retainScrollPosition]="true">
|
|
||||||
@for (file of licenses?.page; track file) {
|
@for (file of licenses?.page; track file) {
|
||||||
<div class="file-section row">
|
<div class="file-section row">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
@@ -59,19 +69,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-7">
|
<div class="col-7">
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-md-4">{{"item.page.filesection.name" | translate}}</dt>
|
<dt class="col-md-4">
|
||||||
|
{{ "item.page.filesection.name" | translate }}
|
||||||
|
</dt>
|
||||||
<dd class="col-md-8">{{ dsoNameService.getName(file) }}</dd>
|
<dd class="col-md-8">{{ dsoNameService.getName(file) }}</dd>
|
||||||
<dt class="col-md-4">{{"item.page.filesection.size" | translate}}</dt>
|
<dt class="col-md-4">
|
||||||
<dd class="col-md-8">{{(file.sizeBytes) | dsFileSize }}</dd>
|
{{ "item.page.filesection.size" | translate }}
|
||||||
<dt class="col-md-4">{{"item.page.filesection.format" | translate}}</dt>
|
</dt>
|
||||||
<dd class="col-md-8">{{(file.format | async)?.payload?.description}}</dd>
|
<dd class="col-md-8">{{ file.sizeBytes | dsFileSize }}</dd>
|
||||||
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
<dt class="col-md-4">
|
||||||
<dd class="col-md-8">{{file.firstMetadataValue("dc.description")}}</dd>
|
{{ "item.page.filesection.format" | translate }}
|
||||||
|
</dt>
|
||||||
|
<dd class="col-md-8">
|
||||||
|
{{ (file.format | async)?.payload?.description }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-md-4">
|
||||||
|
{{ "item.page.filesection.description" | translate }}
|
||||||
|
</dt>
|
||||||
|
<dd class="col-md-8">
|
||||||
|
{{ file.firstMetadataValue("dc.description") }}
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<ds-file-download-link [bitstream]="file" [item]="item">
|
<ds-file-download-link [showIcon]="true" [bitstream]="file" [item]="item" cssClasses="btn btn-outline-primary btn-download">
|
||||||
|
<span class="d-none d-md-inline">
|
||||||
{{ "item.page.filesection.download" | translate }}
|
{{ "item.page.filesection.download" | translate }}
|
||||||
|
</span>
|
||||||
</ds-file-download-link>
|
</ds-file-download-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
[showMessage]="false"
|
[showMessage]="false"
|
||||||
></ds-loading>
|
></ds-loading>
|
||||||
}
|
}
|
||||||
@if (!isLoading) {
|
@else {
|
||||||
<div class="media-viewer">
|
<div class="media-viewer">
|
||||||
@if (mediaList.length > 0) {
|
@if (mediaList.length > 0) {
|
||||||
<ng-container *ngVar="mediaOptions.video && ['audio', 'video'].includes(mediaList[0]?.format) as showVideo">
|
<ng-container *ngVar="mediaOptions.video && ['audio', 'video'].includes(mediaList[0]?.format) as showVideo">
|
||||||
@@ -33,18 +33,10 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
} @else {
|
} @else {
|
||||||
@if (mediaOptions.image && mediaOptions.video) {
|
|
||||||
<ds-media-viewer-image
|
|
||||||
[image]="(thumbnailsRD$ | async)?.payload?.page[0]?._links.content.href || thumbnailPlaceholder"
|
|
||||||
[preview]="false"
|
|
||||||
></ds-media-viewer-image>
|
|
||||||
}
|
|
||||||
@if (!(mediaOptions.image && mediaOptions.video)) {
|
|
||||||
<ds-thumbnail
|
<ds-thumbnail
|
||||||
[thumbnail]="(thumbnailsRD$ | async)?.payload?.page[0]">
|
[thumbnail]="(thumbnailsRD$ | async)?.payload?.page[0]">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import {
|
||||||
|
NO_ERRORS_SCHEMA,
|
||||||
|
PLATFORM_ID,
|
||||||
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
TestBed,
|
TestBed,
|
||||||
@@ -15,7 +18,9 @@ import { of as observableOf } from 'rxjs';
|
|||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
|
import { FileService } from '../../core/shared/file.service';
|
||||||
import { MediaViewerItem } from '../../core/shared/media-viewer-item.model';
|
import { MediaViewerItem } from '../../core/shared/media-viewer-item.model';
|
||||||
import { MetadataFieldWrapperComponent } from '../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
|
import { MetadataFieldWrapperComponent } from '../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
|
||||||
import { AuthServiceMock } from '../../shared/mocks/auth.service.mock';
|
import { AuthServiceMock } from '../../shared/mocks/auth.service.mock';
|
||||||
@@ -33,6 +38,9 @@ import { MediaViewerComponent } from './media-viewer.component';
|
|||||||
describe('MediaViewerComponent', () => {
|
describe('MediaViewerComponent', () => {
|
||||||
let comp: MediaViewerComponent;
|
let comp: MediaViewerComponent;
|
||||||
let fixture: ComponentFixture<MediaViewerComponent>;
|
let fixture: ComponentFixture<MediaViewerComponent>;
|
||||||
|
let authService;
|
||||||
|
let authorizationService;
|
||||||
|
let fileService;
|
||||||
|
|
||||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
||||||
sizeBytes: 10201,
|
sizeBytes: 10201,
|
||||||
@@ -57,7 +65,7 @@ describe('MediaViewerComponent', () => {
|
|||||||
'dc.title': [
|
'dc.title': [
|
||||||
{
|
{
|
||||||
language: null,
|
language: null,
|
||||||
value: 'test_word.docx',
|
value: 'test_image.jpg',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -75,6 +83,15 @@ describe('MediaViewerComponent', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
authService = jasmine.createSpyObj('AuthService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
});
|
||||||
|
authorizationService = jasmine.createSpyObj('AuthorizationService', {
|
||||||
|
isAuthorized: observableOf(true),
|
||||||
|
});
|
||||||
|
fileService = jasmine.createSpyObj('FileService', {
|
||||||
|
retrieveFileDownloadLink: null,
|
||||||
|
});
|
||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
@@ -90,6 +107,10 @@ describe('MediaViewerComponent', () => {
|
|||||||
MetadataFieldWrapperComponent,
|
MetadataFieldWrapperComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: FileService, useValue: fileService },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||||
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
||||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||||
@@ -153,9 +174,9 @@ describe('MediaViewerComponent', () => {
|
|||||||
expect(mediaItem.thumbnail).toBe(null);
|
expect(mediaItem.thumbnail).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display a default, thumbnail', () => {
|
it('should display a default thumbnail', () => {
|
||||||
const defaultThumbnail = fixture.debugElement.query(
|
const defaultThumbnail = fixture.debugElement.query(
|
||||||
By.css('ds-media-viewer-image'),
|
By.css('ds-thumbnail'),
|
||||||
);
|
);
|
||||||
expect(defaultThumbnail.nativeElement).toBeDefined();
|
expect(defaultThumbnail.nativeElement).toBeDefined();
|
||||||
});
|
});
|
||||||
|
@@ -45,7 +45,7 @@
|
|||||||
name="syncPublications" id="publicationOption_{{option.value}}" [value]="option.value"
|
name="syncPublications" id="publicationOption_{{option.value}}" [value]="option.value"
|
||||||
required>
|
required>
|
||||||
<label for="publicationOption_{{option.value}}"
|
<label for="publicationOption_{{option.value}}"
|
||||||
class="ms-2 form-label">{{option.label | translate}}</label>
|
class="form-label">{{option.label | translate}}</label>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
.form-check input, .form-check label{
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
@@ -50,6 +50,6 @@ export class ItemPageAuthorFieldComponent extends ItemPageFieldComponent {
|
|||||||
/**
|
/**
|
||||||
* Label i18n key for the rendered metadata
|
* Label i18n key for the rendered metadata
|
||||||
*/
|
*/
|
||||||
label = 'item.page.author';
|
label = 'item.page.authors';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -35,7 +35,7 @@
|
|||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[itemType]="'Person'"
|
[itemType]="'Person'"
|
||||||
[metadataFields]="['dc.contributor.author', 'dc.creator']"
|
[metadataFields]="['dc.contributor.author', 'dc.creator']"
|
||||||
[label]="'relationships.isAuthorOf' | translate">
|
[label]="'item.page.authors' | translate">
|
||||||
</ds-metadata-representation-list>
|
</ds-metadata-representation-list>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
[fields]="['journal.title']"
|
[fields]="['journal.title']"
|
||||||
@@ -58,17 +58,17 @@
|
|||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isProjectOfPublication'"
|
[relationType]="'isProjectOfPublication'"
|
||||||
[label]="'relationships.isProjectOf' | translate">
|
[label]="'item.page.projects' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isOrgUnitOfPublication'"
|
[relationType]="'isOrgUnitOfPublication'"
|
||||||
[label]="'relationships.isOrgUnitOf' | translate">
|
[label]="'item.page.org-units' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isJournalIssueOfPublication'"
|
[relationType]="'isJournalIssueOfPublication'"
|
||||||
[label]="'relationships.isJournalIssueOf' | translate">
|
[label]="'item.page.journal-issue' | translate">
|
||||||
</ds-related-items>
|
</ds-related-items>
|
||||||
<ds-item-page-abstract-field [item]="object"></ds-item-page-abstract-field>
|
<ds-item-page-abstract-field [item]="object"></ds-item-page-abstract-field>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
|
@@ -36,7 +36,7 @@
|
|||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[itemType]="'Person'"
|
[itemType]="'Person'"
|
||||||
[metadataFields]="['dc.contributor.author', 'dc.creator']"
|
[metadataFields]="['dc.contributor.author', 'dc.creator']"
|
||||||
[label]="'relationships.isAuthorOf' | translate">
|
[label]="'item.page.authors' | translate">
|
||||||
</ds-metadata-representation-list>
|
</ds-metadata-representation-list>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
[fields]="['journal.title']"
|
[fields]="['journal.title']"
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
@if ((isMobile$ | async) && (isAuthenticated$ | async)) {
|
@if ((isMobile$ | async) && (isAuthenticated$ | async)) {
|
||||||
<ds-user-menu [inExpandableNavbar]="true"></ds-user-menu>
|
<ds-user-menu [inExpandableNavbar]="true"></ds-user-menu>
|
||||||
}
|
}
|
||||||
<div class="navbar-nav align-items-md-center me-auto shadow-none gapx-3">
|
<div class="navbar-nav align-items-md-center me-auto shadow-none gapx-3" role="menubar">
|
||||||
@for (section of (sections | async); track section) {
|
@for (section of (sections | async); track section) {
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
|
*ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
|
||||||
|
@@ -10,6 +10,7 @@ import {
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-value-input',
|
selector: 'ds-value-input',
|
||||||
|
standalone: true,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
export abstract class ValueInputComponent<T> {
|
export abstract class ValueInputComponent<T> {
|
||||||
|
@@ -1,20 +1,25 @@
|
|||||||
|
@let isAuthenticated = (isAuthenticated$ | async);
|
||||||
@if ((isMobile$ | async) !== true) {
|
@if ((isMobile$ | async) !== true) {
|
||||||
<div class="navbar-nav me-auto" data-test="auth-nav">
|
<div class="navbar-nav me-auto" data-test="auth-nav">
|
||||||
@if ((isAuthenticated | async) !== true && (showAuth | async)) {
|
@let showAuth = (showAuth$ | async);
|
||||||
|
@if (isAuthenticated !== true && showAuth) {
|
||||||
<div
|
<div
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
(click)="$event.stopPropagation();">
|
(click)="$event.stopPropagation();">
|
||||||
<div ngbDropdown #loginDrop="ngbDropdown" display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
<div ngbDropdown #loginDrop="ngbDropdown" display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||||
<a href="javascript:void(0);" class="dropdownLogin px-0.5" [attr.aria-label]="'nav.login' |translate"
|
<button class="dropdownLogin btn btn-link px-0" [attr.aria-label]="'nav.login' |translate"
|
||||||
(click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly"
|
(click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly"
|
||||||
role="menuitem"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-controls="loginDropdownMenu"
|
aria-controls="loginDropdownMenu"
|
||||||
[attr.aria-expanded]="loginDrop.isOpen()"
|
[attr.aria-expanded]="loginDrop.isOpen()"
|
||||||
ngbDropdownToggle>{{ 'nav.login' | translate }}</a>
|
ngbDropdownToggle>
|
||||||
|
{{ 'nav.login' | translate }}
|
||||||
|
</button>
|
||||||
<div id="loginDropdownMenu" [ngClass]="{'ps-3 pe-3': (loading | async)}" ngbDropdownMenu
|
<div id="loginDropdownMenu" [ngClass]="{'ps-3 pe-3': (loading | async)}" ngbDropdownMenu
|
||||||
role="menu"
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
[attr.aria-label]="'nav.login' | translate">
|
[attr.aria-label]="'nav.login' | translate">
|
||||||
<ds-log-in
|
<ds-log-in
|
||||||
[isStandalonePage]="false"></ds-log-in>
|
[isStandalonePage]="false"></ds-log-in>
|
||||||
@@ -22,19 +27,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if ((isAuthenticated | async) && (showAuth | async)) {
|
@if (isAuthenticated && showAuth) {
|
||||||
<div class="nav-item">
|
<div class="nav-item">
|
||||||
<div ngbDropdown #loggedInDrop="ngbDropdown" display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
<div ngbDropdown #loggedInDrop="ngbDropdown" display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||||
<a href="javascript:void(0);"
|
<button
|
||||||
role="menuitem"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
[attr.aria-label]="'nav.user-profile-menu-and-logout' | translate"
|
[attr.aria-label]="'nav.user-profile-menu-and-logout' | translate"
|
||||||
aria-controls="user-menu-dropdown"
|
aria-controls="user-menu-dropdown"
|
||||||
(click)="$event.preventDefault()" [title]="'nav.user-profile-menu-and-logout' | translate"
|
(click)="$event.preventDefault()" [title]="'nav.user-profile-menu-and-logout' | translate"
|
||||||
class="dropdownLogout px-1"
|
class="dropdownLogout btn btn-link px-0"
|
||||||
[attr.data-test]="'user-menu' | dsBrowserOnly"
|
[attr.data-test]="'user-menu' | dsBrowserOnly"
|
||||||
ngbDropdownToggle>
|
ngbDropdownToggle>
|
||||||
<i class="fas fa-user-circle fa-lg fa-fw"></i></a>
|
<i class="fas fa-user-circle fa-lg fa-fw"></i>
|
||||||
|
</button>
|
||||||
<div id="logoutDropdownMenu" ngbDropdownMenu>
|
<div id="logoutDropdownMenu" ngbDropdownMenu>
|
||||||
<ds-user-menu [inExpandableNavbar]="false" (changedRoute)="loggedInDrop.close()"></ds-user-menu>
|
<ds-user-menu [inExpandableNavbar]="false" (changedRoute)="loggedInDrop.close()"></ds-user-menu>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,20 +50,17 @@
|
|||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div data-test="auth-nav">
|
<div data-test="auth-nav">
|
||||||
@if ((isAuthenticated | async) !== true) {
|
@if (isAuthenticated === true) {
|
||||||
<a routerLink="/login" routerLinkActive="active" class="loginLink px-0.5" role="button" tabindex="0">
|
<a [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="logoutLink px-0" role="link" tabindex="0">
|
||||||
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
@if ((isAuthenticated | async)) {
|
|
||||||
<a role="button" [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="logoutLink px-1" role="button" tabindex="0">
|
|
||||||
<i class="fas fa-sign-out-alt fa-lg fa-fw"></i>
|
<i class="fas fa-sign-out-alt fa-lg fa-fw"></i>
|
||||||
<span class="sr-only">(current)</span>
|
<span class="sr-only">(current)</span>
|
||||||
</a>
|
</a>
|
||||||
|
} @else {
|
||||||
|
<a routerLink="/login" routerLinkActive="active" class="loginLink px-0" role="link" tabindex="0">
|
||||||
|
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
|
||||||
|
</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Do not use ul/li in this menu as it breaks e2e accessibility tests -->
|
<!-- Do not use ul/li in this menu as it breaks e2e accessibility tests -->
|
||||||
|
@@ -28,3 +28,7 @@
|
|||||||
box-shadow: unset;
|
box-shadow: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle::after {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
@@ -64,7 +64,7 @@ export class AuthNavMenuComponent implements OnInit {
|
|||||||
* Whether user is authenticated.
|
* Whether user is authenticated.
|
||||||
* @type {Observable<string>}
|
* @type {Observable<string>}
|
||||||
*/
|
*/
|
||||||
public isAuthenticated: Observable<boolean>;
|
public isAuthenticated$: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if the authentication is loading.
|
* True if the authentication is loading.
|
||||||
@@ -74,7 +74,7 @@ export class AuthNavMenuComponent implements OnInit {
|
|||||||
|
|
||||||
public isMobile$: Observable<boolean>;
|
public isMobile$: Observable<boolean>;
|
||||||
|
|
||||||
public showAuth = observableOf(false);
|
public showAuth$ = observableOf(false);
|
||||||
|
|
||||||
public user: Observable<EPerson>;
|
public user: Observable<EPerson>;
|
||||||
|
|
||||||
@@ -89,14 +89,14 @@ export class AuthNavMenuComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// set isAuthenticated
|
// set isAuthenticated
|
||||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
this.isAuthenticated$ = this.store.pipe(select(isAuthenticated));
|
||||||
|
|
||||||
// set loading
|
// set loading
|
||||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||||
|
|
||||||
this.user = this.authService.getAuthenticatedUserFromStore();
|
this.user = this.authService.getAuthenticatedUserFromStore();
|
||||||
|
|
||||||
this.showAuth = this.store.pipe(
|
this.showAuth$ = this.store.pipe(
|
||||||
select(routerStateSelector),
|
select(routerStateSelector),
|
||||||
filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state)),
|
filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state)),
|
||||||
map((router: RouterReducerState) => (!router.state.url.startsWith(LOGIN_ROUTE)
|
map((router: RouterReducerState) => (!router.state.url.startsWith(LOGIN_ROUTE)
|
||||||
|
@@ -58,6 +58,7 @@ import { BrowseByComponent } from './browse-by.component';
|
|||||||
@Component({
|
@Component({
|
||||||
// eslint-disable-next-line @angular-eslint/component-selector
|
// eslint-disable-next-line @angular-eslint/component-selector
|
||||||
selector: 'ds-browse-entry-list-element',
|
selector: 'ds-browse-entry-list-element',
|
||||||
|
standalone: true,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
class MockThemedBrowseEntryListElementComponent {
|
class MockThemedBrowseEntryListElementComponent {
|
||||||
|
@@ -23,6 +23,7 @@ import { ComcolBrowseByComponent } from './comcol-browse-by.component';
|
|||||||
@Component({
|
@Component({
|
||||||
// eslint-disable-next-line @angular-eslint/component-selector
|
// eslint-disable-next-line @angular-eslint/component-selector
|
||||||
selector: '',
|
selector: '',
|
||||||
|
standalone: true,
|
||||||
template: '<span id="ComcolBrowseByComponent"></span>',
|
template: '<span id="ComcolBrowseByComponent"></span>',
|
||||||
})
|
})
|
||||||
class BrowseByTestComponent {
|
class BrowseByTestComponent {
|
||||||
|
@@ -6,6 +6,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { RestResponse } from '../../core/cache/response.models';
|
import { RestResponse } from '../../core/cache/response.models';
|
||||||
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||||
@@ -67,6 +68,7 @@ describe('BrowserOrejimeService', () => {
|
|||||||
authService = jasmine.createSpyObj('authService', {
|
authService = jasmine.createSpyObj('authService', {
|
||||||
isAuthenticated: observableOf(true),
|
isAuthenticated: observableOf(true),
|
||||||
getAuthenticatedUserFromStore: observableOf(user),
|
getAuthenticatedUserFromStore: observableOf(user),
|
||||||
|
getAuthenticatedUserIdFromStore: observableOf(user.id),
|
||||||
});
|
});
|
||||||
configurationDataService = createConfigSuccessSpy(recaptchaValue);
|
configurationDataService = createConfigSuccessSpy(recaptchaValue);
|
||||||
findByPropertyName = configurationDataService.findByPropertyName;
|
findByPropertyName = configurationDataService.findByPropertyName;
|
||||||
@@ -77,6 +79,8 @@ describe('BrowserOrejimeService', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
environment.info.enableCookieConsentPopup = true;
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
BrowserOrejimeService,
|
BrowserOrejimeService,
|
||||||
@@ -136,6 +140,7 @@ describe('BrowserOrejimeService', () => {
|
|||||||
|
|
||||||
describe('initialize with user', () => {
|
describe('initialize with user', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn((service as any), 'getUserId$').and.returnValue(observableOf(user.uuid));
|
||||||
spyOn((service as any), 'getUser$').and.returnValue(observableOf(user));
|
spyOn((service as any), 'getUser$').and.returnValue(observableOf(user));
|
||||||
translateService.get.and.returnValue(observableOf('loading...'));
|
translateService.get.and.returnValue(observableOf('loading...'));
|
||||||
spyOn(service, 'addAppMessages');
|
spyOn(service, 'addAppMessages');
|
||||||
@@ -152,6 +157,7 @@ describe('BrowserOrejimeService', () => {
|
|||||||
|
|
||||||
describe('to not call the initialize user method, but the other methods', () => {
|
describe('to not call the initialize user method, but the other methods', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn((service as any), 'getUserId$').and.returnValue(observableOf(undefined));
|
||||||
spyOn((service as any), 'getUser$').and.returnValue(observableOf(undefined));
|
spyOn((service as any), 'getUser$').and.returnValue(observableOf(undefined));
|
||||||
translateService.get.and.returnValue(observableOf('loading...'));
|
translateService.get.and.returnValue(observableOf('loading...'));
|
||||||
spyOn(service, 'addAppMessages');
|
spyOn(service, 'addAppMessages');
|
||||||
@@ -203,22 +209,22 @@ describe('BrowserOrejimeService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getUser$ when there is no one authenticated', () => {
|
describe('getUserId$ when there is no one authenticated', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(service as any).authService.isAuthenticated.and.returnValue(observableOf(false));
|
(service as any).authService.isAuthenticated.and.returnValue(observableOf(false));
|
||||||
});
|
});
|
||||||
it('should return undefined', () => {
|
it('should return undefined', () => {
|
||||||
getTestScheduler().expectObservable((service as any).getUser$()).toBe('(a|)', { a: undefined });
|
getTestScheduler().expectObservable((service as any).getUserId$()).toBe('(a|)', { a: undefined });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getUser$ when there someone is authenticated', () => {
|
describe('getUserId$ when there someone is authenticated', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(service as any).authService.isAuthenticated.and.returnValue(observableOf(true));
|
(service as any).authService.isAuthenticated.and.returnValue(observableOf(true));
|
||||||
(service as any).authService.getAuthenticatedUserFromStore.and.returnValue(observableOf(user));
|
(service as any).authService.getAuthenticatedUserIdFromStore.and.returnValue(observableOf(user.id));
|
||||||
});
|
});
|
||||||
it('should return the user', () => {
|
it('should return the user id', () => {
|
||||||
getTestScheduler().expectObservable((service as any).getUser$()).toBe('(a|)', { a: user });
|
getTestScheduler().expectObservable((service as any).getUserId$()).toBe('(a|)', { a: user.id });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -243,7 +249,7 @@ describe('BrowserOrejimeService', () => {
|
|||||||
|
|
||||||
describe('when no user is autheticated', () => {
|
describe('when no user is autheticated', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(service as any, 'getUser$').and.returnValue(observableOf(undefined));
|
spyOn(service as any, 'getUserId$').and.returnValue(observableOf(undefined));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the cookie consents object', () => {
|
it('should return the cookie consents object', () => {
|
||||||
@@ -256,7 +262,7 @@ describe('BrowserOrejimeService', () => {
|
|||||||
|
|
||||||
describe('when user is autheticated', () => {
|
describe('when user is autheticated', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(service as any, 'getUser$').and.returnValue(observableOf(user));
|
spyOn(service as any, 'getUserId$').and.returnValue(observableOf(user.uuid));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the cookie consents object', () => {
|
it('should return the cookie consents object', () => {
|
||||||
@@ -316,7 +322,7 @@ describe('BrowserOrejimeService', () => {
|
|||||||
GOOGLE_ANALYTICS_KEY = clone((service as any).GOOGLE_ANALYTICS_KEY);
|
GOOGLE_ANALYTICS_KEY = clone((service as any).GOOGLE_ANALYTICS_KEY);
|
||||||
REGISTRATION_VERIFICATION_ENABLED_KEY = clone((service as any).REGISTRATION_VERIFICATION_ENABLED_KEY);
|
REGISTRATION_VERIFICATION_ENABLED_KEY = clone((service as any).REGISTRATION_VERIFICATION_ENABLED_KEY);
|
||||||
MATOMO_ENABLED = clone((service as any).MATOMO_ENABLED);
|
MATOMO_ENABLED = clone((service as any).MATOMO_ENABLED);
|
||||||
spyOn((service as any), 'getUser$').and.returnValue(observableOf(user));
|
spyOn((service as any), 'getUserId$').and.returnValue(observableOf(user.uuid));
|
||||||
translateService.get.and.returnValue(observableOf('loading...'));
|
translateService.get.and.returnValue(observableOf('loading...'));
|
||||||
spyOn(service, 'addAppMessages');
|
spyOn(service, 'addAppMessages');
|
||||||
spyOn((service as any), 'initializeUser');
|
spyOn((service as any), 'initializeUser');
|
||||||
|
@@ -191,11 +191,13 @@ export class BrowserOrejimeService extends OrejimeService {
|
|||||||
*/
|
*/
|
||||||
this.translateConfiguration();
|
this.translateConfiguration();
|
||||||
|
|
||||||
|
if (!environment.info?.enableCookieConsentPopup) {
|
||||||
|
this.orejimeConfig.apps = [];
|
||||||
|
} else {
|
||||||
this.orejimeConfig.apps = this.filterConfigApps(appsToHide);
|
this.orejimeConfig.apps = this.filterConfigApps(appsToHide);
|
||||||
|
}
|
||||||
this.applyUpdateSettingsCallbackToApps(user);
|
this.applyUpdateSettingsCallbackToApps(user);
|
||||||
|
this.lazyOrejime.then(({ init }) => {
|
||||||
void this.lazyOrejime.then(({ init }) => {
|
|
||||||
this.orejimeInstance = init(this.orejimeConfig);
|
this.orejimeInstance = init(this.orejimeConfig);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -229,13 +231,13 @@ export class BrowserOrejimeService extends OrejimeService {
|
|||||||
* Return saved preferences stored in the orejime cookie
|
* Return saved preferences stored in the orejime cookie
|
||||||
*/
|
*/
|
||||||
getSavedPreferences(): Observable<any> {
|
getSavedPreferences(): Observable<any> {
|
||||||
return this.getUser$().pipe(
|
return this.getUserId$().pipe(
|
||||||
map((user: EPerson) => {
|
map((userId: string) => {
|
||||||
let storageName;
|
let storageName;
|
||||||
if (isEmpty(user)) {
|
if (isEmpty(userId)) {
|
||||||
storageName = ANONYMOUS_STORAGE_NAME_OREJIME;
|
storageName = ANONYMOUS_STORAGE_NAME_OREJIME;
|
||||||
} else {
|
} else {
|
||||||
storageName = this.getStorageName(user.uuid);
|
storageName = this.getStorageName(userId);
|
||||||
}
|
}
|
||||||
return this.cookieService.get(storageName);
|
return this.cookieService.get(storageName);
|
||||||
}),
|
}),
|
||||||
@@ -258,6 +260,24 @@ export class BrowserOrejimeService extends OrejimeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the currently logged in user id
|
||||||
|
* Returns undefined when no one is logged in
|
||||||
|
*/
|
||||||
|
private getUserId$() {
|
||||||
|
return this.authService.isAuthenticated()
|
||||||
|
.pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((loggedIn: boolean) => {
|
||||||
|
if (loggedIn) {
|
||||||
|
return this.authService.getAuthenticatedUserIdFromStore();
|
||||||
|
}
|
||||||
|
return observableOf(undefined);
|
||||||
|
}),
|
||||||
|
take(1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the currently logged in user
|
* Retrieves the currently logged in user
|
||||||
* Returns undefined when no one is logged in
|
* Returns undefined when no one is logged in
|
||||||
|
@@ -24,6 +24,10 @@ export const MATOMO_OREJIME_KEY = 'matomo';
|
|||||||
|
|
||||||
export const MATOMO_COOKIE = 'dsMatomo';
|
export const MATOMO_COOKIE = 'dsMatomo';
|
||||||
|
|
||||||
|
export const CORRELATION_ID_OREJIME_KEY = 'correlation-id';
|
||||||
|
|
||||||
|
export const CORRELATION_ID_COOKIE = 'CORRELATION-ID';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Orejime configuration
|
* Orejime configuration
|
||||||
* For more information see https://github.com/empreinte-digitale/orejime
|
* For more information see https://github.com/empreinte-digitale/orejime
|
||||||
@@ -142,6 +146,17 @@ export function getOrejimeConfiguration(_window: NativeWindowRef): any {
|
|||||||
HAS_AGREED_END_USER,
|
HAS_AGREED_END_USER,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: CORRELATION_ID_OREJIME_KEY,
|
||||||
|
purposes: ['statistical'],
|
||||||
|
required: false,
|
||||||
|
cookies: [
|
||||||
|
CORRELATION_ID_COOKIE,
|
||||||
|
],
|
||||||
|
callback: () => {
|
||||||
|
_window?.nativeWindow.initCorrelationId();
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: MATOMO_OREJIME_KEY,
|
name: MATOMO_OREJIME_KEY,
|
||||||
purposes: ['statistical'],
|
purposes: ['statistical'],
|
||||||
|
@@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
|
|||||||
/**
|
/**
|
||||||
* Abstract class representing a service for handling Orejime consent preferences and UI
|
* Abstract class representing a service for handling Orejime consent preferences and UI
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable({ providedIn: 'root' })
|
||||||
export abstract class OrejimeService {
|
export abstract class OrejimeService {
|
||||||
/**
|
/**
|
||||||
* Initializes the service
|
* Initializes the service
|
||||||
|
39
src/app/shared/cookies/server-orejime.service.ts
Normal file
39
src/app/shared/cookies/server-orejime.service.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { OrejimeService } from './orejime.service';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server implementation for the OrejimeService, representing a service for handling Orejime consent preferences and UI
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ServerOrejimeService extends OrejimeService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the service:
|
||||||
|
* - Retrieves the current authenticated user
|
||||||
|
* - Checks if the translation service is ready
|
||||||
|
* - Initialize configuration for users
|
||||||
|
* - Add and translate orejime configuration messages
|
||||||
|
*/
|
||||||
|
initialize() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return saved preferences stored in the orejime cookie
|
||||||
|
*/
|
||||||
|
getSavedPreferences(): Observable<any> {
|
||||||
|
return of({});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the cookie consent form
|
||||||
|
*/
|
||||||
|
showSettings() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -11,9 +11,13 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { BtnDisabledDirective } from './btn-disabled.directive';
|
import { BtnDisabledDirective } from './btn-disabled.directive';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
standalone: true,
|
||||||
template: `
|
template: `
|
||||||
<button [dsBtnDisabled]="isDisabled">Test Button</button>
|
<button [dsBtnDisabled]="isDisabled">Test Button</button>
|
||||||
`,
|
`,
|
||||||
|
imports: [
|
||||||
|
BtnDisabledDirective,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
class TestComponent {
|
class TestComponent {
|
||||||
isDisabled = false;
|
isDisabled = false;
|
||||||
@@ -26,8 +30,7 @@ describe('DisabledDirective', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [BtnDisabledDirective],
|
imports: [BtnDisabledDirective, TestComponent],
|
||||||
declarations: [TestComponent],
|
|
||||||
});
|
});
|
||||||
fixture = TestBed.createComponent(TestComponent);
|
fixture = TestBed.createComponent(TestComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
@@ -34,6 +34,7 @@ export enum SelectorActionType {
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-dso-selector-modal',
|
selector: 'ds-dso-selector-modal',
|
||||||
|
standalone: true,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
export abstract class DSOSelectorModalWrapperComponent implements OnInit {
|
export abstract class DSOSelectorModalWrapperComponent implements OnInit {
|
||||||
|
@@ -34,7 +34,7 @@ import { lazyDataService } from '../../core/lazy-data-service';
|
|||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import {
|
import {
|
||||||
getFirstCompletedRemoteData,
|
getAllCompletedRemoteData,
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
} from '../../core/shared/operators';
|
} from '../../core/shared/operators';
|
||||||
import { ResourceType } from '../../core/shared/resource-type';
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
@@ -177,7 +177,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy {
|
|||||||
(this.dataService as EPersonDataService).searchByScope(scope, query, options) :
|
(this.dataService as EPersonDataService).searchByScope(scope, query, options) :
|
||||||
(this.dataService as GroupDataService).searchGroups(query, options);
|
(this.dataService as GroupDataService).searchGroups(query, options);
|
||||||
}),
|
}),
|
||||||
getFirstCompletedRemoteData(),
|
getAllCompletedRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -5,17 +5,29 @@
|
|||||||
[queryParams]="(bitstreamPath$| async)?.queryParams"
|
[queryParams]="(bitstreamPath$| async)?.queryParams"
|
||||||
[target]="isBlank ? '_blank': '_self'"
|
[target]="isBlank ? '_blank': '_self'"
|
||||||
[ngClass]="cssClasses"
|
[ngClass]="cssClasses"
|
||||||
[attr.aria-label]="('file-download-link.download' | translate) + dsoNameService.getName(bitstream)"
|
[attr.aria-label]="getDownloadLinkTitle(canDownload$ | async, canDownloadWithToken$ | async, dsoNameService.getName(bitstream))"
|
||||||
|
[title]="getDownloadLinkTitle(canDownload$ | async, canDownloadWithToken$ | async, dsoNameService.getName(bitstream))"
|
||||||
role="link"
|
role="link"
|
||||||
tabindex="0">
|
tabindex="0">
|
||||||
@if ((canDownload$ | async) === false && (canDownloadWithToken$ | async) === false) {
|
@if ((canDownload$ | async) === false && (canDownloadWithToken$ | async) === false) {
|
||||||
<!-- If the user cannot download the file by auth or token, show a lock icon -->
|
<!-- If the user cannot download the file by auth or token, show a lock icon -->
|
||||||
<span role="img" [attr.aria-label]="'file-download-link.restricted' | translate" class="pr-1"><i class="fas fa-lock"></i></span>
|
<span role="img"
|
||||||
|
[attr.aria-label]="'file-download-link.restricted' | translate"
|
||||||
|
[title]="'file-download-link.restricted' | translate"
|
||||||
|
class="pr-1">
|
||||||
|
<i class="fas fa-lock"></i>
|
||||||
|
</span>
|
||||||
} @else if ((canDownloadWithToken$ | async) && (canDownload$ | async) === false) {
|
} @else if ((canDownloadWithToken$ | async) && (canDownload$ | async) === false) {
|
||||||
<!-- If the user can download the file by token, and NOT normally show a lock open icon -->
|
<!-- If the user can download the file by token, and NOT normally show a lock open icon -->
|
||||||
<span role="img" [attr.aria-label]="'file-download-link.secure-access' | translate" class="pr-1 request-a-copy-access-icon"><i class="fa-solid fa-lock-open" style=""></i></span>
|
<span role="img"
|
||||||
|
[attr.aria-label]="'file-download-link.secure-access' | translate"
|
||||||
|
[title]="'file-download-link.secure-access' | translate"
|
||||||
|
class="pr-1 request-a-copy-access-icon">
|
||||||
|
<i class="fa-solid fa-lock-open"></i>
|
||||||
|
</span>
|
||||||
|
} @else if (showIcon) {
|
||||||
|
<i class="fas fa-download d-inline"></i>
|
||||||
}
|
}
|
||||||
<!-- Otherwise, show no icon (normal download by authorized user), public access etc. -->
|
|
||||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
.request-a-copy-access-icon {
|
.request-a-copy-access-icon {
|
||||||
color: var(--bs-success);
|
color: var(--bs-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-download{
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
@@ -12,7 +12,10 @@ import {
|
|||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import {
|
||||||
|
TranslateModule,
|
||||||
|
TranslateService,
|
||||||
|
} from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
@@ -75,6 +78,11 @@ export class FileDownloadLinkComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() showAccessStatusBadge = true;
|
@Input() showAccessStatusBadge = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean indicating whether the download icon should be displayed.
|
||||||
|
*/
|
||||||
|
@Input() showIcon = false;
|
||||||
|
|
||||||
itemRequest: ItemRequest;
|
itemRequest: ItemRequest;
|
||||||
|
|
||||||
bitstreamPath$: Observable<{
|
bitstreamPath$: Observable<{
|
||||||
@@ -90,6 +98,7 @@ export class FileDownloadLinkComponent implements OnInit {
|
|||||||
private authorizationService: AuthorizationDataService,
|
private authorizationService: AuthorizationDataService,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
private translateService: TranslateService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,4 +162,9 @@ export class FileDownloadLinkComponent implements OnInit {
|
|||||||
queryParams: {},
|
queryParams: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDownloadLinkTitle(canDownload: boolean,canDownloadWithToken: boolean, bitstreamName: string): string {
|
||||||
|
return (canDownload || canDownloadWithToken ? this.translateService.instant('file-download-link.download') :
|
||||||
|
this.translateService.instant('file-download-link.request-copy')) + bitstreamName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,8 @@ export class ThemedFileDownloadLinkComponent extends ThemedComponent<FileDownloa
|
|||||||
|
|
||||||
@Input() showAccessStatusBadge: boolean;
|
@Input() showAccessStatusBadge: boolean;
|
||||||
|
|
||||||
|
@Input() showIcon = false;
|
||||||
|
|
||||||
protected inAndOutputNames: (keyof FileDownloadLinkComponent & keyof this)[] = [
|
protected inAndOutputNames: (keyof FileDownloadLinkComponent & keyof this)[] = [
|
||||||
'bitstream',
|
'bitstream',
|
||||||
'item',
|
'item',
|
||||||
@@ -36,6 +38,7 @@ export class ThemedFileDownloadLinkComponent extends ThemedComponent<FileDownloa
|
|||||||
'isBlank',
|
'isBlank',
|
||||||
'enableRequestACopy',
|
'enableRequestACopy',
|
||||||
'showAccessStatusBadge',
|
'showAccessStatusBadge',
|
||||||
|
'showIcon',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected getComponentName(): string {
|
protected getComponentName(): string {
|
||||||
|
@@ -96,6 +96,7 @@ import {
|
|||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
} from '../../../../core/shared/operators';
|
} from '../../../../core/shared/operators';
|
||||||
import { SubmissionObject } from '../../../../core/submission/models/submission-object.model';
|
import { SubmissionObject } from '../../../../core/submission/models/submission-object.model';
|
||||||
|
import { SUBMISSION_LINKS_TO_FOLLOW } from '../../../../core/submission/resolver/submission-links-to-follow';
|
||||||
import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service';
|
import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service';
|
||||||
import { paginatedRelationsToItems } from '../../../../item-page/simple/item-types/shared/item-relationships-utils';
|
import { paginatedRelationsToItems } from '../../../../item-page/simple/item-types/shared/item-relationships-utils';
|
||||||
import { SubmissionService } from '../../../../submission/submission.service';
|
import { SubmissionService } from '../../../../submission/submission.service';
|
||||||
@@ -450,7 +451,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
|||||||
*/
|
*/
|
||||||
private setItem() {
|
private setItem() {
|
||||||
const submissionObject$ = this.submissionObjectService
|
const submissionObject$ = this.submissionObjectService
|
||||||
.findById(this.model.submissionId, true, true, followLink('item'), followLink('collection')).pipe(
|
.findById(this.model.submissionId, true, true, ...SUBMISSION_LINKS_TO_FOLLOW).pipe(
|
||||||
getAllSucceededRemoteData(),
|
getAllSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
);
|
);
|
||||||
|
@@ -28,6 +28,7 @@ import { DsDynamicInputModel } from './ds-dynamic-input.model';
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-dynamic-vocabulary',
|
selector: 'ds-dynamic-vocabulary',
|
||||||
|
standalone: true,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
export abstract class DsDynamicVocabularyComponent extends DynamicFormControlComponent {
|
export abstract class DsDynamicVocabularyComponent extends DynamicFormControlComponent {
|
||||||
|
@@ -1,25 +1,29 @@
|
|||||||
@if (moreThanOneLanguage) {
|
@if (moreThanOneLanguage) {
|
||||||
<div ngbDropdown class="navbar-nav" display="dynamic" placement="bottom-right">
|
<div ngbDropdown class="navbar-nav" display="dynamic" placement="bottom-right">
|
||||||
<a href="javascript:void(0);" role="menuitem"
|
<button role="button"
|
||||||
[attr.aria-label]="'nav.language' |translate"
|
[attr.aria-label]="'nav.language' |translate"
|
||||||
aria-controls="language-menu-list"
|
aria-controls="language-menu-list"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
|
class="dropdown-toggle btn btn-link px-0"
|
||||||
[title]="'nav.language' | translate"
|
[title]="'nav.language' | translate"
|
||||||
(click)="$event.preventDefault()" data-toggle="dropdown" ngbDropdownToggle
|
(click)="$event.preventDefault()" data-toggle="dropdown" ngbDropdownToggle
|
||||||
data-test="lang-switch"
|
data-test="lang-switch"
|
||||||
tabindex="0">
|
tabindex="0">
|
||||||
<i class="fas fa-globe-asia fa-lg fa-fw"></i>
|
<i class="fas fa-globe-asia fa-lg fa-fw"></i>
|
||||||
</a>
|
</button>
|
||||||
<ul ngbDropdownMenu class="dropdown-menu" [attr.aria-label]="'nav.language' |translate" id="language-menu-list" role="menu">
|
<div ngbDropdownMenu class="dropdown-menu" [attr.aria-label]="'nav.language' |translate" id="language-menu-list"
|
||||||
|
role="listbox">
|
||||||
@for (lang of translate.getLangs(); track lang) {
|
@for (lang of translate.getLangs(); track lang) {
|
||||||
<li class="dropdown-item" tabindex="0" #langSelect
|
<div class="dropdown-item" tabindex="0"
|
||||||
role="menuitem"
|
role="option"
|
||||||
|
[lang]="lang"
|
||||||
(keyup.enter)="useLang(lang)"
|
(keyup.enter)="useLang(lang)"
|
||||||
(click)="useLang(lang)"
|
(click)="useLang(lang)"
|
||||||
|
[attr.aria-selected]="lang === translate.currentLang"
|
||||||
[class.active]="lang === translate.currentLang">
|
[class.active]="lang === translate.currentLang">
|
||||||
{{ langLabel(lang) }}
|
{{ langLabel(lang) }}
|
||||||
</li>
|
</div>
|
||||||
}
|
}
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@@ -128,7 +128,7 @@ describe('LangSwitchComponent', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
it('should define the main A HREF in the UI', (() => {
|
it('should define the main A HREF in the UI', (() => {
|
||||||
expect(langSwitchElement.querySelector('a')).not.toBeNull();
|
expect(langSwitchElement.querySelector('button.dropdown-toggle')).not.toBeNull();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('when selecting a language', () => {
|
describe('when selecting a language', () => {
|
||||||
|
@@ -35,11 +35,11 @@
|
|||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
@if (canRegister$ | async) {
|
@if (canRegister$ | async) {
|
||||||
<a class="dropdown-item" [routerLink]="[getRegisterRoute()]"
|
<a class="dropdown-item" [routerLink]="[getRegisterRoute()]"
|
||||||
[attr.data-test]="'register' | dsBrowserOnly" role="menuitem" tabindex="0">{{"login.form.new-user" | translate}}</a>
|
[attr.data-test]="'register' | dsBrowserOnly" tabindex="0">{{"login.form.new-user" | translate}}</a>
|
||||||
}
|
}
|
||||||
@if (canForgot$ | async) {
|
@if (canForgot$ | async) {
|
||||||
<a class="dropdown-item" [routerLink]="[getForgotRoute()]"
|
<a class="dropdown-item" [routerLink]="[getForgotRoute()]"
|
||||||
[attr.data-test]="'forgot' | dsBrowserOnly" role="menuitem" tabindex="0">{{"login.form.forgot-password" | translate}}</a>
|
[attr.data-test]="'forgot' | dsBrowserOnly" tabindex="0">{{"login.form.forgot-password" | translate}}</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,7 @@ import { ClaimedTaskActionsAbstractComponent } from './claimed-task-actions-abst
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-advanced-claimed-task-action-abstract',
|
selector: 'ds-advanced-claimed-task-action-abstract',
|
||||||
|
standalone: true,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
export abstract class AdvancedClaimedTaskActionsAbstractComponent extends ClaimedTaskActionsAbstractComponent implements OnInit {
|
export abstract class AdvancedClaimedTaskActionsAbstractComponent extends ClaimedTaskActionsAbstractComponent implements OnInit {
|
||||||
|
@@ -31,6 +31,7 @@ import { MyDSpaceReloadableActionsComponent } from '../../mydspace-reloadable-ac
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-claimed-task-action-abstract',
|
selector: 'ds-claimed-task-action-abstract',
|
||||||
|
standalone: true,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
export abstract class ClaimedTaskActionsAbstractComponent extends MyDSpaceReloadableActionsComponent<ClaimedTask, ClaimedTaskDataService> implements OnDestroy {
|
export abstract class ClaimedTaskActionsAbstractComponent extends MyDSpaceReloadableActionsComponent<ClaimedTask, ClaimedTaskDataService> implements OnDestroy {
|
||||||
|
@@ -37,6 +37,7 @@ export interface MyDSpaceActionsResult {
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-mydspace-actions-abstract',
|
selector: 'ds-mydspace-actions-abstract',
|
||||||
|
standalone: true,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
export abstract class MyDSpaceActionsComponent<T extends DSpaceObject, TService extends IdentifiableDataService<T>> {
|
export abstract class MyDSpaceActionsComponent<T extends DSpaceObject, TService extends IdentifiableDataService<T>> {
|
||||||
|
@@ -34,6 +34,7 @@ import { MyDSpaceActionsComponent } from './mydspace-actions';
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-mydspace-reloadable-actions',
|
selector: 'ds-mydspace-reloadable-actions',
|
||||||
|
standalone: true,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
export abstract class MyDSpaceReloadableActionsComponent<T extends DSpaceObject, TService extends IdentifiableDataService<T>>
|
export abstract class MyDSpaceReloadableActionsComponent<T extends DSpaceObject, TService extends IdentifiableDataService<T>>
|
||||||
|
@@ -111,7 +111,7 @@ const ePersonMock: EPerson = Object.assign(new EPerson(), {
|
|||||||
uuid: '0a53a0f2-e168-4ed9-b4af-cba9a2d267ca',
|
uuid: '0a53a0f2-e168-4ed9-b4af-cba9a2d267ca',
|
||||||
language: null,
|
language: null,
|
||||||
value:
|
value:
|
||||||
'{"authentication":true,"preferences":true,"acknowledgement":true,"google-analytics":true}',
|
'{"authentication":true,"preferences":true,"acknowledgement":true,"google-analytics":true,"correlation-id":true}',
|
||||||
place: 0,
|
place: 0,
|
||||||
authority: null,
|
authority: null,
|
||||||
confidence: -1,
|
confidence: -1,
|
||||||
|
@@ -1,5 +1,10 @@
|
|||||||
@if (showAccessStatus) {
|
@if (showAccessStatus) {
|
||||||
@if ({ status: accessStatus$ | async, date: embargoDate$ | async }; as accessStatus) {
|
@if ((accessStatus$ | async); as status) {
|
||||||
<span [class]="'badge bg-secondary access-status-list-element-badge ' + accessStatusClass">{{ accessStatus.status | translate: {date: accessStatus.date} }}</span>
|
@let date = embargoDate$ | async;
|
||||||
|
<span [class]="'badge bg-secondary dont-break-out access-status-list-element-badge ' + accessStatusClass">
|
||||||
|
<span class="sr-only">{{ 'listelement.badge.access-status' | translate }}</span>
|
||||||
|
{{ status | translate: { date: date } }}
|
||||||
|
<span class="sr-only">, </span>
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1 +1,3 @@
|
|||||||
|
span{
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
@@ -19,7 +19,7 @@ import { TruncatePipe } from '../../../../utils/truncate.pipe';
|
|||||||
import { AccessStatusObject } from './access-status.model';
|
import { AccessStatusObject } from './access-status.model';
|
||||||
import { AccessStatusBadgeComponent } from './access-status-badge.component';
|
import { AccessStatusBadgeComponent } from './access-status-badge.component';
|
||||||
|
|
||||||
describe('ItemAccessStatusBadgeComponent', () => {
|
describe('AccessStatusBadgeComponent', () => {
|
||||||
let component: AccessStatusBadgeComponent;
|
let component: AccessStatusBadgeComponent;
|
||||||
let fixture: ComponentFixture<AccessStatusBadgeComponent>;
|
let fixture: ComponentFixture<AccessStatusBadgeComponent>;
|
||||||
|
|
||||||
@@ -100,17 +100,17 @@ describe('ItemAccessStatusBadgeComponent', () => {
|
|||||||
|
|
||||||
function lookForAccessStatusBadgeForItem(status: string) {
|
function lookForAccessStatusBadgeForItem(status: string) {
|
||||||
const badge = fixture.debugElement.query(By.css('span.badge'));
|
const badge = fixture.debugElement.query(By.css('span.badge'));
|
||||||
expect(badge.nativeElement.textContent).toEqual(`access-status.${status.toLowerCase()}.listelement.badge`);
|
expect(badge.nativeElement.textContent).toContain(`access-status.${status.toLowerCase()}.listelement.badge`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function lookForAccessStatusBadgeForBitstream() {
|
function lookForAccessStatusBadgeForBitstream() {
|
||||||
const badge = fixture.debugElement.query(By.css('span.badge'));
|
const badge = fixture.debugElement.query(By.css('span.badge'));
|
||||||
expect(badge.nativeElement.textContent).toEqual(`embargo.listelement.badge`);
|
expect(badge.nativeElement.textContent).toContain('embargo.listelement.badge');
|
||||||
}
|
}
|
||||||
|
|
||||||
function lookForNoAccessStatusBadgeForBitstream() {
|
function lookForNoAccessStatusBadgeForBitstream() {
|
||||||
const badge = fixture.debugElement.query(By.css('span.badge'));
|
const badge = fixture.debugElement.query(By.css('span.badge'));
|
||||||
expect(badge.nativeElement.textContent).toEqual(``);
|
expect(badge).toBeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('init with item', () => {
|
describe('init with item', () => {
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span [className]="badgeClass">
|
<span [className]="badgeClass">
|
||||||
|
<span class="sr-only">{{ 'mydspace.status' | translate }}</span>
|
||||||
{{('mydspace.status.' + badgeContent) | translate}}
|
{{('mydspace.status.' + badgeContent) | translate}}
|
||||||
|
<span class="sr-only">, </span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,10 +1,16 @@
|
|||||||
@if (privateBadge) {
|
@if (privateBadge) {
|
||||||
<div class="private-badge">
|
<div class="private-badge">
|
||||||
<span class="badge bg-danger">{{ "item.badge.private" | translate }}</span>
|
<span class="badge bg-danger">
|
||||||
|
<span class="sr-only">{{ 'item.badge.status' | translate }}</span>
|
||||||
|
{{ "item.badge.private" | translate }}
|
||||||
|
<span class="sr-only">, </span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (withdrawnBadge) {
|
@if (withdrawnBadge) {
|
||||||
<div class="withdrawn-badge">
|
<div class="withdrawn-badge">
|
||||||
|
<span class="sr-only">{{ 'item.badge.status' | translate }}</span>
|
||||||
<span class="badge bg-warning">{{ "item.badge.withdrawn" | translate }}</span>
|
<span class="badge bg-warning">{{ "item.badge.withdrawn" | translate }}</span>
|
||||||
|
<span class="sr-only">, </span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
@if (typeMessage) {
|
@if (typeMessage) {
|
||||||
<span>
|
<span>
|
||||||
|
<span class="sr-only">{{ 'listelement.badge.dso-type' | translate }}</span>
|
||||||
<span class="badge bg-info">{{ typeMessage | translate }}</span>
|
<span class="badge bg-info">{{ typeMessage | translate }}</span>
|
||||||
|
<span class="sr-only">, </span>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
@@ -35,13 +35,15 @@
|
|||||||
[label]="('item.page.date' | translate)"
|
[label]="('item.page.date' | translate)"
|
||||||
[metadata]="'dc.date.issued'"
|
[metadata]="'dc.date.issued'"
|
||||||
[separator]="separator"
|
[separator]="separator"
|
||||||
[placeholder]="('mydspace.results.no-date' | translate)"></ds-item-detail-preview-field>
|
[placeholder]="('mydspace.results.no-date' | translate)">
|
||||||
|
</ds-item-detail-preview-field>
|
||||||
<ds-item-detail-preview-field [item]="item"
|
<ds-item-detail-preview-field [item]="item"
|
||||||
[object]="object"
|
[object]="object"
|
||||||
[label]="('item.page.author' | translate)"
|
[label]="('item.page.authors' | translate)"
|
||||||
[metadata]="['dc.contributor', 'dc.creator', 'dc.contributor.*']"
|
[metadata]="['dc.contributor', 'dc.creator', 'dc.contributor.*']"
|
||||||
[separator]="separator"
|
[separator]="separator"
|
||||||
[placeholder]="('mydspace.results.no-authors' | translate)"></ds-item-detail-preview-field>
|
[placeholder]="('mydspace.results.no-authors' | translate)">
|
||||||
|
</ds-item-detail-preview-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-6">
|
<div class="col-xs-12 col-md-6">
|
||||||
<ds-item-detail-preview-field [item]="item"
|
<ds-item-detail-preview-field [item]="item"
|
||||||
@@ -49,13 +51,15 @@
|
|||||||
[label]="('item.page.abstract' | translate)"
|
[label]="('item.page.abstract' | translate)"
|
||||||
[metadata]="'dc.description.abstract'"
|
[metadata]="'dc.description.abstract'"
|
||||||
[separator]="separator"
|
[separator]="separator"
|
||||||
[placeholder]="('mydspace.results.no-abstract' | translate)"></ds-item-detail-preview-field>
|
[placeholder]="('mydspace.results.no-abstract' | translate)">
|
||||||
|
</ds-item-detail-preview-field>
|
||||||
<ds-item-detail-preview-field [item]="item"
|
<ds-item-detail-preview-field [item]="item"
|
||||||
[object]="object"
|
[object]="object"
|
||||||
[label]="('item.page.uri' | translate)"
|
[label]="('item.page.uri' | translate)"
|
||||||
[metadata]="'dc.identifier.uri'"
|
[metadata]="'dc.identifier.uri'"
|
||||||
[separator]="separator"
|
[separator]="separator"
|
||||||
[placeholder]="('mydspace.results.no-uri' | translate)"></ds-item-detail-preview-field>
|
[placeholder]="('mydspace.results.no-uri' | translate)">
|
||||||
|
</ds-item-detail-preview-field>
|
||||||
<div>
|
<div>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
@if (linkType !== linkTypes.None) {
|
@if (linkType !== linkTypes.None) {
|
||||||
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="['/collections/', dso.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate">
|
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="['/collections/', dso.id]" class="card-img-top" tabindex="-1" [attr.title]="'search.results.view-result' | translate">
|
||||||
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
@if (linkType !== linkTypes.None) {
|
@if (linkType !== linkTypes.None) {
|
||||||
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="['/communities/', dso.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate">
|
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="['/communities/', dso.id]" class="card-img-top" tabindex="-1" [attr.title]="'search.results.view-result' | translate">
|
||||||
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@if (linkType !== linkTypes.None) {
|
@if (linkType !== linkTypes.None) {
|
||||||
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
|
||||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
|
class="card-img-top full-width" tabindex="-1" [attr.title]="'search.results.view-result' | translate">
|
||||||
<div>
|
<div>
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<div class="col-3 col-md-2">
|
<div class="col-3 col-md-2">
|
||||||
@if (linkType !== linkTypes.None) {
|
@if (linkType !== linkTypes.None) {
|
||||||
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
|
||||||
[routerLink]="[itemPageRoute]" class="dont-break-out" role="button" tabindex="0">
|
[routerLink]="[itemPageRoute]" class="dont-break-out" tabindex="-1">
|
||||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
||||||
</ds-thumbnail>
|
</ds-thumbnail>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -30,6 +30,7 @@ import { ObjectSelectService } from '../object-select.service';
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-object-select-abstract',
|
selector: 'ds-object-select-abstract',
|
||||||
|
standalone: true,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
export abstract class ObjectSelectComponent<TDomain extends DSpaceObject> implements OnInit, OnDestroy {
|
export abstract class ObjectSelectComponent<TDomain extends DSpaceObject> implements OnInit, OnDestroy {
|
||||||
|
@@ -23,6 +23,7 @@ import { StartsWithType } from './starts-with-type';
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-start-with-abstract',
|
selector: 'ds-start-with-abstract',
|
||||||
|
standalone: true,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
export abstract class StartsWithAbstractComponent implements OnInit, OnDestroy {
|
export abstract class StartsWithAbstractComponent implements OnInit, OnDestroy {
|
||||||
|
@@ -3,6 +3,7 @@ import { Component } from '@angular/core';
|
|||||||
// noinspection AngularMissingOrInvalidDeclarationInModule
|
// noinspection AngularMissingOrInvalidDeclarationInModule
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-test-component',
|
selector: 'ds-test-component',
|
||||||
|
standalone: true,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
export class TestComponent {
|
export class TestComponent {
|
||||||
|
@@ -3,6 +3,7 @@ import { Component } from '@angular/core';
|
|||||||
// noinspection AngularMissingOrInvalidDeclarationInModule
|
// noinspection AngularMissingOrInvalidDeclarationInModule
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-test-component',
|
selector: 'ds-test-component',
|
||||||
|
standalone: true,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
export class TestComponent {
|
export class TestComponent {
|
||||||
|
@@ -37,6 +37,7 @@ import { ThemeService } from './theme.service';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-themed',
|
selector: 'ds-themed',
|
||||||
|
standalone: true,
|
||||||
styleUrls: ['./themed.component.scss'],
|
styleUrls: ['./themed.component.scss'],
|
||||||
templateUrl: './themed.component.html',
|
templateUrl: './themed.component.html',
|
||||||
})
|
})
|
||||||
|
@@ -22,6 +22,11 @@ export class UploaderOptions {
|
|||||||
*/
|
*/
|
||||||
maxFileNumber: number;
|
maxFileNumber: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Impersonating user uuid
|
||||||
|
*/
|
||||||
|
impersonatingID: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The request method to use for the file upload request
|
* The request method to use for the file upload request
|
||||||
*/
|
*/
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user