mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +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-output-native": "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
|
||||
# Tell Cypress to run e2e tests using the same UI URL
|
||||
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
|
||||
# Comment this out to use the latest release
|
||||
#CHROME_VERSION: "90.0.4430.212-1"
|
||||
@@ -268,6 +270,37 @@ jobs:
|
||||
echo "$result"
|
||||
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
|
||||
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
|
||||
# disabled (false) by default to boost server performance at the expense of loading smoothness.
|
||||
inlineCriticalCss: false
|
||||
# Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects.
|
||||
# NOTE: The "/handle/" path ensures Handle redirects work via SSR. The "/reload/" path ensures
|
||||
# hard refreshes (e.g. after login) trigger SSR while fully reloading the page.
|
||||
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/', '/reload/' ]
|
||||
# Patterns to be run as regexes against the path of the page to check if SSR is allowed.
|
||||
# If the path match any of the regexes it will be served directly in CSR.
|
||||
# By default, excludes community and collection browse, global browse, global search, community list, statistics and various administrative tools.
|
||||
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.
|
||||
# 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.
|
||||
@@ -453,6 +467,8 @@ info:
|
||||
enableEndUserAgreement: true
|
||||
enablePrivacyStatement: 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/)
|
||||
# display in supported metadata fields. By default, only dc.description.abstract is supported.
|
||||
|
@@ -15,24 +15,24 @@ describe('Header', () => {
|
||||
cy.visit('/');
|
||||
|
||||
// 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
|
||||
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"
|
||||
cy.get('html').invoke('attr', 'lang').should('eq', 'de');
|
||||
|
||||
// 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
|
||||
cy.get('a[data-test="lang-switch"]').click();
|
||||
cy.get('#language-menu-list li').contains('English').click();
|
||||
cy.get('button[data-test="lang-switch"]').click();
|
||||
cy.get('#language-menu-list div[role="option"]').contains('English').click();
|
||||
|
||||
// HTML "lang" attribute should switch to "en"
|
||||
cy.get('html').invoke('attr', 'lang').should('eq', 'en');
|
||||
|
||||
// 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
|
||||
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
|
||||
testA11y('ds-home-page');
|
||||
});
|
||||
|
@@ -54,12 +54,9 @@ before(() => {
|
||||
|
||||
// Runs once before the first test in each "block"
|
||||
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.
|
||||
const cookieContent = '{"authentication":true,"preferences":true,"acknowledgement":true,"google-analytics":true,"matomo":true,"google-recaptcha":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);
|
||||
cy.setCookie('orejime-anonymous', '{"authentication":true,"preferences":true,"acknowledgement":true,"google-analytics":true,"correlation-id":true,"accessibility":true}');
|
||||
|
||||
// Remove any CSRF cookies saved from prior tests
|
||||
cy.clearCookie(DSPACE_XSRF_COOKIE);
|
||||
|
30
package-lock.json
generated
30
package-lock.json
generated
@@ -42,7 +42,7 @@
|
||||
"colors": "^1.4.0",
|
||||
"compression": "^1.7.5",
|
||||
"cookie-parser": "1.4.7",
|
||||
"core-js": "^3.41.0",
|
||||
"core-js": "^3.42.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"date-fns-tz": "^1.3.7",
|
||||
"deepmerge": "^4.3.1",
|
||||
@@ -150,12 +150,12 @@
|
||||
"postcss-loader": "^4.0.3",
|
||||
"postcss-preset-env": "^7.4.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "~1.87.0",
|
||||
"sass": "~1.88.0",
|
||||
"sass-loader": "^12.6.0",
|
||||
"sass-resources-loader": "^2.2.5",
|
||||
"ts-node": "^8.10.2",
|
||||
"typescript": "~5.4.5",
|
||||
"webpack": "5.99.7",
|
||||
"webpack": "5.99.8",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1"
|
||||
}
|
||||
@@ -9108,9 +9108,9 @@
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.5",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz",
|
||||
"integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==",
|
||||
"version": "5.3.6",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
|
||||
"integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -10198,9 +10198,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.41.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.41.0.tgz",
|
||||
"integrity": "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==",
|
||||
"version": "3.42.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz",
|
||||
"integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -20677,9 +20677,9 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.87.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz",
|
||||
"integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==",
|
||||
"version": "1.88.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz",
|
||||
"integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -22772,9 +22772,9 @@
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.99.7",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz",
|
||||
"integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==",
|
||||
"version": "5.99.8",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
|
||||
"integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@@ -124,7 +124,7 @@
|
||||
"colors": "^1.4.0",
|
||||
"compression": "^1.7.5",
|
||||
"cookie-parser": "1.4.7",
|
||||
"core-js": "^3.41.0",
|
||||
"core-js": "^3.42.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"date-fns-tz": "^1.3.7",
|
||||
"deepmerge": "^4.3.1",
|
||||
@@ -232,12 +232,12 @@
|
||||
"postcss-loader": "^4.0.3",
|
||||
"postcss-preset-env": "^7.4.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "~1.87.0",
|
||||
"sass": "~1.88.0",
|
||||
"sass-loader": "^12.6.0",
|
||||
"sass-resources-loader": "^2.2.5",
|
||||
"ts-node": "^8.10.2",
|
||||
"typescript": "~5.4.5",
|
||||
"webpack": "5.99.7",
|
||||
"webpack": "5.99.8",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1"
|
||||
}
|
||||
|
18
server.ts
18
server.ts
@@ -58,6 +58,7 @@ import {
|
||||
REQUEST,
|
||||
RESPONSE,
|
||||
} from './src/express.tokens';
|
||||
import { SsrExcludePatterns } from "./src/config/ssr-config.interface";
|
||||
|
||||
/*
|
||||
* Set path for the browser application's dist folder
|
||||
@@ -221,7 +222,7 @@ export function app() {
|
||||
* The callback function to serve server side angular
|
||||
*/
|
||||
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)
|
||||
serverSideRender(req, res, next);
|
||||
} 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
|
||||
*/
|
||||
|
@@ -20,6 +20,7 @@ import { BrowseByPageComponent } from './browse-by-page.component';
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: '',
|
||||
standalone: true,
|
||||
template: '<span id="BrowseByTestComponent"></span>',
|
||||
})
|
||||
class BrowseByTestComponent {
|
||||
|
@@ -20,6 +20,7 @@ import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: '',
|
||||
standalone: true,
|
||||
template: '<span id="BrowseByTestComponent"></span>',
|
||||
})
|
||||
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,
|
||||
* or null if the user is not authenticated.
|
||||
|
@@ -10,12 +10,18 @@ import {
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import {
|
||||
appReducers,
|
||||
storeModuleConfig,
|
||||
} from '../../app.reducer';
|
||||
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 { RouterStub } from '../../shared/testing/router.stub';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
@@ -40,6 +46,8 @@ describe('LogInterceptor', () => {
|
||||
const mockStatusCode = 200;
|
||||
const mockStatusText = 'SUCCESS';
|
||||
|
||||
const mockOrejimeService = jasmine.createSpyObj('OrejimeService', ['getSavedPreferences']);
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -56,6 +64,7 @@ describe('LogInterceptor', () => {
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: CorrelationIdService, useClass: CorrelationIdService },
|
||||
{ provide: UUIDService, useClass: UUIDService },
|
||||
{ provide: OrejimeService, useValue: mockOrejimeService },
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
@@ -66,12 +75,14 @@ describe('LogInterceptor', () => {
|
||||
cookieService = TestBed.inject(CookieService);
|
||||
correlationIdService = TestBed.inject(CorrelationIdService);
|
||||
|
||||
cookieService.set('CORRELATION-ID','123455');
|
||||
correlationIdService.initCorrelationId();
|
||||
cookieService.set(CORRELATION_ID_COOKIE,'123455');
|
||||
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) => {
|
||||
expect(response).toBeTruthy();
|
||||
done();
|
||||
@@ -83,7 +94,23 @@ describe('LogInterceptor', () => {
|
||||
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) => {
|
||||
expect(response).toBeTruthy();
|
||||
done();
|
||||
|
@@ -7,9 +7,15 @@ import {
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
|
||||
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
|
||||
@@ -19,22 +25,37 @@ import { hasValue } from '../../shared/empty.util';
|
||||
@Injectable()
|
||||
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>> {
|
||||
|
||||
// Get the correlation id for the user from the store
|
||||
const correlationId = this.cidService.getCorrelationId();
|
||||
return this.orejimeService.getSavedPreferences().pipe(
|
||||
switchMap(preferences => {
|
||||
// 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
|
||||
let headers = request.headers;
|
||||
if (!correlationDeclined) {
|
||||
// Get the correlation id for the user from the store
|
||||
const correlationId = this.cidService.getCorrelationId();
|
||||
if (hasValue(correlationId)) {
|
||||
headers = headers.append('X-CORRELATION-ID', correlationId);
|
||||
}
|
||||
}
|
||||
headers = headers.append('X-REFERRER', this.router.url);
|
||||
|
||||
// Add new headers to the intercepted request
|
||||
request = request.clone({ withCredentials: true, headers: headers });
|
||||
return next.handle(request);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { of } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model';
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
SubmissionRequest,
|
||||
} from '../data/request.models';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { RequestEntry } from '../data/request-entry.model';
|
||||
import { SubmissionRestService } from './submission-rest.service';
|
||||
|
||||
describe('SubmissionRestService test suite', () => {
|
||||
@@ -38,7 +40,9 @@ describe('SubmissionRestService test suite', () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
requestService = getMockRequestService();
|
||||
requestService = getMockRequestService(of(Object.assign(new RequestEntry(), {
|
||||
request: new SubmissionRequest('mock-request-uuid', 'mock-request-href'),
|
||||
})));
|
||||
rdbService = getMockRemoteDataBuildService();
|
||||
scheduler = getTestScheduler();
|
||||
halService = new HALEndpointServiceStub(resourceEndpointURL);
|
||||
@@ -62,7 +66,7 @@ describe('SubmissionRestService test suite', () => {
|
||||
scheduler.schedule(() => service.getDataById(resourceEndpoint, resourceScope).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.send).toHaveBeenCalledWith(expected);
|
||||
expect(requestService.send).toHaveBeenCalledWith(expected, false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,15 +1,20 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
Observable,
|
||||
skipWhile,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
mergeMap,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
hasValue,
|
||||
hasValueOperator,
|
||||
isNotEmpty,
|
||||
} from '../../shared/empty.util';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
@@ -25,7 +30,6 @@ import {
|
||||
} from '../data/request.models';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { RequestError } from '../data/request-error.model';
|
||||
import { RestRequest } from '../data/rest-request.model';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||
@@ -33,6 +37,23 @@ import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-respon
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
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
|
||||
*/
|
||||
@@ -56,15 +77,7 @@ export class SubmissionRestService {
|
||||
*/
|
||||
protected fetchRequest(requestId: string): Observable<SubmitDataResponseDefinitionObject> {
|
||||
return this.rdbService.buildFromRequestUUID<SubmissionResponse>(requestId).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) ? response.payload.dataDefinition : response.payload;
|
||||
}
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
getFirstDataDefinition(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,21 +129,52 @@ export class SubmissionRestService {
|
||||
* The endpoint link name
|
||||
* @param id
|
||||
* 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>
|
||||
* server response
|
||||
*/
|
||||
public getDataById(linkName: string, id: string): Observable<SubmitDataResponseDefinitionObject> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
public getDataById(linkName: string, id: string, useCachedVersionIfAvailable = false): Observable<SubmitDataResponseDefinitionObject> {
|
||||
return this.halService.getEndpoint(linkName).pipe(
|
||||
map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, id)),
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
distinctUntilChanged(),
|
||||
map((endpointURL: string) => new SubmissionRequest(requestId, endpointURL)),
|
||||
tap((request: RestRequest) => {
|
||||
this.requestService.send(request);
|
||||
mergeMap((endpointURL: string) => {
|
||||
this.sendGetDataRequest(endpointURL, useCachedVersionIfAvailable);
|
||||
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,
|
||||
} from '@ngrx/store';
|
||||
import { MockStore } from '@ngrx/store/testing';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import {
|
||||
appReducers,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
storeModuleConfig,
|
||||
} from '../app.reducer';
|
||||
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 { SetCorrelationIdAction } from './correlation-id.actions';
|
||||
import { CorrelationIdService } from './correlation-id.service';
|
||||
@@ -34,7 +36,13 @@ describe('CorrelationIdService', () => {
|
||||
cookieService = new CookieServiceMock();
|
||||
uuidService = new UUIDService();
|
||||
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', () => {
|
||||
@@ -46,45 +54,45 @@ describe('CorrelationIdService', () => {
|
||||
});
|
||||
|
||||
|
||||
describe('initCorrelationId', () => {
|
||||
describe('setCorrelationId', () => {
|
||||
const cookieCID = 'cookie CID';
|
||||
const storeCID = 'store CID';
|
||||
|
||||
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(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', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
it('should set cookie value to store value if present', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
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));
|
||||
|
||||
service.initCorrelationId();
|
||||
service.setCorrelationId();
|
||||
|
||||
expect(cookieService.get('CORRELATION-ID')).toBe(cookieCID);
|
||||
expect(cookieService.get(CORRELATION_ID_COOKIE)).toBe(cookieCID);
|
||||
expect(service.getCorrelationId()).toBe(cookieCID);
|
||||
});
|
||||
});
|
||||
|
@@ -1,4 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
select,
|
||||
Store,
|
||||
@@ -7,8 +10,20 @@ import { take } from 'rxjs/operators';
|
||||
|
||||
import { AppState } from '../app.reducer';
|
||||
import { CookieService } from '../core/services/cookie.service';
|
||||
import {
|
||||
NativeWindowRef,
|
||||
NativeWindowService,
|
||||
} from '../core/services/window.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 { correlationIdSelector } from './correlation-id.selector';
|
||||
|
||||
@@ -24,15 +39,32 @@ export class CorrelationIdService {
|
||||
protected cookieService: CookieService,
|
||||
protected uuidService: UUIDService,
|
||||
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
|
||||
*/
|
||||
initCorrelationId(): void {
|
||||
setCorrelationId(): void {
|
||||
// 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 (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
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
(registryService.queryMetadataFields as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Failed'));
|
||||
|
@@ -43,6 +43,7 @@ import {
|
||||
SortDirection,
|
||||
SortOptions,
|
||||
} from '../../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||
import { RegistryService } from '../../../core/registry/registry.service';
|
||||
import {
|
||||
getAllSucceededRemoteData,
|
||||
@@ -153,7 +154,10 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
|
||||
/**
|
||||
* 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,
|
||||
@@ -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
|
||||
*/
|
||||
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(),
|
||||
switchMap((rd) => {
|
||||
if (rd.hasSucceeded) {
|
||||
@@ -263,9 +267,7 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
|
||||
* @param useCache Whether or not to use the cache
|
||||
*/
|
||||
search(query: string, page: number, useCache: boolean = true) {
|
||||
return this.registryService.queryMetadataFields(query,{
|
||||
elementsPerPage: this.pageOptions.elementsPerPage, sort: this.pageOptions.sort,
|
||||
currentPage: page }, useCache, false, followLink('schema'))
|
||||
return this.registryService.queryMetadataFields(query, Object.assign({}, this.pageOptions, { currentPage: page }), useCache, false, followLink('schema'))
|
||||
.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
metadataFieldsToString(),
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<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" role="link" tabindex="0">
|
||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
|
||||
<div>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<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" role="link" tabindex="0">
|
||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
|
||||
<div>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<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" role="link" tabindex="0">
|
||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
|
||||
<div>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
@if (linkType !== linkTypes.None) {
|
||||
<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>
|
||||
</a>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
@if (linkType !== linkTypes.None) {
|
||||
<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>
|
||||
</a>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div class="col-3 col-md-2">
|
||||
@if (linkType !== linkTypes.None) {
|
||||
<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>
|
||||
</a>
|
||||
|
@@ -36,13 +36,13 @@
|
||||
<ds-related-items
|
||||
[parentItem]="object"
|
||||
[relationType]="'isJournalVolumeOfIssue'"
|
||||
[label]="'relationships.isSingleVolumeOf' | translate">
|
||||
[label]="'item.page.journal-volume' | translate">
|
||||
</ds-related-items>
|
||||
<ds-related-items
|
||||
class="mb-1 mt-1"
|
||||
[parentItem]="object"
|
||||
[relationType]="'isPublicationOfJournalIssue'"
|
||||
[label]="'relationships.isPublicationOfJournalIssue' | translate">
|
||||
[label]="'item.page.articles' | translate">
|
||||
</ds-related-items>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['dc.description']"
|
||||
|
@@ -24,12 +24,12 @@
|
||||
<ds-related-items
|
||||
[parentItem]="object"
|
||||
[relationType]="'isJournalOfVolume'"
|
||||
[label]="'relationships.isSingleJournalOf' | translate">
|
||||
[label]="'item.page.journal' | translate">
|
||||
</ds-related-items>
|
||||
<ds-related-items
|
||||
[parentItem]="object"
|
||||
[relationType]="'isIssueOfJournalVolume'"
|
||||
[label]="'relationships.isIssueOf' | translate">
|
||||
[label]="'item.page.journal-issues' | translate">
|
||||
</ds-related-items>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['dc.description']"
|
||||
|
@@ -28,7 +28,7 @@
|
||||
<ds-related-items
|
||||
[parentItem]="object"
|
||||
[relationType]="'isVolumeOfJournal'"
|
||||
[label]="'relationships.isVolumeOf' | translate">
|
||||
[label]="'item.page.journal-volumes' | translate">
|
||||
</ds-related-items>
|
||||
<ds-generic-item-page-field class="item-page-fields" [item]="object"
|
||||
[fields]="['dc.description']"
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<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" role="link" tabindex="0">
|
||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
|
||||
<div>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<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" role="link" tabindex="0">
|
||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
|
||||
<div>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<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" role="link" tabindex="0">
|
||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" tabindex="-1">
|
||||
<div>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
@if (linkType !== linkTypes.None) {
|
||||
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
[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"
|
||||
[defaultImage]="'assets/images/orgunit-placeholder.svg'"
|
||||
[alt]="'thumbnail.orgunit.alt'"
|
||||
|
@@ -4,7 +4,7 @@
|
||||
@if (linkType !== linkTypes.None) {
|
||||
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
[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"
|
||||
[defaultImage]="'assets/images/person-placeholder.svg'"
|
||||
[alt]="'thumbnail.person.alt'"
|
||||
|
@@ -4,7 +4,7 @@
|
||||
@if (linkType !== linkTypes.None) {
|
||||
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
[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"
|
||||
[defaultImage]="'assets/images/project-placeholder.svg'"
|
||||
[alt]="'thumbnail.project.alt'"
|
||||
|
@@ -49,7 +49,7 @@
|
||||
<ds-related-items
|
||||
[parentItem]="object"
|
||||
[relationType]="'isPublicationOfOrgUnit'"
|
||||
[label]="'relationships.isPublicationOf' | translate">
|
||||
[label]="'item.page.publications' | translate">
|
||||
</ds-related-items>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['dc.description']"
|
||||
|
@@ -28,12 +28,12 @@
|
||||
<ds-related-items
|
||||
[parentItem]="object"
|
||||
[relationType]="'isProjectOfPerson'"
|
||||
[label]="'relationships.isProjectOf' | translate">
|
||||
[label]="'item.page.projects' | translate">
|
||||
</ds-related-items>
|
||||
<ds-related-items
|
||||
[parentItem]="object"
|
||||
[relationType]="'isOrgUnitOfPerson'"
|
||||
[label]="'relationships.isOrgUnitOf' | translate">
|
||||
[label]="'item.page.org-units' | translate">
|
||||
</ds-related-items>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['person.jobTitle']"
|
||||
|
@@ -43,17 +43,17 @@
|
||||
<ds-related-items
|
||||
[parentItem]="object"
|
||||
[relationType]="'isPersonOfProject'"
|
||||
[label]="'relationships.isPersonOf' | translate">
|
||||
[label]="'item.page.authors' | translate">
|
||||
</ds-related-items>
|
||||
<ds-related-items
|
||||
[parentItem]="object"
|
||||
[relationType]="'isPublicationOfProject'"
|
||||
[label]="'relationships.isPublicationOf' | translate">
|
||||
[label]="'item.page.publications' | translate">
|
||||
</ds-related-items>
|
||||
<ds-related-items
|
||||
[parentItem]="object"
|
||||
[relationType]="'isOrgUnitOfProject'"
|
||||
[label]="'relationships.isOrgUnitOf' | translate">
|
||||
[label]="'item.page.org-units' | translate">
|
||||
</ds-related-items>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['dc.description']"
|
||||
|
@@ -49,8 +49,8 @@
|
||||
<!-- Grid container -->
|
||||
|
||||
<!-- Copyright -->
|
||||
<div class="bottom-footer p-1 d-flex justify-content-center align-items-center text-white">
|
||||
<div class="content-container">
|
||||
<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 align-self-center">
|
||||
<p class="m-0">
|
||||
<a class="text-white"
|
||||
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>
|
||||
</p>
|
||||
<ul class="footer-info list-unstyled d-flex justify-content-center mb-0">
|
||||
@if (showCookieSettings) {
|
||||
<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}}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a class="text-white"
|
||||
routerLink="info/accessibility">{{ 'footer.link.accessibility' | translate }}</a>
|
||||
@@ -89,7 +91,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
@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">
|
||||
<img class="n-coar" src="assets/images/n-coar.svg" [attr.alt]="'menu.header.image.logo' | translate" />
|
||||
{{ 'footer.link.coar-notify-support' | translate }}
|
||||
|
@@ -23,9 +23,8 @@
|
||||
|
||||
.bottom-footer {
|
||||
.notify-enabled {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 0;
|
||||
position: relative;
|
||||
margin-top: 4px;
|
||||
|
||||
.coar-notify-support-route {
|
||||
padding: 0 calc(var(--bs-spacer) / 2);
|
||||
@@ -37,7 +36,11 @@
|
||||
margin-bottom: 8.5px;
|
||||
}
|
||||
|
||||
margin-top: 20px;
|
||||
@media screen and (min-width: map-get($grid-breakpoints, md)) {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
li {
|
||||
|
@@ -63,21 +63,21 @@ describe('Footer component', () => {
|
||||
expect(comp.showEndUserAgreement).toBe(environment.info.enableEndUserAgreement);
|
||||
});
|
||||
|
||||
describe('showCookieSettings', () => {
|
||||
describe('openCookieSettings', () => {
|
||||
it('should call cookies.showSettings() if cookies is defined', () => {
|
||||
const cookies = jasmine.createSpyObj('cookies', ['showSettings']);
|
||||
comp.cookies = cookies;
|
||||
comp.showCookieSettings();
|
||||
comp.openCookieSettings();
|
||||
expect(cookies.showSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call cookies.showSettings() if cookies is undefined', () => {
|
||||
comp.cookies = undefined;
|
||||
expect(() => comp.showCookieSettings()).not.toThrow();
|
||||
expect(() => comp.openCookieSettings()).not.toThrow();
|
||||
});
|
||||
|
||||
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
|
||||
*/
|
||||
showTopFooter = false;
|
||||
showCookieSettings = false;
|
||||
showPrivacyPolicy: boolean;
|
||||
showEndUserAgreement: boolean;
|
||||
showSendFeedback$: Observable<boolean>;
|
||||
@@ -53,14 +54,15 @@ export class FooterComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.showCookieSettings = this.appConfig.info.enableCookieConsentPopup;
|
||||
this.showPrivacyPolicy = this.appConfig.info.enablePrivacyStatement;
|
||||
this.showEndUserAgreement = this.appConfig.info.enableEndUserAgreement;
|
||||
this.coarLdnEnabled$ = this.appConfig.info.enableCOARNotifySupport ? this.notifyInfoService.isCoarConfigEnabled() : observableOf(false);
|
||||
this.showSendFeedback$ = this.authorizationService.isAuthorized(FeatureID.CanSendFeedback);
|
||||
}
|
||||
|
||||
showCookieSettings() {
|
||||
if (hasValue(this.cookies)) {
|
||||
openCookieSettings() {
|
||||
if (hasValue(this.cookies) && this.cookies.showSettings instanceof Function) {
|
||||
this.cookies.showSettings();
|
||||
}
|
||||
return false;
|
||||
|
@@ -5,12 +5,14 @@
|
||||
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/>
|
||||
</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>
|
||||
<div role="toolbar" [attr.aria-label]="'nav.user.description' | translate">
|
||||
<ds-lang-switch></ds-lang-switch>
|
||||
<ds-context-help-toggle></ds-context-help-toggle>
|
||||
<ds-auth-nav-menu></ds-auth-nav-menu>
|
||||
<ds-impersonate-navbar></ds-impersonate-navbar>
|
||||
</div>
|
||||
@if (isMobile$ | async) {
|
||||
<div class="ps-2">
|
||||
<button class="navbar-toggler px-0" type="button" (click)="toggleNavbar()"
|
||||
@@ -20,7 +22,7 @@
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
@@ -23,7 +23,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
.navbar, div[role="toolbar"] {
|
||||
display: flex;
|
||||
gap: calc(var(--bs-spacer) / 3);
|
||||
align-items: center;
|
||||
|
@@ -3,13 +3,17 @@
|
||||
<div class="mt-4" [ngClass]="placeholderFontClass" @fadeIn>
|
||||
<div class="d-flex flex-row border-bottom mb-4 pb-4"></div>
|
||||
<h2> {{ 'home.recent-submissions.head' | translate }}</h2>
|
||||
<ul class="list-unstyled m-0 p-0">
|
||||
@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>
|
||||
</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>
|
||||
}
|
||||
@if (itemRD?.hasFailed) {
|
||||
|
@@ -153,6 +153,8 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
relationshipMessageKey$: Observable<string>;
|
||||
|
||||
currentEntityType$: Observable<ItemType>;
|
||||
|
||||
/**
|
||||
* The list ID to save selected entities under
|
||||
*/
|
||||
@@ -222,20 +224,12 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
public getRelationshipMessageKey(): Observable<string> {
|
||||
return observableCombineLatest([
|
||||
this.currentEntityType$,
|
||||
this.getLabel(),
|
||||
this.relatedEntityType$,
|
||||
]).pipe(
|
||||
map(([label, relatedEntityType]) => {
|
||||
if (hasValue(label) && label.indexOf('is') > -1 && label.indexOf('Of') > -1) {
|
||||
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;
|
||||
}
|
||||
map(([currentEntityType, label, relatedEntityType]: [ItemType, string, ItemType]) => {
|
||||
return `relationships.${currentEntityType.label}.${label}.${relatedEntityType.label}`;
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -469,6 +463,17 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
||||
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(
|
||||
take(1),
|
||||
).subscribe(
|
||||
|
@@ -2,14 +2,12 @@
|
||||
<div *ngVar="(originals$ | async)?.payload as originals">
|
||||
@if (hasValuesInBundle(originals)) {
|
||||
<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) {
|
||||
<ds-pagination
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
[paginationOptions]="originalOptions"
|
||||
[collectionSize]="originals?.totalElements"
|
||||
[retainScrollPosition]="true">
|
||||
<ds-pagination [hideGear]="true" [hidePagerWhenSinglePage]="true" [paginationOptions]="originalOptions"
|
||||
[collectionSize]="originals?.totalElements" [retainScrollPosition]="true">
|
||||
@for (file of originals?.page; track file) {
|
||||
<div class="file-section row mb-3">
|
||||
<div class="col-3">
|
||||
@@ -17,21 +15,35 @@
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<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>
|
||||
<dt class="col-md-4">{{"item.page.filesection.size" | translate}}</dt>
|
||||
<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>
|
||||
<dt class="col-md-4">
|
||||
{{ "item.page.filesection.size" | translate }}
|
||||
</dt>
|
||||
<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')) {
|
||||
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
||||
<dd class="col-md-8">{{file.firstMetadataValue("dc.description")}}</dd>
|
||||
<dt class="col-md-4">
|
||||
{{ "item.page.filesection.description" | translate }}
|
||||
</dt>
|
||||
<dd class="col-md-8">
|
||||
{{ file.firstMetadataValue("dc.description") }}
|
||||
</dd>
|
||||
}
|
||||
</dl>
|
||||
</div>
|
||||
<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 }}
|
||||
</span>
|
||||
</ds-file-download-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,14 +56,12 @@
|
||||
<div *ngVar="(licenses$ | async)?.payload as licenses">
|
||||
@if (hasValuesInBundle(licenses)) {
|
||||
<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) {
|
||||
<ds-pagination
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
[paginationOptions]="licenseOptions"
|
||||
[collectionSize]="licenses?.totalElements"
|
||||
[retainScrollPosition]="true">
|
||||
<ds-pagination [hideGear]="true" [hidePagerWhenSinglePage]="true" [paginationOptions]="licenseOptions"
|
||||
[collectionSize]="licenses?.totalElements" [retainScrollPosition]="true">
|
||||
@for (file of licenses?.page; track file) {
|
||||
<div class="file-section row">
|
||||
<div class="col-3">
|
||||
@@ -59,19 +69,33 @@
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<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>
|
||||
<dt class="col-md-4">{{"item.page.filesection.size" | translate}}</dt>
|
||||
<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>
|
||||
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
||||
<dd class="col-md-8">{{file.firstMetadataValue("dc.description")}}</dd>
|
||||
<dt class="col-md-4">
|
||||
{{ "item.page.filesection.size" | translate }}
|
||||
</dt>
|
||||
<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>
|
||||
<dt class="col-md-4">
|
||||
{{ "item.page.filesection.description" | translate }}
|
||||
</dt>
|
||||
<dd class="col-md-8">
|
||||
{{ file.firstMetadataValue("dc.description") }}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<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 }}
|
||||
</span>
|
||||
</ds-file-download-link>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -5,7 +5,7 @@
|
||||
[showMessage]="false"
|
||||
></ds-loading>
|
||||
}
|
||||
@if (!isLoading) {
|
||||
@else {
|
||||
<div class="media-viewer">
|
||||
@if (mediaList.length > 0) {
|
||||
<ng-container *ngVar="mediaOptions.video && ['audio', 'video'].includes(mediaList[0]?.format) as showVideo">
|
||||
@@ -33,18 +33,10 @@
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
} @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
|
||||
[thumbnail]="(thumbnailsRD$ | async)?.payload?.page[0]">
|
||||
</ds-thumbnail>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
|
@@ -1,4 +1,7 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
NO_ERRORS_SCHEMA,
|
||||
PLATFORM_ID,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
@@ -15,7 +18,9 @@ import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthService } from '../../core/auth/auth.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 { FileService } from '../../core/shared/file.service';
|
||||
import { MediaViewerItem } from '../../core/shared/media-viewer-item.model';
|
||||
import { MetadataFieldWrapperComponent } from '../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
|
||||
import { AuthServiceMock } from '../../shared/mocks/auth.service.mock';
|
||||
@@ -33,6 +38,9 @@ import { MediaViewerComponent } from './media-viewer.component';
|
||||
describe('MediaViewerComponent', () => {
|
||||
let comp: MediaViewerComponent;
|
||||
let fixture: ComponentFixture<MediaViewerComponent>;
|
||||
let authService;
|
||||
let authorizationService;
|
||||
let fileService;
|
||||
|
||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
||||
sizeBytes: 10201,
|
||||
@@ -57,7 +65,7 @@ describe('MediaViewerComponent', () => {
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'test_word.docx',
|
||||
value: 'test_image.jpg',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -75,6 +83,15 @@ describe('MediaViewerComponent', () => {
|
||||
);
|
||||
|
||||
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({
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
@@ -90,6 +107,10 @@ describe('MediaViewerComponent', () => {
|
||||
MetadataFieldWrapperComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: FileService, useValue: fileService },
|
||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
@@ -153,9 +174,9 @@ describe('MediaViewerComponent', () => {
|
||||
expect(mediaItem.thumbnail).toBe(null);
|
||||
});
|
||||
|
||||
it('should display a default, thumbnail', () => {
|
||||
it('should display a default thumbnail', () => {
|
||||
const defaultThumbnail = fixture.debugElement.query(
|
||||
By.css('ds-media-viewer-image'),
|
||||
By.css('ds-thumbnail'),
|
||||
);
|
||||
expect(defaultThumbnail.nativeElement).toBeDefined();
|
||||
});
|
||||
|
@@ -45,7 +45,7 @@
|
||||
name="syncPublications" id="publicationOption_{{option.value}}" [value]="option.value"
|
||||
required>
|
||||
<label for="publicationOption_{{option.value}}"
|
||||
class="ms-2 form-label">{{option.label | translate}}</label>
|
||||
class="form-label">{{option.label | translate}}</label>
|
||||
</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 = 'item.page.author';
|
||||
label = 'item.page.authors';
|
||||
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@
|
||||
[parentItem]="object"
|
||||
[itemType]="'Person'"
|
||||
[metadataFields]="['dc.contributor.author', 'dc.creator']"
|
||||
[label]="'relationships.isAuthorOf' | translate">
|
||||
[label]="'item.page.authors' | translate">
|
||||
</ds-metadata-representation-list>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['journal.title']"
|
||||
@@ -58,17 +58,17 @@
|
||||
<ds-related-items
|
||||
[parentItem]="object"
|
||||
[relationType]="'isProjectOfPublication'"
|
||||
[label]="'relationships.isProjectOf' | translate">
|
||||
[label]="'item.page.projects' | translate">
|
||||
</ds-related-items>
|
||||
<ds-related-items
|
||||
[parentItem]="object"
|
||||
[relationType]="'isOrgUnitOfPublication'"
|
||||
[label]="'relationships.isOrgUnitOf' | translate">
|
||||
[label]="'item.page.org-units' | translate">
|
||||
</ds-related-items>
|
||||
<ds-related-items
|
||||
[parentItem]="object"
|
||||
[relationType]="'isJournalIssueOfPublication'"
|
||||
[label]="'relationships.isJournalIssueOf' | translate">
|
||||
[label]="'item.page.journal-issue' | translate">
|
||||
</ds-related-items>
|
||||
<ds-item-page-abstract-field [item]="object"></ds-item-page-abstract-field>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
|
@@ -36,7 +36,7 @@
|
||||
[parentItem]="object"
|
||||
[itemType]="'Person'"
|
||||
[metadataFields]="['dc.contributor.author', 'dc.creator']"
|
||||
[label]="'relationships.isAuthorOf' | translate">
|
||||
[label]="'item.page.authors' | translate">
|
||||
</ds-metadata-representation-list>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['journal.title']"
|
||||
|
@@ -9,7 +9,7 @@
|
||||
@if ((isMobile$ | async) && (isAuthenticated$ | async)) {
|
||||
<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) {
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-value-input',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
export abstract class ValueInputComponent<T> {
|
||||
|
@@ -1,20 +1,25 @@
|
||||
@let isAuthenticated = (isAuthenticated$ | async);
|
||||
@if ((isMobile$ | async) !== true) {
|
||||
<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
|
||||
class="nav-item"
|
||||
(click)="$event.stopPropagation();">
|
||||
<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"
|
||||
role="menuitem"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-haspopup="menu"
|
||||
aria-controls="loginDropdownMenu"
|
||||
[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
|
||||
role="menu"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
[attr.aria-label]="'nav.login' | translate">
|
||||
<ds-log-in
|
||||
[isStandalonePage]="false"></ds-log-in>
|
||||
@@ -22,19 +27,20 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if ((isAuthenticated | async) && (showAuth | async)) {
|
||||
@if (isAuthenticated && showAuth) {
|
||||
<div class="nav-item">
|
||||
<div ngbDropdown #loggedInDrop="ngbDropdown" display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||
<a href="javascript:void(0);"
|
||||
role="menuitem"
|
||||
<button
|
||||
role="button"
|
||||
tabindex="0"
|
||||
[attr.aria-label]="'nav.user-profile-menu-and-logout' | translate"
|
||||
aria-controls="user-menu-dropdown"
|
||||
(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"
|
||||
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>
|
||||
<ds-user-menu [inExpandableNavbar]="false" (changedRoute)="loggedInDrop.close()"></ds-user-menu>
|
||||
</div>
|
||||
@@ -44,20 +50,17 @@
|
||||
</div>
|
||||
} @else {
|
||||
<div data-test="auth-nav">
|
||||
@if ((isAuthenticated | async) !== true) {
|
||||
<a routerLink="/login" routerLinkActive="active" class="loginLink px-0.5" role="button" 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">
|
||||
@if (isAuthenticated === true) {
|
||||
<a [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="logoutLink px-0" role="link" tabindex="0">
|
||||
<i class="fas fa-sign-out-alt fa-lg fa-fw"></i>
|
||||
<span class="sr-only">(current)</span>
|
||||
</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>
|
||||
}
|
||||
|
||||
|
||||
|
||||
<!-- Do not use ul/li in this menu as it breaks e2e accessibility tests -->
|
||||
|
@@ -28,3 +28,7 @@
|
||||
box-shadow: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
@@ -64,7 +64,7 @@ export class AuthNavMenuComponent implements OnInit {
|
||||
* Whether user is authenticated.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public isAuthenticated: Observable<boolean>;
|
||||
public isAuthenticated$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* True if the authentication is loading.
|
||||
@@ -74,7 +74,7 @@ export class AuthNavMenuComponent implements OnInit {
|
||||
|
||||
public isMobile$: Observable<boolean>;
|
||||
|
||||
public showAuth = observableOf(false);
|
||||
public showAuth$ = observableOf(false);
|
||||
|
||||
public user: Observable<EPerson>;
|
||||
|
||||
@@ -89,14 +89,14 @@ export class AuthNavMenuComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
// set isAuthenticated
|
||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||
this.isAuthenticated$ = this.store.pipe(select(isAuthenticated));
|
||||
|
||||
// set loading
|
||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||
|
||||
this.user = this.authService.getAuthenticatedUserFromStore();
|
||||
|
||||
this.showAuth = this.store.pipe(
|
||||
this.showAuth$ = this.store.pipe(
|
||||
select(routerStateSelector),
|
||||
filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state)),
|
||||
map((router: RouterReducerState) => (!router.state.url.startsWith(LOGIN_ROUTE)
|
||||
|
@@ -58,6 +58,7 @@ import { BrowseByComponent } from './browse-by.component';
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: 'ds-browse-entry-list-element',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
class MockThemedBrowseEntryListElementComponent {
|
||||
|
@@ -23,6 +23,7 @@ import { ComcolBrowseByComponent } from './comcol-browse-by.component';
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: '',
|
||||
standalone: true,
|
||||
template: '<span id="ComcolBrowseByComponent"></span>',
|
||||
})
|
||||
class BrowseByTestComponent {
|
||||
|
@@ -6,6 +6,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { RestResponse } from '../../core/cache/response.models';
|
||||
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||
@@ -67,6 +68,7 @@ describe('BrowserOrejimeService', () => {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true),
|
||||
getAuthenticatedUserFromStore: observableOf(user),
|
||||
getAuthenticatedUserIdFromStore: observableOf(user.id),
|
||||
});
|
||||
configurationDataService = createConfigSuccessSpy(recaptchaValue);
|
||||
findByPropertyName = configurationDataService.findByPropertyName;
|
||||
@@ -77,6 +79,8 @@ describe('BrowserOrejimeService', () => {
|
||||
},
|
||||
});
|
||||
|
||||
environment.info.enableCookieConsentPopup = true;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
BrowserOrejimeService,
|
||||
@@ -136,6 +140,7 @@ describe('BrowserOrejimeService', () => {
|
||||
|
||||
describe('initialize with user', () => {
|
||||
beforeEach(() => {
|
||||
spyOn((service as any), 'getUserId$').and.returnValue(observableOf(user.uuid));
|
||||
spyOn((service as any), 'getUser$').and.returnValue(observableOf(user));
|
||||
translateService.get.and.returnValue(observableOf('loading...'));
|
||||
spyOn(service, 'addAppMessages');
|
||||
@@ -152,6 +157,7 @@ describe('BrowserOrejimeService', () => {
|
||||
|
||||
describe('to not call the initialize user method, but the other methods', () => {
|
||||
beforeEach(() => {
|
||||
spyOn((service as any), 'getUserId$').and.returnValue(observableOf(undefined));
|
||||
spyOn((service as any), 'getUser$').and.returnValue(observableOf(undefined));
|
||||
translateService.get.and.returnValue(observableOf('loading...'));
|
||||
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(() => {
|
||||
(service as any).authService.isAuthenticated.and.returnValue(observableOf(false));
|
||||
});
|
||||
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(() => {
|
||||
(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', () => {
|
||||
getTestScheduler().expectObservable((service as any).getUser$()).toBe('(a|)', { a: user });
|
||||
it('should return the user id', () => {
|
||||
getTestScheduler().expectObservable((service as any).getUserId$()).toBe('(a|)', { a: user.id });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -243,7 +249,7 @@ describe('BrowserOrejimeService', () => {
|
||||
|
||||
describe('when no user is autheticated', () => {
|
||||
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', () => {
|
||||
@@ -256,7 +262,7 @@ describe('BrowserOrejimeService', () => {
|
||||
|
||||
describe('when user is autheticated', () => {
|
||||
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', () => {
|
||||
@@ -316,7 +322,7 @@ describe('BrowserOrejimeService', () => {
|
||||
GOOGLE_ANALYTICS_KEY = clone((service as any).GOOGLE_ANALYTICS_KEY);
|
||||
REGISTRATION_VERIFICATION_ENABLED_KEY = clone((service as any).REGISTRATION_VERIFICATION_ENABLED_KEY);
|
||||
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...'));
|
||||
spyOn(service, 'addAppMessages');
|
||||
spyOn((service as any), 'initializeUser');
|
||||
|
@@ -191,11 +191,13 @@ export class BrowserOrejimeService extends OrejimeService {
|
||||
*/
|
||||
this.translateConfiguration();
|
||||
|
||||
if (!environment.info?.enableCookieConsentPopup) {
|
||||
this.orejimeConfig.apps = [];
|
||||
} else {
|
||||
this.orejimeConfig.apps = this.filterConfigApps(appsToHide);
|
||||
|
||||
}
|
||||
this.applyUpdateSettingsCallbackToApps(user);
|
||||
|
||||
void this.lazyOrejime.then(({ init }) => {
|
||||
this.lazyOrejime.then(({ init }) => {
|
||||
this.orejimeInstance = init(this.orejimeConfig);
|
||||
});
|
||||
});
|
||||
@@ -229,13 +231,13 @@ export class BrowserOrejimeService extends OrejimeService {
|
||||
* Return saved preferences stored in the orejime cookie
|
||||
*/
|
||||
getSavedPreferences(): Observable<any> {
|
||||
return this.getUser$().pipe(
|
||||
map((user: EPerson) => {
|
||||
return this.getUserId$().pipe(
|
||||
map((userId: string) => {
|
||||
let storageName;
|
||||
if (isEmpty(user)) {
|
||||
if (isEmpty(userId)) {
|
||||
storageName = ANONYMOUS_STORAGE_NAME_OREJIME;
|
||||
} else {
|
||||
storageName = this.getStorageName(user.uuid);
|
||||
storageName = this.getStorageName(userId);
|
||||
}
|
||||
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
|
||||
* 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 CORRELATION_ID_OREJIME_KEY = 'correlation-id';
|
||||
|
||||
export const CORRELATION_ID_COOKIE = 'CORRELATION-ID';
|
||||
|
||||
/**
|
||||
* Orejime configuration
|
||||
* For more information see https://github.com/empreinte-digitale/orejime
|
||||
@@ -142,6 +146,17 @@ export function getOrejimeConfiguration(_window: NativeWindowRef): any {
|
||||
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,
|
||||
purposes: ['statistical'],
|
||||
|
@@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
|
||||
/**
|
||||
* Abstract class representing a service for handling Orejime consent preferences and UI
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export abstract class OrejimeService {
|
||||
/**
|
||||
* 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';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<button [dsBtnDisabled]="isDisabled">Test Button</button>
|
||||
`,
|
||||
imports: [
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
})
|
||||
class TestComponent {
|
||||
isDisabled = false;
|
||||
@@ -26,8 +30,7 @@ describe('DisabledDirective', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [BtnDisabledDirective],
|
||||
declarations: [TestComponent],
|
||||
imports: [BtnDisabledDirective, TestComponent],
|
||||
});
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@@ -34,6 +34,7 @@ export enum SelectorActionType {
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-dso-selector-modal',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
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 { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import {
|
||||
getFirstCompletedRemoteData,
|
||||
getAllCompletedRemoteData,
|
||||
getRemoteDataPayload,
|
||||
} from '../../core/shared/operators';
|
||||
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 GroupDataService).searchGroups(query, options);
|
||||
}),
|
||||
getFirstCompletedRemoteData(),
|
||||
getAllCompletedRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
}
|
||||
|
@@ -5,17 +5,29 @@
|
||||
[queryParams]="(bitstreamPath$| async)?.queryParams"
|
||||
[target]="isBlank ? '_blank': '_self'"
|
||||
[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"
|
||||
tabindex="0">
|
||||
@if ((canDownload$ | async) === false && (canDownloadWithToken$ | async) === false) {
|
||||
<!-- 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) {
|
||||
<!-- 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>
|
||||
</a>
|
||||
|
||||
|
@@ -1,3 +1,7 @@
|
||||
.request-a-copy-access-icon {
|
||||
color: var(--bs-success);
|
||||
}
|
||||
|
||||
.btn-download{
|
||||
width: fit-content;
|
||||
}
|
||||
|
@@ -12,7 +12,10 @@ import {
|
||||
ActivatedRoute,
|
||||
RouterLink,
|
||||
} from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
@@ -75,6 +78,11 @@ export class FileDownloadLinkComponent implements OnInit {
|
||||
*/
|
||||
@Input() showAccessStatusBadge = true;
|
||||
|
||||
/**
|
||||
* A boolean indicating whether the download icon should be displayed.
|
||||
*/
|
||||
@Input() showIcon = false;
|
||||
|
||||
itemRequest: ItemRequest;
|
||||
|
||||
bitstreamPath$: Observable<{
|
||||
@@ -90,6 +98,7 @@ export class FileDownloadLinkComponent implements OnInit {
|
||||
private authorizationService: AuthorizationDataService,
|
||||
public dsoNameService: DSONameService,
|
||||
private route: ActivatedRoute,
|
||||
private translateService: TranslateService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -153,4 +162,9 @@ export class FileDownloadLinkComponent implements OnInit {
|
||||
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() showIcon = false;
|
||||
|
||||
protected inAndOutputNames: (keyof FileDownloadLinkComponent & keyof this)[] = [
|
||||
'bitstream',
|
||||
'item',
|
||||
@@ -36,6 +38,7 @@ export class ThemedFileDownloadLinkComponent extends ThemedComponent<FileDownloa
|
||||
'isBlank',
|
||||
'enableRequestACopy',
|
||||
'showAccessStatusBadge',
|
||||
'showIcon',
|
||||
];
|
||||
|
||||
protected getComponentName(): string {
|
||||
|
@@ -96,6 +96,7 @@ import {
|
||||
getRemoteDataPayload,
|
||||
} from '../../../../core/shared/operators';
|
||||
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 { paginatedRelationsToItems } from '../../../../item-page/simple/item-types/shared/item-relationships-utils';
|
||||
import { SubmissionService } from '../../../../submission/submission.service';
|
||||
@@ -450,7 +451,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
*/
|
||||
private setItem() {
|
||||
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(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
|
@@ -28,6 +28,7 @@ import { DsDynamicInputModel } from './ds-dynamic-input.model';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-dynamic-vocabulary',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
export abstract class DsDynamicVocabularyComponent extends DynamicFormControlComponent {
|
||||
|
@@ -1,25 +1,29 @@
|
||||
@if (moreThanOneLanguage) {
|
||||
<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"
|
||||
aria-controls="language-menu-list"
|
||||
aria-haspopup="menu"
|
||||
class="dropdown-toggle btn btn-link px-0"
|
||||
[title]="'nav.language' | translate"
|
||||
(click)="$event.preventDefault()" data-toggle="dropdown" ngbDropdownToggle
|
||||
data-test="lang-switch"
|
||||
tabindex="0">
|
||||
<i class="fas fa-globe-asia fa-lg fa-fw"></i>
|
||||
</a>
|
||||
<ul ngbDropdownMenu class="dropdown-menu" [attr.aria-label]="'nav.language' |translate" id="language-menu-list" role="menu">
|
||||
</button>
|
||||
<div ngbDropdownMenu class="dropdown-menu" [attr.aria-label]="'nav.language' |translate" id="language-menu-list"
|
||||
role="listbox">
|
||||
@for (lang of translate.getLangs(); track lang) {
|
||||
<li class="dropdown-item" tabindex="0" #langSelect
|
||||
role="menuitem"
|
||||
<div class="dropdown-item" tabindex="0"
|
||||
role="option"
|
||||
[lang]="lang"
|
||||
(keyup.enter)="useLang(lang)"
|
||||
(click)="useLang(lang)"
|
||||
[attr.aria-selected]="lang === translate.currentLang"
|
||||
[class.active]="lang === translate.currentLang">
|
||||
{{ langLabel(lang) }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@@ -128,7 +128,7 @@ describe('LangSwitchComponent', () => {
|
||||
}));
|
||||
|
||||
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', () => {
|
||||
|
@@ -35,11 +35,11 @@
|
||||
<div class="mt-2">
|
||||
@if (canRegister$ | async) {
|
||||
<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) {
|
||||
<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>
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ import { ClaimedTaskActionsAbstractComponent } from './claimed-task-actions-abst
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-advanced-claimed-task-action-abstract',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
export abstract class AdvancedClaimedTaskActionsAbstractComponent extends ClaimedTaskActionsAbstractComponent implements OnInit {
|
||||
|
@@ -31,6 +31,7 @@ import { MyDSpaceReloadableActionsComponent } from '../../mydspace-reloadable-ac
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-claimed-task-action-abstract',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
export abstract class ClaimedTaskActionsAbstractComponent extends MyDSpaceReloadableActionsComponent<ClaimedTask, ClaimedTaskDataService> implements OnDestroy {
|
||||
|
@@ -37,6 +37,7 @@ export interface MyDSpaceActionsResult {
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-mydspace-actions-abstract',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
export abstract class MyDSpaceActionsComponent<T extends DSpaceObject, TService extends IdentifiableDataService<T>> {
|
||||
|
@@ -34,6 +34,7 @@ import { MyDSpaceActionsComponent } from './mydspace-actions';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-mydspace-reloadable-actions',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
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',
|
||||
language: null,
|
||||
value:
|
||||
'{"authentication":true,"preferences":true,"acknowledgement":true,"google-analytics":true}',
|
||||
'{"authentication":true,"preferences":true,"acknowledgement":true,"google-analytics":true,"correlation-id":true}',
|
||||
place: 0,
|
||||
authority: null,
|
||||
confidence: -1,
|
||||
|
@@ -1,5 +1,10 @@
|
||||
@if (showAccessStatus) {
|
||||
@if ({ status: accessStatus$ | async, date: embargoDate$ | async }; as accessStatus) {
|
||||
<span [class]="'badge bg-secondary access-status-list-element-badge ' + accessStatusClass">{{ accessStatus.status | translate: {date: accessStatus.date} }}</span>
|
||||
@if ((accessStatus$ | async); as status) {
|
||||
@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 { AccessStatusBadgeComponent } from './access-status-badge.component';
|
||||
|
||||
describe('ItemAccessStatusBadgeComponent', () => {
|
||||
describe('AccessStatusBadgeComponent', () => {
|
||||
let component: AccessStatusBadgeComponent;
|
||||
let fixture: ComponentFixture<AccessStatusBadgeComponent>;
|
||||
|
||||
@@ -100,17 +100,17 @@ describe('ItemAccessStatusBadgeComponent', () => {
|
||||
|
||||
function lookForAccessStatusBadgeForItem(status: string) {
|
||||
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() {
|
||||
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() {
|
||||
const badge = fixture.debugElement.query(By.css('span.badge'));
|
||||
expect(badge.nativeElement.textContent).toEqual(``);
|
||||
expect(badge).toBeNull();
|
||||
}
|
||||
|
||||
describe('init with item', () => {
|
||||
|
@@ -1,5 +1,7 @@
|
||||
<div>
|
||||
<span [className]="badgeClass">
|
||||
<span class="sr-only">{{ 'mydspace.status' | translate }}</span>
|
||||
{{('mydspace.status.' + badgeContent) | translate}}
|
||||
<span class="sr-only">, </span>
|
||||
</span>
|
||||
</div>
|
||||
|
@@ -1,10 +1,16 @@
|
||||
@if (privateBadge) {
|
||||
<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>
|
||||
}
|
||||
@if (withdrawnBadge) {
|
||||
<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="sr-only">, </span>
|
||||
</div>
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
@if (typeMessage) {
|
||||
<span>
|
||||
<span class="sr-only">{{ 'listelement.badge.dso-type' | translate }}</span>
|
||||
<span class="badge bg-info">{{ typeMessage | translate }}</span>
|
||||
<span class="sr-only">, </span>
|
||||
</span>
|
||||
}
|
||||
|
@@ -35,13 +35,15 @@
|
||||
[label]="('item.page.date' | translate)"
|
||||
[metadata]="'dc.date.issued'"
|
||||
[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"
|
||||
[object]="object"
|
||||
[label]="('item.page.author' | translate)"
|
||||
[label]="('item.page.authors' | translate)"
|
||||
[metadata]="['dc.contributor', 'dc.creator', 'dc.contributor.*']"
|
||||
[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 class="col-xs-12 col-md-6">
|
||||
<ds-item-detail-preview-field [item]="item"
|
||||
@@ -49,13 +51,15 @@
|
||||
[label]="('item.page.abstract' | translate)"
|
||||
[metadata]="'dc.description.abstract'"
|
||||
[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"
|
||||
[object]="object"
|
||||
[label]="('item.page.uri' | translate)"
|
||||
[metadata]="'dc.identifier.uri'"
|
||||
[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>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<div class="card">
|
||||
@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>
|
||||
</a>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<div class="card">
|
||||
@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>
|
||||
</a>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
@if (linkType !== linkTypes.None) {
|
||||
<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>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div class="col-3 col-md-2">
|
||||
@if (linkType !== linkTypes.None) {
|
||||
<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>
|
||||
</a>
|
||||
|
@@ -30,6 +30,7 @@ import { ObjectSelectService } from '../object-select.service';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-object-select-abstract',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
export abstract class ObjectSelectComponent<TDomain extends DSpaceObject> implements OnInit, OnDestroy {
|
||||
|
@@ -23,6 +23,7 @@ import { StartsWithType } from './starts-with-type';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-start-with-abstract',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
export abstract class StartsWithAbstractComponent implements OnInit, OnDestroy {
|
||||
|
@@ -3,6 +3,7 @@ import { Component } from '@angular/core';
|
||||
// noinspection AngularMissingOrInvalidDeclarationInModule
|
||||
@Component({
|
||||
selector: 'ds-test-component',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
export class TestComponent {
|
||||
|
@@ -3,6 +3,7 @@ import { Component } from '@angular/core';
|
||||
// noinspection AngularMissingOrInvalidDeclarationInModule
|
||||
@Component({
|
||||
selector: 'ds-test-component',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
export class TestComponent {
|
||||
|
@@ -37,6 +37,7 @@ import { ThemeService } from './theme.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed',
|
||||
standalone: true,
|
||||
styleUrls: ['./themed.component.scss'],
|
||||
templateUrl: './themed.component.html',
|
||||
})
|
||||
|
@@ -22,6 +22,11 @@ export class UploaderOptions {
|
||||
*/
|
||||
maxFileNumber: number;
|
||||
|
||||
/**
|
||||
* Impersonating user uuid
|
||||
*/
|
||||
impersonatingID: string;
|
||||
|
||||
/**
|
||||
* 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