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:
Andreas Awouters
2025-05-16 08:21:14 +02:00
154 changed files with 4574 additions and 3302 deletions

View File

@@ -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",

View File

@@ -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 &amp; 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)

View File

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

View File

@@ -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');
});
});

View File

@@ -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');
});

View File

@@ -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
View File

@@ -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": {

View File

@@ -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"
}

View File

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

View File

@@ -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 {

View File

@@ -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 {

View File

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

View File

@@ -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();

View File

@@ -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);
}),
);
}
}

View File

@@ -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);
});
});

View File

@@ -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);
}
/**

View File

@@ -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);
});
});

View File

@@ -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);
}
/**

View File

@@ -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'));

View File

@@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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']"

View File

@@ -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']"

View File

@@ -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']"

View File

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

View File

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

View File

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

View File

@@ -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'"

View File

@@ -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'"

View File

@@ -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'"

View File

@@ -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']"

View File

@@ -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']"

View File

@@ -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']"

View File

@@ -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 }}

View File

@@ -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 {

View File

@@ -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();
});
});

View File

@@ -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;

View File

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

View File

@@ -23,7 +23,7 @@
}
}
.navbar {
.navbar, div[role="toolbar"] {
display: flex;
gap: calc(var(--bs-spacer) / 3);
align-items: center;

View File

@@ -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) {

View File

@@ -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(

View File

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

View File

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

View File

@@ -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();
});

View File

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

View File

@@ -0,0 +1,3 @@
.form-check input, .form-check label{
width: auto;
}

View File

@@ -50,6 +50,6 @@ export class ItemPageAuthorFieldComponent extends ItemPageFieldComponent {
/**
* Label i18n key for the rendered metadata
*/
label = 'item.page.author';
label = 'item.page.authors';
}

View File

@@ -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"

View File

@@ -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']"

View File

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

View File

@@ -10,6 +10,7 @@ import {
*/
@Component({
selector: 'ds-value-input',
standalone: true,
template: '',
})
export abstract class ValueInputComponent<T> {

View File

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

View File

@@ -28,3 +28,7 @@
box-shadow: unset;
}
}
.dropdown-toggle::after {
margin-left: 0;
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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');

View File

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

View File

@@ -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'],

View File

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

View 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() {
}
}

View File

@@ -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;

View File

@@ -34,6 +34,7 @@ export enum SelectorActionType {
*/
@Component({
selector: 'ds-dso-selector-modal',
standalone: true,
template: '',
})
export abstract class DSOSelectorModalWrapperComponent implements OnInit {

View File

@@ -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(),
);
}

View File

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

View File

@@ -1,3 +1,7 @@
.request-a-copy-access-icon {
color: var(--bs-success);
}
.btn-download{
width: fit-content;
}

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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(),
);

View File

@@ -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 {

View File

@@ -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>
}

View File

@@ -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', () => {

View File

@@ -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>
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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>> {

View File

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

View File

@@ -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,

View File

@@ -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>
}
}

View File

@@ -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', () => {

View File

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

View File

@@ -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>
}

View File

@@ -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>
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -3,6 +3,7 @@ import { Component } from '@angular/core';
// noinspection AngularMissingOrInvalidDeclarationInModule
@Component({
selector: 'ds-test-component',
standalone: true,
template: '',
})
export class TestComponent {

View File

@@ -3,6 +3,7 @@ import { Component } from '@angular/core';
// noinspection AngularMissingOrInvalidDeclarationInModule
@Component({
selector: 'ds-test-component',
standalone: true,
template: '',
})
export class TestComponent {

View File

@@ -37,6 +37,7 @@ import { ThemeService } from './theme.service';
@Component({
selector: 'ds-themed',
standalone: true,
styleUrls: ['./themed.component.scss'],
templateUrl: './themed.component.html',
})

View File

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