Merge branch 'dspace-7_x' into accessibility-settings-7_x

This commit is contained in:
Andreas Awouters
2025-06-30 10:37:10 +02:00
194 changed files with 2116 additions and 1047 deletions

View File

@@ -231,10 +231,13 @@
"*.json5" "*.json5"
], ],
"extends": [ "extends": [
"plugin:jsonc/recommended-with-jsonc" "plugin:jsonc/recommended-with-json5"
], ],
"rules": { "rules": {
"no-irregular-whitespace": "error", // The ESLint core no-irregular-whitespace rule doesn't work well in JSON
// See: https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-irregular-whitespace.html
"no-irregular-whitespace": "off",
"jsonc/no-irregular-whitespace": "error",
"no-trailing-spaces": "error", "no-trailing-spaces": "error",
"jsonc/comma-dangle": [ "jsonc/comma-dangle": [
"error", "error",

View File

@@ -184,12 +184,115 @@ jobs:
# Get homepage and verify that the <meta name="title"> tag includes "DSpace". # Get homepage and verify that the <meta name="title"> tag includes "DSpace".
# If it does, then SSR is working, as this tag is created by our MetadataService. # If it does, then SSR is working, as this tag is created by our MetadataService.
# This step also prints entire HTML of homepage for easier debugging if grep fails. # This step also prints entire HTML of homepage for easier debugging if grep fails.
- name: Verify SSR (server-side rendering) - name: Verify SSR (server-side rendering) on Homepage
run: | run: |
result=$(wget -O- -q http://127.0.0.1:4000/home) result=$(wget -O- -q http://127.0.0.1:4000/home)
echo "$result" echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace
# Get a specific community in our test data and verify that the "<h1>" tag includes "Publications" (the community name).
# If it does, then SSR is working.
- name: Verify SSR on a Community page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/communities/0958c910-2037-42a9-81c7-dca80e3892b4)
echo "$result"
echo "$result" | grep -oE "<h1 [^>]*>[^><]*</h1>" | grep Publications
# Get a specific collection in our test data and verify that the "<h1>" tag includes "Articles" (the collection name).
# If it does, then SSR is working.
- name: Verify SSR on a Collection page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/collections/282164f5-d325-4740-8dd1-fa4d6d3e7200)
echo "$result"
echo "$result" | grep -oE "<h1 [^>]*>[^><]*</h1>" | grep Articles
# Get a specific publication in our test data and verify that the <meta name="title"> tag includes
# the title of this publication. If it does, then SSR is working.
- name: Verify SSR on a Publication page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/publication/6160810f-1e53-40db-81ef-f6621a727398)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "An Economic Model of Mortality Salience"
# Get a specific person in our test data and verify that the <meta name="title"> tag includes
# the name of the person. If it does, then SSR is working.
- name: Verify SSR on a Person page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/person/b1b2c768-bda1-448a-a073-fc541e8b24d9)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Simmons, Cameron"
# Get a specific project in our test data and verify that the <meta name="title"> tag includes
# the name of the project. If it does, then SSR is working.
- name: Verify SSR on a Project page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/project/46ccb608-a74c-4bf6-bc7a-e29cc7defea9)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "University Research Fellowship"
# Get a specific orgunit in our test data and verify that the <meta name="title"> tag includes
# the name of the orgunit. If it does, then SSR is working.
- name: Verify SSR on an OrgUnit page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/orgunit/9851674d-bd9a-467b-8d84-068deb568ccf)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Law and Development"
# Get a specific journal in our test data and verify that the <meta name="title"> tag includes
# the name of the journal. If it does, then SSR is working.
- name: Verify SSR on a Journal page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/journal/d4af6c3e-53d0-4757-81eb-566f3b45d63a)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental &amp; Architectural Phenomenology"
# Get a specific journal volume in our test data and verify that the <meta name="title"> tag includes
# the name of the volume. If it does, then SSR is working.
- name: Verify SSR on a Journal Volume page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/journalvolume/07c6249f-4bf7-494d-9ce3-6ffdb2aed538)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental &amp; Architectural Phenomenology Volume 28 (2017)"
# Get a specific journal issue in our test data and verify that the <meta name="title"> tag includes
# the name of the issue. If it does, then SSR is working.
- name: Verify SSR on a Journal Issue page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/journalissue/44c29473-5de2-48fa-b005-e5029aa1a50b)
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 - name: Stop running app
run: kill -9 $(lsof -t -i:4000) run: kill -9 $(lsof -t -i:4000)

View File

@@ -23,10 +23,24 @@ universal:
# Determining which styles are critical is a relatively expensive operation; this option is # Determining which styles are critical is a relatively expensive operation; this option is
# disabled (false) by default to boost server performance at the expense of loading smoothness. # disabled (false) by default to boost server performance at the expense of loading smoothness.
inlineCriticalCss: false inlineCriticalCss: false
# Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects. # Patterns to be run as regexes against the path of the page to check if SSR is allowed.
# NOTE: The "/handle/" path ensures Handle redirects work via SSR. The "/reload/" path ensures # If the path match any of the regexes it will be served directly in CSR.
# hard refreshes (e.g. after login) trigger SSR while fully reloading the page. # By default, excludes community and collection browse, global browse, global search, community list, statistics and various administrative tools.
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/', '/reload/' ] excludePathPatterns:
- pattern: "^/communities/[a-f0-9-]{36}/browse(/.*)?$"
flag: "i"
- pattern: "^/collections/[a-f0-9-]{36}/browse(/.*)?$"
flag: "i"
- pattern: "^/browse/"
- pattern: "^/search$"
- pattern: "^/community-list$"
- pattern: "^/admin/"
- pattern: "^/processes/?"
- pattern: "^/notifications/"
- pattern: "^/statistics/?"
- pattern: "^/access-control/"
- pattern: "^/health$"
# Whether to enable rendering of Search component on SSR. # Whether to enable rendering of Search component on SSR.
# If set to true the component will be included in the HTML returned from the server side rendering. # If set to true the component will be included in the HTML returned from the server side rendering.
# If set to false the component will not be included in the HTML returned from the server side rendering. # If set to false the component will not be included in the HTML returned from the server side rendering.

View File

@@ -60,7 +60,7 @@
"@angular/platform-browser-dynamic": "^15.2.10", "@angular/platform-browser-dynamic": "^15.2.10",
"@angular/platform-server": "^15.2.10", "@angular/platform-server": "^15.2.10",
"@angular/router": "^15.2.10", "@angular/router": "^15.2.10",
"@babel/runtime": "7.26.7", "@babel/runtime": "7.27.6",
"@kolkov/ngx-gallery": "^2.0.1", "@kolkov/ngx-gallery": "^2.0.1",
"@ng-bootstrap/ng-bootstrap": "^11.0.0", "@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@ng-dynamic-forms/core": "^15.0.0", "@ng-dynamic-forms/core": "^15.0.0",
@@ -73,14 +73,14 @@
"@nicky-lenaers/ngx-scroll-to": "^14.0.0", "@nicky-lenaers/ngx-scroll-to": "^14.0.0",
"angular-idle-preload": "3.0.0", "angular-idle-preload": "3.0.0",
"angulartics2": "^12.2.1", "angulartics2": "^12.2.1",
"axios": "^1.7.9", "axios": "^1.10.0",
"bootstrap": "^4.6.1", "bootstrap": "^4.6.1",
"cerialize": "0.1.18", "cerialize": "0.1.18",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
"colors": "^1.4.0", "colors": "^1.4.0",
"compression": "^1.7.5", "compression": "^1.8.0",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"core-js": "^3.40.0", "core-js": "^3.42.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"date-fns-tz": "^1.3.7", "date-fns-tz": "^1.3.7",
"deepmerge": "^4.3.1", "deepmerge": "^4.3.1",
@@ -89,9 +89,9 @@
"express-rate-limit": "^5.1.3", "express-rate-limit": "^5.1.3",
"fast-json-patch": "^3.1.1", "fast-json-patch": "^3.1.1",
"filesize": "^6.1.0", "filesize": "^6.1.0",
"http-proxy-middleware": "^2.0.7", "http-proxy-middleware": "^2.0.9",
"http-terminator": "^3.2.0", "http-terminator": "^3.2.0",
"isbot": "^5.1.22", "isbot": "^5.1.28",
"js-cookie": "2.2.1", "js-cookie": "2.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"json5": "^2.2.3", "json5": "^2.2.3",
@@ -116,8 +116,8 @@
"nouislider": "^15.8.1", "nouislider": "^15.8.1",
"pem": "1.14.8", "pem": "1.14.8",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.0", "rxjs": "^7.8.2",
"sanitize-html": "^2.14.0", "sanitize-html": "^2.17.0",
"sortablejs": "1.15.6", "sortablejs": "1.15.6",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"zone.js": "~0.13.3" "zone.js": "~0.13.3"
@@ -146,12 +146,12 @@
"@types/grecaptcha": "^3.0.9", "@types/grecaptcha": "^3.0.9",
"@types/jasmine": "~3.6.0", "@types/jasmine": "~3.6.0",
"@types/js-cookie": "2.2.6", "@types/js-cookie": "2.2.6",
"@types/lodash": "^4.17.15", "@types/lodash": "^4.17.17",
"@types/node": "^14.18.63", "@types/node": "^14.18.63",
"@types/sanitize-html": "^2.13.0", "@types/sanitize-html": "^2.16.0",
"@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0", "@typescript-eslint/parser": "^5.62.0",
"axe-core": "^4.10.2", "axe-core": "^4.10.3",
"compression-webpack-plugin": "^9.2.0", "compression-webpack-plugin": "^9.2.0",
"copy-webpack-plugin": "^6.4.1", "copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@@ -163,7 +163,7 @@
"eslint-plugin-deprecation": "^1.5.0", "eslint-plugin-deprecation": "^1.5.0",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsdoc": "^45.0.0", "eslint-plugin-jsdoc": "^45.0.0",
"eslint-plugin-jsonc": "^2.19.1", "eslint-plugin-jsonc": "^2.20.1",
"eslint-plugin-lodash": "^7.4.0", "eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"express-static-gzip": "^2.2.0", "express-static-gzip": "^2.2.0",
@@ -175,7 +175,7 @@
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "^1.5.0",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"ng-mocks": "^14.13.2", "ng-mocks": "^14.13.5",
"ngx-mask": "^13.1.7", "ngx-mask": "^13.1.7",
"nodemon": "^2.0.22", "nodemon": "^2.0.22",
"postcss": "^8.5", "postcss": "^8.5",
@@ -187,7 +187,7 @@
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "~1.84.0", "sass": "~1.89.1",
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",
"sass-resources-loader": "^2.2.5", "sass-resources-loader": "^2.2.5",
"ts-node": "^8.10.2", "ts-node": "^8.10.2",

View File

@@ -55,6 +55,7 @@ import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
import { extendEnvironmentWithAppConfig } from './src/config/config.util'; import { extendEnvironmentWithAppConfig } from './src/config/config.util';
import { logStartupMessage } from './startup-message'; import { logStartupMessage } from './startup-message';
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model'; import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
import { SsrExcludePatterns } from './src/config/universal-config.interface';
/* /*
@@ -241,7 +242,7 @@ export function app() {
* The callback function to serve server side angular * The callback function to serve server side angular
*/ */
function ngApp(req, res) { function ngApp(req, res) {
if (environment.universal.preboot && req.method === 'GET' && (req.path === '/' || environment.universal.paths.some(pathPrefix => req.path.startsWith(pathPrefix)))) { if (environment.universal.preboot && req.method === 'GET' && (req.path === '/' || !isExcludedFromSsr(req.path, environment.universal.excludePathPatterns))) {
// Render the page to user via SSR (server side rendering) // Render the page to user via SSR (server side rendering)
serverSideRender(req, res); serverSideRender(req, res);
} else { } else {
@@ -625,6 +626,21 @@ function start() {
} }
} }
/**
* Check if SSR should be skipped for path
*
* @param path
* @param excludePathPattern
*/
function isExcludedFromSsr(path: string, excludePathPattern: SsrExcludePatterns[]): boolean {
const patterns = excludePathPattern.map(p =>
new RegExp(p.pattern, p.flag || '')
);
return patterns.some((regex) => {
return regex.test(path)
});
}
/* /*
* The callback function to serve health check requests * The callback function to serve health check requests
*/ */

View File

@@ -1,5 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA, Component } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs'; import { of } from 'rxjs';
@@ -57,10 +57,15 @@ describe('BulkAccessComponent', () => {
'file': { } 'file': { }
}; };
const mockSettings: any = jasmine.createSpyObj('AccessControlFormContainerComponent', { @Component({
getValue: jasmine.createSpy('getValue'), selector: 'ds-bulk-access-settings',
reset: jasmine.createSpy('reset') template: ''
}); })
class MockBulkAccessSettingsComponent {
isFormValid = jasmine.createSpy('isFormValid').and.returnValue(false);
getValue = jasmine.createSpy('getValue');
reset = jasmine.createSpy('reset');
}
const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }]; const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }];
const selectableListState: SelectableListState = { id: 'test', selection }; const selectableListState: SelectableListState = { id: 'test', selection };
const expectedIdList = ['1234', '5678']; const expectedIdList = ['1234', '5678'];
@@ -73,7 +78,10 @@ describe('BulkAccessComponent', () => {
RouterTestingModule, RouterTestingModule,
TranslateModule.forRoot() TranslateModule.forRoot()
], ],
declarations: [ BulkAccessComponent ], declarations: [
BulkAccessComponent,
MockBulkAccessSettingsComponent,
],
providers: [ providers: [
{ provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock }, { provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock },
{ provide: NotificationsService, useValue: NotificationsServiceStub }, { provide: NotificationsService, useValue: NotificationsServiceStub },
@@ -102,7 +110,6 @@ describe('BulkAccessComponent', () => {
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListStateEmpty)); (component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListStateEmpty));
fixture.detectChanges(); fixture.detectChanges();
component.settings = mockSettings;
}); });
it('should create', () => { it('should create', () => {
@@ -119,13 +126,12 @@ describe('BulkAccessComponent', () => {
}); });
describe('when there are elements selected', () => { describe('when there are elements selected and step two form is invalid', () => {
beforeEach(() => { beforeEach(() => {
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState)); (component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState));
fixture.detectChanges(); fixture.detectChanges();
component.settings = mockSettings;
}); });
it('should create', () => { it('should create', () => {
@@ -136,9 +142,9 @@ describe('BulkAccessComponent', () => {
expect(component.objectsSelected$.value).toEqual(expectedIdList); expect(component.objectsSelected$.value).toEqual(expectedIdList);
}); });
it('should enable the execute button when there are objects selected', () => { it('should not enable the execute button when there are objects selected and step two form is invalid', () => {
component.objectsSelected$.next(['1234']); component.objectsSelected$.next(['1234']);
expect(component.canExport()).toBe(true); expect(component.canExport()).toBe(false);
}); });
it('should call the settings reset method when reset is called', () => { it('should call the settings reset method when reset is called', () => {
@@ -146,6 +152,23 @@ describe('BulkAccessComponent', () => {
expect(component.settings.reset).toHaveBeenCalled(); expect(component.settings.reset).toHaveBeenCalled();
}); });
});
describe('when there are elements selectedted and the step two form is valid', () => {
beforeEach(() => {
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState));
fixture.detectChanges();
(component as any).settings.isFormValid.and.returnValue(true);
});
it('should enable the execute button when there are objects selected and step two form is valid', () => {
component.objectsSelected$.next(['1234']);
expect(component.canExport()).toBe(true);
});
it('should call the bulkAccessControlService executeScript method when submit is called', () => { it('should call the bulkAccessControlService executeScript method when submit is called', () => {
(component.settings as any).getValue.and.returnValue(mockFormState); (component.settings as any).getValue.and.returnValue(mockFormState);
bulkAccessControlService.createPayloadFile.and.returnValue(mockFile); bulkAccessControlService.createPayloadFile.and.returnValue(mockFile);

View File

@@ -37,7 +37,7 @@ export class BulkAccessComponent implements OnInit {
constructor( constructor(
private bulkAccessControlService: BulkAccessControlService, private bulkAccessControlService: BulkAccessControlService,
private selectableListService: SelectableListService private selectableListService: SelectableListService,
) { ) {
} }
@@ -51,7 +51,7 @@ export class BulkAccessComponent implements OnInit {
} }
canExport(): boolean { canExport(): boolean {
return this.objectsSelected$.value?.length > 0; return this.objectsSelected$.value?.length > 0 && this.settings?.isFormValid();
} }
/** /**

View File

@@ -31,4 +31,8 @@ export class BulkAccessSettingsComponent {
this.controlForm.reset(); this.controlForm.reset();
} }
isFormValid() {
return this.controlForm.isValid();
}
} }

View File

@@ -1,4 +1,4 @@
<div *ngIf="registryService.getActiveMetadataField() | async; then editheader; else createHeader"></div> <div *ngIf="activeMetadataField$ | async; then editheader; else createHeader"></div>
<ng-template #createHeader> <ng-template #createHeader>
<h2>{{messagePrefix + '.create' | translate}}</h2> <h2>{{messagePrefix + '.create' | translate}}</h2>

View File

@@ -11,7 +11,7 @@ import { RegistryService } from '../../../../core/registry/registry.service';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { combineLatest } from 'rxjs'; import { Observable } from 'rxjs';
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
import { MetadataField } from '../../../../core/metadata/metadata-field.model'; import { MetadataField } from '../../../../core/metadata/metadata-field.model';
@@ -90,6 +90,8 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
*/ */
@Output() submitForm: EventEmitter<any> = new EventEmitter(); @Output() submitForm: EventEmitter<any> = new EventEmitter();
activeMetadataField$: Observable<MetadataField>;
constructor(public registryService: RegistryService, constructor(public registryService: RegistryService,
private formBuilderService: FormBuilderService, private formBuilderService: FormBuilderService,
private translateService: TranslateService) { private translateService: TranslateService) {
@@ -99,70 +101,64 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
* Initialize the component, setting up the necessary Models for the dynamic form * Initialize the component, setting up the necessary Models for the dynamic form
*/ */
ngOnInit() { ngOnInit() {
combineLatest([ this.element = new DynamicInputModel({
this.translateService.get(`${this.messagePrefix}.element`), id: 'element',
this.translateService.get(`${this.messagePrefix}.qualifier`), label: this.translateService.instant(`${this.messagePrefix}.element`),
this.translateService.get(`${this.messagePrefix}.scopenote`) name: 'element',
]).subscribe(([element, qualifier, scopenote]) => { validators: {
this.element = new DynamicInputModel({ required: null,
id: 'element', pattern: '^[^. ,]*$',
label: element, maxLength: 64,
name: 'element', },
validators: { required: true,
required: null, errorMessages: {
pattern: '^[^. ,]*$', pattern: 'error.validation.metadata.element.invalid-pattern',
maxLength: 64, maxLength: 'error.validation.metadata.element.max-length',
}, },
required: true, });
errorMessages: { this.qualifier = new DynamicInputModel({
pattern: 'error.validation.metadata.element.invalid-pattern', id: 'qualifier',
maxLength: 'error.validation.metadata.element.max-length', label: this.translateService.instant(`${this.messagePrefix}.qualifier`),
}, name: 'qualifier',
}); validators: {
this.qualifier = new DynamicInputModel({ pattern: '^[^. ,]*$',
id: 'qualifier', maxLength: 64,
label: qualifier, },
name: 'qualifier', required: false,
validators: { errorMessages: {
pattern: '^[^. ,]*$', pattern: 'error.validation.metadata.qualifier.invalid-pattern',
maxLength: 64, maxLength: 'error.validation.metadata.qualifier.max-length',
}, },
required: false, });
errorMessages: { this.scopeNote = new DynamicTextAreaModel({
pattern: 'error.validation.metadata.qualifier.invalid-pattern', id: 'scopeNote',
maxLength: 'error.validation.metadata.qualifier.max-length', label: this.translateService.instant(`${this.messagePrefix}.scopenote`),
}, name: 'scopeNote',
}); required: false,
this.scopeNote = new DynamicTextAreaModel({ rows: 5,
id: 'scopeNote', });
label: scopenote, this.formModel = [
name: 'scopeNote', new DynamicFormGroupModel(
required: false, {
rows: 5, id: 'metadatadatafieldgroup',
}); group:[this.element, this.qualifier, this.scopeNote]
this.formModel = [ })
new DynamicFormGroupModel( ];
{ this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
id: 'metadatadatafieldgroup', this.registryService.getActiveMetadataField().subscribe((field: MetadataField): void => {
group:[this.element, this.qualifier, this.scopeNote] if (field == null) {
}) this.clearFields();
]; } else {
this.formGroup = this.formBuilderService.createFormGroup(this.formModel); this.formGroup.patchValue({
this.registryService.getActiveMetadataField().subscribe((field: MetadataField): void => { metadatadatafieldgroup: {
if (field == null) { element: field.element,
this.clearFields(); qualifier: field.qualifier,
} else { scopeNote: field.scopeNote,
this.formGroup.patchValue({ },
metadatadatafieldgroup: { });
element: field.element, this.element.disabled = true;
qualifier: field.qualifier, this.qualifier.disabled = true;
scopeNote: field.scopeNote, }
},
});
this.element.disabled = true;
this.qualifier.disabled = true;
}
});
}); });
} }

View File

@@ -1,13 +1,13 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component'; import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component';
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; import { ThemedAdminSearchPageComponent } from './admin-search-page/themed-admin-search-page.component';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
import { REGISTRIES_MODULE_PATH } from './admin-routing-paths'; import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
import { ThemedAdminWorkflowPageComponent } from './admin-workflow-page/themed-admin-workflow-page.component';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -20,13 +20,13 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import
{ {
path: 'search', path: 'search',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: I18nBreadcrumbResolver },
component: AdminSearchPageComponent, component: ThemedAdminSearchPageComponent,
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' } data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }
}, },
{ {
path: 'workflow', path: 'workflow',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: I18nBreadcrumbResolver },
component: AdminWorkflowPageComponent, component: ThemedAdminWorkflowPageComponent,
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' } data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }
}, },
{ {

View File

@@ -1,5 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
import { ThemedAdminSearchPageComponent } from './themed-admin-search-page.component';
import { AdminSearchPageComponent } from './admin-search-page.component'; import { AdminSearchPageComponent } from './admin-search-page.component';
import { ItemAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component'; import { ItemAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component';
import { CommunityAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component'; import { CommunityAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component';
@@ -31,6 +32,7 @@ const ENTRY_COMPONENTS = [
ResearchEntitiesModule.withEntryComponents() ResearchEntitiesModule.withEntryComponents()
], ],
declarations: [ declarations: [
ThemedAdminSearchPageComponent,
AdminSearchPageComponent, AdminSearchPageComponent,
...ENTRY_COMPONENTS ...ENTRY_COMPONENTS
] ]

View File

@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { AdminSearchPageComponent } from './admin-search-page.component';
/**
* Themed wrapper for {@link AdminSearchPageComponent}
*/
@Component({
selector: 'ds-themed-admin-search-page',
templateUrl: '../../shared/theme-support/themed.component.html',
})
export class ThemedAdminSearchPageComponent extends ThemedComponent<AdminSearchPageComponent> {
protected getComponentName(): string {
return 'AdminSearchPageComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/admin/admin-search-page/admin-search-page.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import('./admin-search-page.component');
}
}

View File

@@ -27,6 +27,7 @@ import {
import { import {
SupervisionOrderStatusComponent SupervisionOrderStatusComponent
} from './admin-workflow-search-results/actions/workspace-item/supervision-order-status/supervision-order-status.component'; } from './admin-workflow-search-results/actions/workspace-item/supervision-order-status/supervision-order-status.component';
import { ThemedAdminWorkflowPageComponent } from './themed-admin-workflow-page.component';
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator // put only entry components that use custom decorator
@@ -42,6 +43,7 @@ const ENTRY_COMPONENTS = [
SharedModule.withEntryComponents() SharedModule.withEntryComponents()
], ],
declarations: [ declarations: [
ThemedAdminWorkflowPageComponent,
AdminWorkflowPageComponent, AdminWorkflowPageComponent,
SupervisionOrderGroupSelectorComponent, SupervisionOrderGroupSelectorComponent,
SupervisionOrderStatusComponent, SupervisionOrderStatusComponent,

View File

@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { AdminWorkflowPageComponent } from './admin-workflow-page.component';
/**
* Themed wrapper for {@link AdminWorkflowPageComponent}
*/
@Component({
selector: 'ds-themed-admin-workflow-page',
templateUrl: '../../shared/theme-support/themed.component.html',
})
export class ThemedAdminWorkflowPageComponent extends ThemedComponent<AdminWorkflowPageComponent> {
protected getComponentName(): string {
return 'AdminWorkflowPageComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/admin/admin-workflow-page/admin-workflow-page.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import('./admin-workflow-page.component');
}
}

View File

@@ -40,6 +40,8 @@ import {
import { ServerCheckGuard } from './core/server-check/server-check.guard'; import { ServerCheckGuard } from './core/server-check/server-check.guard';
import { MenuResolver } from './menu.resolver'; import { MenuResolver } from './menu.resolver';
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
import { HomePageResolver } from './home-page/home-page.resolver';
import { ViewTrackerResolverService } from './statistics/angulartics/dspace/view-tracker-resolver.service';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -63,7 +65,15 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
path: 'home', path: 'home',
loadChildren: () => import('./home-page/home-page.module') loadChildren: () => import('./home-page/home-page.module')
.then((m) => m.HomePageModule), .then((m) => m.HomePageModule),
data: { showBreadcrumbs: false }, data: {
showBreadcrumbs: false,
dsoPath: 'site'
},
resolve: {
site: HomePageResolver,
tracking: ViewTrackerResolverService,
},
canActivate: [EndUserAgreementCurrentUserGuard] canActivate: [EndUserAgreementCurrentUserGuard]
}, },
{ {
@@ -251,6 +261,7 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
}) })
], ],
exports: [RouterModule], exports: [RouterModule],
providers: [HomePageResolver, ViewTrackerResolverService],
}) })
export class AppRoutingModule { export class AppRoutingModule {

View File

@@ -10,7 +10,7 @@
</nav> </nav>
<ng-template #breadcrumb let-text="text" let-url="url"> <ng-template #breadcrumb let-text="text" let-url="url">
<li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate" [ngbTooltip]="text | translate" placement="bottom" >{{text | translate}}</a></div></li> <li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate" [ngbTooltip]="text | translate" placement="bottom" role="link" tabindex="0">{{text | translate}}</a></div></li>
</ng-template> </ng-template>
<ng-template #activeBreadcrumb let-text="text"> <ng-template #activeBreadcrumb let-text="text">

View File

@@ -10,6 +10,8 @@
<a class="btn btn-primary" <a class="btn btn-primary"
[routerLink]="['/search']" [routerLink]="['/search']"
[queryParams]="queryParams" [queryParams]="queryParams"
[queryParamsHandling]="'merge'"> [queryParamsHandling]="'merge'"
role="link"
tabindex="0">
{{ 'browse.taxonomy.button' | translate }}</a> {{ 'browse.taxonomy.button' | translate }}</a>
</div> </div>

View File

@@ -23,6 +23,7 @@ import { ThemedCollectionPageComponent } from './themed-collection-page.componen
import { MenuItemType } from '../shared/menu/menu-item-type.model'; import { MenuItemType } from '../shared/menu/menu-item-type.model';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver'; import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver';
import { ViewTrackerResolverService } from '../statistics/angulartics/dspace/view-tracker-resolver.service';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -86,6 +87,7 @@ import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-bread
pathMatch: 'full', pathMatch: 'full',
resolve: { resolve: {
menu: DSOEditMenuResolver, menu: DSOEditMenuResolver,
tracking: ViewTrackerResolverService,
}, },
} }
], ],
@@ -116,6 +118,7 @@ import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-bread
CreateCollectionPageGuard, CreateCollectionPageGuard,
CollectionPageAdministratorGuard, CollectionPageAdministratorGuard,
CommunityBreadcrumbResolver, CommunityBreadcrumbResolver,
ViewTrackerResolverService,
] ]
}) })
export class CollectionPageRoutingModule { export class CollectionPageRoutingModule {

View File

@@ -3,7 +3,6 @@
*ngVar="(collectionRD$ | async) as collectionRD"> *ngVar="(collectionRD$ | async) as collectionRD">
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut> <div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
<div *ngIf="collectionRD?.payload as collection"> <div *ngIf="collectionRD?.payload as collection">
<ds-view-tracker [object]="collection"></ds-view-tracker>
<div class="d-flex flex-row border-bottom mb-4 pb-4"> <div class="d-flex flex-row border-bottom mb-4 pb-4">
<header class="comcol-header mr-auto"> <header class="comcol-header mr-auto">
<!-- Collection Name --> <!-- Collection Name -->

View File

@@ -9,7 +9,7 @@
</span> </span>
<div class="align-middle pt-2"> <div class="align-middle pt-2">
<button *ngIf="!(dataSource.loading$ | async)" (click)="getNextPage(node)" <button *ngIf="!(dataSource.loading$ | async)" (click)="getNextPage(node)"
class="btn btn-outline-primary btn-sm" role="button"> class="btn btn-outline-primary btn-sm" role="button" tabindex="0">
<i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }} <i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }}
</button> </button>
<ds-themed-loading *ngIf="node===loadingNode && dataSource.loading$ | async" class="ds-themed-loading"></ds-themed-loading> <ds-themed-loading *ngIf="node===loadingNode && dataSource.loading$ | async" class="ds-themed-loading"></ds-themed-loading>
@@ -27,7 +27,11 @@
<button *ngIf="hasChild(null, node) | async" type="button" class="btn btn-default" cdkTreeNodeToggle <button *ngIf="hasChild(null, node) | async" type="button" class="btn btn-default" cdkTreeNodeToggle
[attr.aria-label]="(node.isExpanded ? 'communityList.collapse' : 'communityList.expand') | translate:{ name: dsoNameService.getName(node.payload) }" [attr.aria-label]="(node.isExpanded ? 'communityList.collapse' : 'communityList.expand') | translate:{ name: dsoNameService.getName(node.payload) }"
(click)="toggleExpanded(node)" (click)="toggleExpanded(node)"
data-test="expand-button"> data-test="expand-button"
(keyup.enter)="toggleExpanded(node)"
(keyup.space)="toggleExpanded(node)"
role="button"
tabindex="0">
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}" <span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
aria-hidden="true"></span> aria-hidden="true"></span>
<span class="sr-only">{{ (node.isExpanded ? 'communityList.collapse' : 'communityList.expand') | translate:{ name: dsoNameService.getName(node.payload) } }}</span> <span class="sr-only">{{ (node.isExpanded ? 'communityList.collapse' : 'communityList.expand') | translate:{ name: dsoNameService.getName(node.payload) } }}</span>
@@ -38,7 +42,7 @@
</span> </span>
<div class="d-flex flex-row"> <div class="d-flex flex-row">
<span class="align-middle pt-2 lead"> <span class="align-middle pt-2 lead">
<a [routerLink]="node.route" class="lead">{{ dsoNameService.getName(node.payload) }}</a> <a [routerLink]="node.route" class="lead" role="link" tabindex="0">{{ dsoNameService.getName(node.payload) }}</a>
<span class="pr-2">&nbsp;</span> <span class="pr-2">&nbsp;</span>
<span *ngIf="node.payload.archivedItemsCount >= 0" class="badge badge-pill badge-secondary align-top archived-items-lead">{{node.payload.archivedItemsCount}}</span> <span *ngIf="node.payload.archivedItemsCount >= 0" class="badge badge-pill badge-secondary align-top archived-items-lead">{{node.payload.archivedItemsCount}}</span>
</span> </span>
@@ -72,7 +76,7 @@
<span class="fa fa-chevron-right"></span> <span class="fa fa-chevron-right"></span>
</span> </span>
<h6 class="align-middle pt-2"> <h6 class="align-middle pt-2">
<a [routerLink]="node.route" class="lead">{{ dsoNameService.getName(node.payload) }}</a> <a [routerLink]="node.route" class="lead" role="link" tabindex="0">{{ dsoNameService.getName(node.payload) }}</a>
</h6> </h6>
</div> </div>
<ds-truncatable [id]="node.id"> <ds-truncatable [id]="node.id">

View File

@@ -16,6 +16,7 @@ import { ThemedCommunityPageComponent } from './themed-community-page.component'
import { MenuItemType } from '../shared/menu/menu-item-type.model'; import { MenuItemType } from '../shared/menu/menu-item-type.model';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { ViewTrackerResolverService } from '../statistics/angulartics/dspace/view-tracker-resolver.service';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -69,6 +70,7 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
pathMatch: 'full', pathMatch: 'full',
resolve: { resolve: {
menu: DSOEditMenuResolver, menu: DSOEditMenuResolver,
tracking: ViewTrackerResolverService,
}, },
} }
], ],
@@ -97,6 +99,7 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
LinkService, LinkService,
CreateCommunityPageGuard, CreateCommunityPageGuard,
CommunityPageAdministratorGuard, CommunityPageAdministratorGuard,
ViewTrackerResolverService,
] ]
}) })
export class CommunityPageRoutingModule { export class CommunityPageRoutingModule {

View File

@@ -1,7 +1,6 @@
<div class="container" *ngVar="(communityRD$ | async) as communityRD"> <div class="container" *ngVar="(communityRD$ | async) as communityRD">
<div class="community-page" *ngIf="communityRD?.hasSucceeded" @fadeInOut> <div class="community-page" *ngIf="communityRD?.hasSucceeded" @fadeInOut>
<div *ngIf="communityRD?.payload; let communityPayload"> <div *ngIf="communityRD?.payload; let communityPayload">
<ds-view-tracker [object]="communityPayload"></ds-view-tracker>
<div class="d-flex flex-row border-bottom mb-4 pb-4"> <div class="d-flex flex-row border-bottom mb-4 pb-4">
<header class="comcol-header mr-auto"> <header class="comcol-header mr-auto">
<!-- Community name --> <!-- Community name -->

View File

@@ -260,7 +260,7 @@ describe('AuthService test', () => {
(state as any).core = Object.create({}); (state as any).core = Object.create({});
(state as any).core.auth = authenticatedState; (state as any).core.auth = authenticatedState;
}); });
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService); authService = new AuthService(window, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
})); }));
it('should return true when user is logged in', () => { it('should return true when user is logged in', () => {
@@ -345,7 +345,7 @@ describe('AuthService test', () => {
(state as any).core = Object.create({}); (state as any).core = Object.create({});
(state as any).core.auth = authenticatedState; (state as any).core.auth = authenticatedState;
}); });
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService); authService = new AuthService(window, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
storage = (authService as any).storage; storage = (authService as any).storage;
routeServiceMock = TestBed.inject(RouteService); routeServiceMock = TestBed.inject(RouteService);
routerStub = TestBed.inject(Router); routerStub = TestBed.inject(Router);
@@ -565,7 +565,7 @@ describe('AuthService test', () => {
(state as any).core = Object.create({}); (state as any).core = Object.create({});
(state as any).core.auth = unAuthenticatedState; (state as any).core.auth = unAuthenticatedState;
}); });
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService); authService = new AuthService(window, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
})); }));
it('should return null for the shortlived token', () => { it('should return null for the shortlived token', () => {
@@ -605,7 +605,7 @@ describe('AuthService test', () => {
(state as any).core = Object.create({}); (state as any).core = Object.create({});
(state as any).core.auth = idleState; (state as any).core.auth = idleState;
}); });
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService); authService = new AuthService(window, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
})); }));
it('isUserIdle should return true when user is not idle', () => { it('isUserIdle should return true when user is not idle', () => {

View File

@@ -1,7 +1,6 @@
import { Inject, Injectable, Optional } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { HttpHeaders } from '@angular/common/http'; import { HttpHeaders } from '@angular/common/http';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
import { Observable, of as observableOf } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import { filter, map, startWith, switchMap, take } from 'rxjs/operators'; import { filter, map, startWith, switchMap, take } from 'rxjs/operators';
@@ -83,18 +82,17 @@ export class AuthService {
*/ */
private tokenRefreshTimer; private tokenRefreshTimer;
constructor(@Inject(REQUEST) protected req: any, constructor(
@Inject(NativeWindowService) protected _window: NativeWindowRef, @Inject(NativeWindowService) protected _window: NativeWindowRef,
@Optional() @Inject(RESPONSE) private response: any, protected authRequestService: AuthRequestService,
protected authRequestService: AuthRequestService, protected epersonService: EPersonDataService,
protected epersonService: EPersonDataService, protected router: Router,
protected router: Router, protected routeService: RouteService,
protected routeService: RouteService, protected storage: CookieService,
protected storage: CookieService, protected store: Store<AppState>,
protected store: Store<AppState>, protected hardRedirectService: HardRedirectService,
protected hardRedirectService: HardRedirectService, protected notificationService: NotificationsService,
private notificationService: NotificationsService, protected translateService: TranslateService
private translateService: TranslateService
) { ) {
this.store.pipe( this.store.pipe(
// when this service is constructed the store is not fully initialized yet // when this service is constructed the store is not fully initialized yet
@@ -494,10 +492,6 @@ export class AuthService {
if (this._window.nativeWindow.location) { if (this._window.nativeWindow.location) {
// Hard redirect to login page, so that all state is definitely lost // Hard redirect to login page, so that all state is definitely lost
this._window.nativeWindow.location.href = redirectUrl; this._window.nativeWindow.location.href = redirectUrl;
} else if (this.response) {
if (!this.response._headerSent) {
this.response.redirect(302, redirectUrl);
}
} else { } else {
this.router.navigateByUrl(redirectUrl); this.router.navigateByUrl(redirectUrl);
} }

View File

@@ -1,15 +1,25 @@
import { Injectable } from '@angular/core'; import { Injectable, Inject, Optional } from '@angular/core';
import { HttpHeaders } from '@angular/common/http'; import { HttpHeaders } from '@angular/common/http';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { AuthService } from './auth.service'; import { AuthService, LOGIN_ROUTE } from './auth.service';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthTokenInfo } from './models/auth-token-info.model';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { NativeWindowService, NativeWindowRef } from '../services/window.service';
import { AuthRequestService } from './auth-request.service';
import { EPersonDataService } from '../eperson/eperson-data.service';
import { Router } from '@angular/router';
import { RouteService } from '../services/route.service';
import { CookieService } from '../services/cookie.service';
import { Store } from '@ngrx/store';
import { AppState } from '../../app.reducer';
import { HardRedirectService } from '../services/hard-redirect.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
/** /**
* The auth service. * The auth service.
@@ -17,6 +27,34 @@ import { RemoteData } from '../data/remote-data';
@Injectable() @Injectable()
export class ServerAuthService extends AuthService { export class ServerAuthService extends AuthService {
constructor(
@Inject(REQUEST) protected req: any,
@Optional() @Inject(RESPONSE) private response: any,
@Inject(NativeWindowService) protected _window: NativeWindowRef,
protected authRequestService: AuthRequestService,
protected epersonService: EPersonDataService,
protected router: Router,
protected routeService: RouteService,
protected storage: CookieService,
protected store: Store<AppState>,
protected hardRedirectService: HardRedirectService,
protected notificationService: NotificationsService,
protected translateService: TranslateService
) {
super(
_window,
authRequestService,
epersonService,
router,
routeService,
storage,
store,
hardRedirectService,
notificationService,
translateService
);
}
/** /**
* Returns the authenticated user * Returns the authenticated user
* @returns {User} * @returns {User}
@@ -60,4 +98,18 @@ export class ServerAuthService extends AuthService {
map((rd: RemoteData<AuthStatus>) => Object.assign(new AuthStatus(), rd.payload)) map((rd: RemoteData<AuthStatus>) => Object.assign(new AuthStatus(), rd.payload))
); );
} }
override redirectToLoginWhenTokenExpired() {
const redirectUrl = LOGIN_ROUTE + '?expired=true';
if (this._window.nativeWindow.location) {
// Hard redirect to login page, so that all state is definitely lost
this._window.nativeWindow.location.href = redirectUrl;
} else if (this.response) {
if (!this.response._headerSent) {
this.response.redirect(302, redirectUrl);
}
} else {
this.router.navigateByUrl(redirectUrl);
}
}
} }

View File

@@ -4,14 +4,7 @@ import { typedObject } from '../../cache/builders/build-decorators';
import { HALLink } from '../../shared/hal-link.model'; import { HALLink } from '../../shared/hal-link.model';
import { ConfigObject } from './config.model'; import { ConfigObject } from './config.model';
import { SUBMISSION_SECTION_TYPE } from './config-type'; import { SUBMISSION_SECTION_TYPE } from './config-type';
import { SectionScope, SectionVisibility } from '../../../submission/objects/section-visibility.model';
/**
* An interface that define section visibility and its properties.
*/
export interface SubmissionSectionVisibility {
main: any;
other: any;
}
@typedObject @typedObject
@inheritSerialization(ConfigObject) @inheritSerialization(ConfigObject)
@@ -30,6 +23,12 @@ export class SubmissionSectionModel extends ConfigObject {
@autoserialize @autoserialize
mandatory: boolean; mandatory: boolean;
/**
* The submission scope for this section
*/
@autoserialize
scope: SectionScope;
/** /**
* A string representing the kind of section object * A string representing the kind of section object
*/ */
@@ -37,10 +36,10 @@ export class SubmissionSectionModel extends ConfigObject {
sectionType: SectionsType; sectionType: SectionsType;
/** /**
* The [SubmissionSectionVisibility] object for this section * The [SectionVisibility] object for this section
*/ */
@autoserialize @autoserialize
visibility: SubmissionSectionVisibility; visibility: SectionVisibility;
/** /**
* The {@link HALLink}s for this SubmissionSectionModel * The {@link HALLink}s for this SubmissionSectionModel

View File

@@ -22,6 +22,8 @@ import objectContaining = jasmine.objectContaining;
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { RequestParam } from '../cache/models/request-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { RestResponse } from '../cache/response.models';
import { RequestEntry } from './request-entry.model';
describe('BitstreamDataService', () => { describe('BitstreamDataService', () => {
let service: BitstreamDataService; let service: BitstreamDataService;
@@ -31,6 +33,7 @@ describe('BitstreamDataService', () => {
let bitstreamFormatService: BitstreamFormatDataService; let bitstreamFormatService: BitstreamFormatDataService;
let rdbService: RemoteDataBuildService; let rdbService: RemoteDataBuildService;
const bitstreamFormatHref = 'rest-api/bitstreamformats'; const bitstreamFormatHref = 'rest-api/bitstreamformats';
let responseCacheEntry: RequestEntry;
const bitstream1 = Object.assign(new Bitstream(), { const bitstream1 = Object.assign(new Bitstream(), {
id: 'fake-bitstream1', id: 'fake-bitstream1',
@@ -55,8 +58,13 @@ describe('BitstreamDataService', () => {
const url = 'fake-bitstream-url'; const url = 'fake-bitstream-url';
beforeEach(() => { beforeEach(() => {
responseCacheEntry = new RequestEntry();
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
objectCache = jasmine.createSpyObj('objectCache', { objectCache = jasmine.createSpyObj('objectCache', {
remove: jasmine.createSpy('remove') remove: jasmine.createSpy('remove'),
getByHref: observableOf(responseCacheEntry),
}); });
requestService = getMockRequestService(); requestService = getMockRequestService();
halService = Object.assign(new HALEndpointServiceStub(url)); halService = Object.assign(new HALEndpointServiceStub(url));

View File

@@ -137,12 +137,25 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
sendRequest(this.requestService), sendRequest(this.requestService),
take(1) take(1)
).subscribe(() => { ).subscribe(() => {
this.requestService.removeByHrefSubstring(bitstream.self + '/format'); this.deleteFormatCache(bitstream);
}); });
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
} }
private deleteFormatCache(bitstream: Bitstream) {
const bitsreamFormatUrl = bitstream.self + '/format';
this.requestService.setStaleByHrefSubstring(bitsreamFormatUrl);
// Delete also cache by uuid as the format could be cached also there
this.objectCache.getByHref(bitsreamFormatUrl).pipe(take(1)).subscribe((cachedRequest) => {
if (cachedRequest.requestUUIDs && cachedRequest.requestUUIDs.length > 0){
const requestUuid = cachedRequest.requestUUIDs[0];
if (this.requestService.hasByUUID(requestUuid)) {
this.requestService.setStaleByUUID(requestUuid);
}
}
});
}
/** /**
* Returns an observable of {@link RemoteData} of a {@link Bitstream}, based on a handle and an * Returns an observable of {@link RemoteData} of a {@link Bitstream}, based on a handle and an
* optional sequenceId or filename, with a list of {@link FollowLinkConfig}, to automatically * optional sequenceId or filename, with a list of {@link FollowLinkConfig}, to automatically

View File

@@ -20,7 +20,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { EPersonDataService } from './eperson-data.service'; import { EPersonDataService } from './eperson-data.service';
import { EPerson } from './models/eperson.model'; import { EPerson } from './models/eperson.model';
import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock'; import { EPersonMock, EPersonMock2, EPersonMockWithNoName } from '../../shared/testing/eperson.mock';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
@@ -279,6 +279,37 @@ describe('EPersonDataService', () => {
}); });
}); });
describe('updateEPerson with non existing metadata', () => {
beforeEach(() => {
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(EPersonMockWithNoName));
});
describe('add name that was not previously set', () => {
beforeEach(() => {
const changedEPerson = Object.assign(new EPerson(), {
id: EPersonMock.id,
metadata: Object.assign(EPersonMock.metadata, {
'eperson.firstname': [
{
language: null,
value: 'User',
},
],
}),
email: EPersonMock.email,
canLogIn: EPersonMock.canLogIn,
requireCertificate: EPersonMock.requireCertificate,
_links: EPersonMock._links,
});
service.updateEPerson(changedEPerson).subscribe();
});
it('should send PatchRequest with add email operation', () => {
const operations = [{ op: 'add', path: '/eperson.firstname', value: [{ language: null, value: 'User' }] }];
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
expect(requestService.send).toHaveBeenCalledWith(expected);
});
});
});
describe('getActiveEPerson', () => { describe('getActiveEPerson', () => {
it('should retrieve the ePerson currently getting edited, if any', () => { it('should retrieve the ePerson currently getting edited, if any', () => {
service.editEPerson(EPersonMock); service.editEPerson(EPersonMock);

View File

@@ -233,7 +233,8 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
* @param newEPerson * @param newEPerson
*/ */
private generateOperations(oldEPerson: EPerson, newEPerson: EPerson): Operation[] { private generateOperations(oldEPerson: EPerson, newEPerson: EPerson): Operation[] {
let operations = this.comparator.diff(oldEPerson, newEPerson).filter((operation: Operation) => operation.op === 'replace'); let operations = this.comparator.diff(oldEPerson, newEPerson)
.filter((operation: Operation) => ['replace', 'add'].includes(operation.op));
if (hasValue(oldEPerson.email) && oldEPerson.email !== newEPerson.email) { if (hasValue(oldEPerson.email) && oldEPerson.email !== newEPerson.email) {
operations = [...operations, { operations = [...operations, {
op: 'replace', path: '/email', value: newEPerson.email op: 'replace', path: '/email', value: newEPerson.email

View File

@@ -161,6 +161,7 @@ export class MetadataService {
this.setCitationKeywordsTag(); this.setCitationKeywordsTag();
this.setCitationAbstractUrlTag(); this.setCitationAbstractUrlTag();
this.setCitationDoiTag();
this.setCitationPdfUrlTag(); this.setCitationPdfUrlTag();
this.setCitationPublisherTag(); this.setCitationPublisherTag();
@@ -173,7 +174,6 @@ export class MetadataService {
// this.setCitationIssueTag(); // this.setCitationIssueTag();
// this.setCitationFirstPageTag(); // this.setCitationFirstPageTag();
// this.setCitationLastPageTag(); // this.setCitationLastPageTag();
// this.setCitationDOITag();
// this.setCitationPMIDTag(); // this.setCitationPMIDTag();
// this.setCitationFullTextTag(); // this.setCitationFullTextTag();
@@ -294,6 +294,18 @@ export class MetadataService {
} }
} }
/**
* Add <meta name="citation_doi" ... > to the <head>
*/
private setCitationDoiTag(): void {
if (this.currentObject.value instanceof Item) {
let doi = this.getMetaTagValue('dc.identifier.doi');
if (hasValue(doi)) {
this.addMetaTag('citation_doi', doi);
}
}
}
/** /**
* Add <meta name="citation_pdf_url" ... > to the <head> * Add <meta name="citation_pdf_url" ... > to the <head>
*/ */

View File

@@ -1,7 +1,4 @@
import { Inject, Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { Subject , Observable } from 'rxjs'; import { Subject , Observable } from 'rxjs';
import { CookieAttributes } from 'js-cookie'; import { CookieAttributes } from 'js-cookie';
@@ -22,9 +19,6 @@ export abstract class CookieService implements ICookieService {
protected readonly cookieSource = new Subject<{ readonly [key: string]: any }>(); protected readonly cookieSource = new Subject<{ readonly [key: string]: any }>();
public readonly cookies$ = this.cookieSource.asObservable(); public readonly cookies$ = this.cookieSource.asObservable();
constructor(@Inject(REQUEST) protected req: any) {
}
public abstract set(name: string, value: any, options?: CookieAttributes): void; public abstract set(name: string, value: any, options?: CookieAttributes): void;
public abstract remove(name: string, options?: CookieAttributes): void; public abstract remove(name: string, options?: CookieAttributes): void;

View File

@@ -1,10 +1,15 @@
import { Injectable } from '@angular/core'; import { Injectable, Inject } from '@angular/core';
import { CookieAttributes } from 'js-cookie'; import { CookieAttributes } from 'js-cookie';
import { CookieService, ICookieService } from './cookie.service'; import { CookieService, ICookieService } from './cookie.service';
import { REQUEST } from '@nguniversal/express-engine/tokens';
@Injectable() @Injectable()
export class ServerCookieService extends CookieService implements ICookieService { export class ServerCookieService extends CookieService implements ICookieService {
constructor(@Inject(REQUEST) protected req: any) {
super();
}
public set(name: string, value: any, options?: CookieAttributes): void { public set(name: string, value: any, options?: CookieAttributes): void {
return; return;
} }

View File

@@ -14,6 +14,8 @@ import {
SubmissionRequest SubmissionRequest
} from '../data/request.models'; } from '../data/request.models';
import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model';
import { of } from 'rxjs';
import { RequestEntry } from '../data/request-entry.model';
describe('SubmissionRestService test suite', () => { describe('SubmissionRestService test suite', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
@@ -38,7 +40,9 @@ describe('SubmissionRestService test suite', () => {
} }
beforeEach(() => { beforeEach(() => {
requestService = getMockRequestService(); requestService = getMockRequestService(of(Object.assign(new RequestEntry(), {
request: new SubmissionRequest('mock-request-uuid', 'mock-request-href'),
})));
rdbService = getMockRemoteDataBuildService(); rdbService = getMockRemoteDataBuildService();
scheduler = getTestScheduler(); scheduler = getTestScheduler();
halService = new HALEndpointServiceStub(resourceEndpointURL); halService = new HALEndpointServiceStub(resourceEndpointURL);
@@ -62,7 +66,7 @@ describe('SubmissionRestService test suite', () => {
scheduler.schedule(() => service.getDataById(resourceEndpoint, resourceScope).subscribe()); scheduler.schedule(() => service.getDataById(resourceEndpoint, resourceScope).subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.send).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected, false);
}); });
}); });

View File

@@ -1,10 +1,10 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable, skipWhile } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
import { import {
DeleteRequest, DeleteRequest,
PostRequest, PostRequest,
@@ -19,11 +19,25 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { getFirstCompletedRemoteData } from '../shared/operators'; import { getFirstCompletedRemoteData } from '../shared/operators';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { ErrorResponse } from '../cache/response.models';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { SubmissionResponse } from './submission-response.model'; import { SubmissionResponse } from './submission-response.model';
import { RequestError } from '../data/request-error.model';
import { RestRequest } from '../data/rest-request.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 Error(response.errorMessage);
} else {
return hasValue(response?.payload?.dataDefinition) ? response.payload.dataDefinition : [response.payload];
}
}),
distinctUntilChanged(),
);
/** /**
* The service handling all submission REST requests * The service handling all submission REST requests
@@ -48,15 +62,7 @@ export class SubmissionRestService {
*/ */
protected fetchRequest(requestId: string): Observable<SubmitDataResponseDefinitionObject> { protected fetchRequest(requestId: string): Observable<SubmitDataResponseDefinitionObject> {
return this.rdbService.buildFromRequestUUID<SubmissionResponse>(requestId).pipe( return this.rdbService.buildFromRequestUUID<SubmissionResponse>(requestId).pipe(
getFirstCompletedRemoteData(), getFirstDataDefinition(),
map((response: RemoteData<SubmissionResponse>) => {
if (response.hasFailed) {
throw new ErrorResponse({ statusText: response.errorMessage, statusCode: response.statusCode } as RequestError);
} else {
return hasValue(response.payload) ? response.payload.dataDefinition : response.payload;
}
}),
distinctUntilChanged()
); );
} }
@@ -108,21 +114,52 @@ export class SubmissionRestService {
* The endpoint link name * The endpoint link name
* @param id * @param id
* The submission Object to retrieve * The submission Object to retrieve
* @param useCachedVersionIfAvailable
* If this is true, the request will only be sent if there's no valid & cached version. Defaults to false
* @return Observable<SubmitDataResponseDefinitionObject> * @return Observable<SubmitDataResponseDefinitionObject>
* server response * server response
*/ */
public getDataById(linkName: string, id: string): Observable<SubmitDataResponseDefinitionObject> { public getDataById(linkName: string, id: string, useCachedVersionIfAvailable = false): Observable<SubmitDataResponseDefinitionObject> {
const requestId = this.requestService.generateRequestId();
return this.halService.getEndpoint(linkName).pipe( return this.halService.getEndpoint(linkName).pipe(
map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, id)), map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, id)),
filter((href: string) => isNotEmpty(href)), filter((href: string) => isNotEmpty(href)),
distinctUntilChanged(), distinctUntilChanged(),
map((endpointURL: string) => new SubmissionRequest(requestId, endpointURL)), mergeMap((endpointURL: string) => {
tap((request: RestRequest) => { this.sendGetDataRequest(endpointURL, useCachedVersionIfAvailable);
this.requestService.send(request); const startTime: number = new Date().getTime();
return this.requestService.getByHref(endpointURL).pipe(
map((requestEntry) => requestEntry?.request?.uuid),
hasValueOperator(),
distinctUntilChanged(),
switchMap((requestId) => this.rdbService.buildFromRequestUUID<SubmissionResponse>(requestId)),
// This skip ensures that if a stale object is present in the cache when you do a
// call it isn't immediately returned, but we wait until the remote data for the new request
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
// cached completed object
skipWhile((rd: RemoteData<SubmissionResponse>) => rd.isStale || (!useCachedVersionIfAvailable && rd.lastUpdated < startTime)),
tap((rd: RemoteData<SubmissionResponse>) => {
if (hasValue(rd) && rd.isStale) {
this.sendGetDataRequest(endpointURL, useCachedVersionIfAvailable);
}
})
);
}), }),
mergeMap(() => this.fetchRequest(requestId)), getFirstDataDefinition(),
distinctUntilChanged()); );
}
/**
* 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

@@ -106,6 +106,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: 10, sort: new SortOptions('fieldName', SortDirection.ASC) }, true, false, followLink('schema'));
});
describe('when querying the metadata fields returns an error response', () => { describe('when querying the metadata fields returns an error response', () => {
beforeEach(() => { beforeEach(() => {
(registryService.queryMetadataFields as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Failed')); (registryService.queryMetadataFields as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Failed'));

View File

@@ -29,6 +29,7 @@ import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { FindListOptions } from '../../../core/data/find-list-options.model';
@Component({ @Component({
selector: 'ds-metadata-field-selector', selector: 'ds-metadata-field-selector',
@@ -100,6 +101,11 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
*/ */
showInvalid = false; showInvalid = false;
searchOptions: FindListOptions = {
elementsPerPage: 10,
sort: new SortOptions('fieldName', SortDirection.ASC),
};
/** /**
* Subscriptions to unsubscribe from on destroy * Subscriptions to unsubscribe from on destroy
*/ */
@@ -182,7 +188,7 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
* Upon subscribing to the returned observable, the showInvalid flag is updated accordingly to show the feedback under the input * Upon subscribing to the returned observable, the showInvalid flag is updated accordingly to show the feedback under the input
*/ */
validate(): Observable<boolean> { validate(): Observable<boolean> {
return this.registryService.queryMetadataFields(this.mdField, null, true, false, followLink('schema')).pipe( return this.registryService.queryMetadataFields(this.mdField, this.searchOptions, true, false, followLink('schema')).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
switchMap((rd) => { switchMap((rd) => {
if (rd.hasSucceeded) { if (rd.hasSucceeded) {

View File

@@ -6,7 +6,7 @@
<a *ngIf="linkType != linkTypes.None" <a *ngIf="linkType != linkTypes.None"
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
<div> <div>
<ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-themed-thumbnail> </ds-themed-thumbnail>
@@ -37,7 +37,7 @@
<div *ngIf="linkType != linkTypes.None" class="text-center"> <div *ngIf="linkType != linkTypes.None" class="text-center">
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a> class="lead btn btn-primary viewButton" role="link" tabindex="0">{{ 'search.results.view-result' | translate}}</a>
</div> </div>
</div> </div>
</ds-truncatable> </ds-truncatable>

View File

@@ -6,7 +6,7 @@
<a *ngIf="linkType != linkTypes.None" <a *ngIf="linkType != linkTypes.None"
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
<div> <div>
<ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-themed-thumbnail> </ds-themed-thumbnail>
@@ -37,7 +37,7 @@
<div *ngIf="linkType != linkTypes.None" class="text-center"> <div *ngIf="linkType != linkTypes.None" class="text-center">
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a> class="lead btn btn-primary viewButton" role="link" tabindex="0">{{ 'search.results.view-result' | translate}}</a>
</div> </div>
</div> </div>
</ds-truncatable> </ds-truncatable>

View File

@@ -6,7 +6,7 @@
<a *ngIf="linkType != linkTypes.None" <a *ngIf="linkType != linkTypes.None"
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
<div> <div>
<ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-themed-thumbnail> </ds-themed-thumbnail>
@@ -41,7 +41,7 @@
<div *ngIf="linkType != linkTypes.None" class="text-center"> <div *ngIf="linkType != linkTypes.None" class="text-center">
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a> class="lead btn btn-primary viewButton" role="link" tabindex="0">{{ 'search.results.view-result' | translate}}</a>
</div> </div>
</div> </div>
</ds-truncatable> </ds-truncatable>

View File

@@ -2,7 +2,7 @@
<div *ngIf="showThumbnails" class="col-3 col-md-2"> <div *ngIf="showThumbnails" class="col-3 col-md-2">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"> [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" role="link" tabindex="0">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail> </ds-thumbnail>
</a> </a>
@@ -17,7 +17,7 @@
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a> [innerHTML]="dsoTitle" role="link" tabindex="0"></a>
<span *ngIf="linkType == linkTypes.None" <span *ngIf="linkType == linkTypes.None"
class="lead item-list-title dont-break-out" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span> [innerHTML]="dsoTitle"></span>

View File

@@ -2,7 +2,7 @@
<div *ngIf="showThumbnails" class="col-3 col-md-2"> <div *ngIf="showThumbnails" class="col-3 col-md-2">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"> [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" role="link" tabindex="0">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail> </ds-thumbnail>
</a> </a>
@@ -17,7 +17,7 @@
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a> [innerHTML]="dsoTitle" role="link" tabindex="0"></a>
<span *ngIf="linkType == linkTypes.None" <span *ngIf="linkType == linkTypes.None"
class="lead item-list-title dont-break-out" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span> [innerHTML]="dsoTitle"></span>

View File

@@ -1,7 +1,7 @@
<div class="row"> <div class="row">
<div *ngIf="showThumbnails" class="col-3 col-md-2"> <div *ngIf="showThumbnails" class="col-3 col-md-2">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"> [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" role="link" tabindex="0">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail> </ds-thumbnail>
</a> </a>
@@ -15,7 +15,7 @@
<ds-truncatable [id]="dso.id"> <ds-truncatable [id]="dso.id">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a> [innerHTML]="dsoTitle" role="link" tabindex="0"></a>
<span *ngIf="linkType == linkTypes.None" <span *ngIf="linkType == linkTypes.None"
class="lead item-list-title dont-break-out" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span> [innerHTML]="dsoTitle"></span>

View File

@@ -51,7 +51,7 @@
[label]="'journalissue.page.keyword'"> [label]="'journalissue.page.keyword'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -34,7 +34,7 @@
[label]="'journalvolume.page.description'"> [label]="'journalvolume.page.description'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -33,7 +33,7 @@
[label]="'journal.page.description'"> [label]="'journal.page.description'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -6,7 +6,7 @@
<a *ngIf="linkType != linkTypes.None" <a *ngIf="linkType != linkTypes.None"
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
<div> <div>
<ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-themed-thumbnail> </ds-themed-thumbnail>
@@ -43,7 +43,7 @@
<div *ngIf="linkType != linkTypes.None" class="text-center"> <div *ngIf="linkType != linkTypes.None" class="text-center">
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a> class="lead btn btn-primary viewButton" role="button" tabindex="0">{{ 'search.results.view-result' | translate}}</a>
</div> </div>
</div> </div>
</ds-truncatable> </ds-truncatable>

View File

@@ -6,7 +6,7 @@
<a *ngIf="linkType != linkTypes.None" <a *ngIf="linkType != linkTypes.None"
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
<div> <div>
<ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-themed-thumbnail> </ds-themed-thumbnail>
@@ -36,7 +36,7 @@
<div *ngIf="linkType != linkTypes.None" class="text-center"> <div *ngIf="linkType != linkTypes.None" class="text-center">
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a> class="lead btn btn-primary viewButton" role="button" tabindex="0">{{ 'search.results.view-result' | translate}}</a>
</div> </div>
</div> </div>
</ds-truncatable> </ds-truncatable>

View File

@@ -6,7 +6,7 @@
<a *ngIf="linkType != linkTypes.None" <a *ngIf="linkType != linkTypes.None"
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
<div> <div>
<ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-themed-thumbnail> </ds-themed-thumbnail>
@@ -31,7 +31,7 @@
<div *ngIf="linkType != linkTypes.None" class="text-center"> <div *ngIf="linkType != linkTypes.None" class="text-center">
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a> class="lead btn btn-primary viewButton" role="button" tabindex="0">{{ 'search.results.view-result' | translate}}</a>
</div> </div>
</div> </div>
</ds-truncatable> </ds-truncatable>

View File

@@ -2,7 +2,7 @@
<div *ngIf="showThumbnails" class="col-3 col-md-2"> <div *ngIf="showThumbnails" class="col-3 col-md-2">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="dont-break-out"> [routerLink]="[itemPageRoute]" class="dont-break-out" role="link" tabindex="0">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" <ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/orgunit-placeholder.svg'" [defaultImage]="'assets/images/orgunit-placeholder.svg'"
[alt]="'thumbnail.orgunit.alt'" [alt]="'thumbnail.orgunit.alt'"
@@ -23,7 +23,7 @@
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead" [routerLink]="[itemPageRoute]" class="lead"
[innerHTML]="dsoTitle || ('orgunit.listelement.no-title' | translate)"></a> [innerHTML]="dsoTitle || ('orgunit.listelement.no-title' | translate)" role="link" tabindex="0"></a>
<span *ngIf="linkType == linkTypes.None" <span *ngIf="linkType == linkTypes.None"
class="lead" class="lead"
[innerHTML]="dsoTitle || ('orgunit.listelement.no-title' | translate)"></span> [innerHTML]="dsoTitle || ('orgunit.listelement.no-title' | translate)"></span>

View File

@@ -2,7 +2,7 @@
<div *ngIf="showThumbnails" class="col-3 col-md-2"> <div *ngIf="showThumbnails" class="col-3 col-md-2">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="dont-break-out"> [routerLink]="[itemPageRoute]" class="dont-break-out" role="link" tabindex="0">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" <ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/person-placeholder.svg'" [defaultImage]="'assets/images/person-placeholder.svg'"
[alt]="'thumbnail.person.alt'" [alt]="'thumbnail.person.alt'"
@@ -23,7 +23,7 @@
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead" [routerLink]="[itemPageRoute]" class="lead"
[innerHTML]="dsoTitle || ('person.listelement.no-title' | translate)"></a> [innerHTML]="dsoTitle || ('person.listelement.no-title' | translate)" role="link" tabindex="0"></a>
<span *ngIf="linkType == linkTypes.None" <span *ngIf="linkType == linkTypes.None"
class="lead" class="lead"
[innerHTML]="dsoTitle || ('person.listelement.no-title' | translate)"></span> [innerHTML]="dsoTitle || ('person.listelement.no-title' | translate)"></span>

View File

@@ -2,7 +2,7 @@
<div *ngIf="showThumbnails" class="col-3 col-md-2"> <div *ngIf="showThumbnails" class="col-3 col-md-2">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="dont-break-out"> [routerLink]="[itemPageRoute]" class="dont-break-out" role="link" tabindex="0">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" <ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/project-placeholder.svg'" [defaultImage]="'assets/images/project-placeholder.svg'"
[alt]="'thumbnail.project.alt'" [alt]="'thumbnail.project.alt'"
@@ -23,7 +23,7 @@
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a> [innerHTML]="dsoTitle" role="link" tabindex="0"></a>
<span *ngIf="linkType == linkTypes.None" <span *ngIf="linkType == linkTypes.None"
class="lead item-list-title dont-break-out" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span> [innerHTML]="dsoTitle"></span>

View File

@@ -42,7 +42,7 @@
[label]="'orgunit.page.description'"> [label]="'orgunit.page.description'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -50,7 +50,7 @@
[label]="'person.page.name'"> [label]="'person.page.name'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -62,7 +62,7 @@
[label]="'project.page.keyword'"> [label]="'project.page.keyword'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -11,13 +11,13 @@
<ul class="list-unstyled mb-0"> <ul class="list-unstyled mb-0">
<li> <li>
<a routerLink="./" class="">Lorem ipsum</a> <a routerLink="./" class="" role="link" tabindex="0">Lorem ipsum</a>
</li> </li>
<li> <li>
<a routerLink="./" class="">Ut facilisis</a> <a routerLink="./" class="" role="link" tabindex="0">Ut facilisis</a>
</li> </li>
<li> <li>
<a routerLink="./" class="">Aenean sit</a> <a routerLink="./" class="" role="link" tabindex="0">Aenean sit</a>
</li> </li>
</ul> </ul>
</div> </div>
@@ -29,7 +29,7 @@
<ul class="list-unstyled mb-0"> <ul class="list-unstyled mb-0">
<li> <li>
<a routerLink="./" class="">Suspendisse potenti</a> <a routerLink="./" class="" role="link" tabindex="0">Suspendisse potenti</a>
</li> </li>
</ul> </ul>
</div> </div>
@@ -57,14 +57,14 @@
<div class="content-container"> <div class="content-container">
<p class="m-0"> <p class="m-0">
<a class="text-white" <a class="text-white"
href="http://www.dspace.org/">{{ 'footer.link.dspace' | translate}}</a> href="http://www.dspace.org/" role="link" tabindex="0">{{ 'footer.link.dspace' | translate}}</a>
{{ 'footer.copyright' | translate:{year: dateObj | date:'y'} }} {{ 'footer.copyright' | translate:{year: dateObj | date:'y'} }}
<a class="text-white" <a class="text-white"
href="https://www.lyrasis.org/">{{ 'footer.link.lyrasis' | translate}}</a> href="https://www.lyrasis.org/" role="link" tabindex="0">{{ 'footer.link.lyrasis' | translate}}</a>
</p> </p>
<ul class="footer-info list-unstyled d-flex justify-content-center mb-0"> <ul class="footer-info list-unstyled d-flex justify-content-center mb-0">
<li> <li>
<button class="btn btn-link text-white" type="button" (click)="showCookieSettings()"> <button class="btn btn-link text-white" type="button" (click)="showCookieSettings()" role="button" tabindex="0">
{{ 'footer.link.cookies' | translate}} {{ 'footer.link.cookies' | translate}}
</button> </button>
</li> </li>
@@ -74,15 +74,15 @@
</li> </li>
<li *ngIf="showPrivacyPolicy"> <li *ngIf="showPrivacyPolicy">
<a class="btn text-white" <a class="btn text-white"
routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a> routerLink="info/privacy" role="link" tabindex="0">{{ 'footer.link.privacy-policy' | translate}}</a>
</li> </li>
<li *ngIf="showEndUserAgreement"> <li *ngIf="showEndUserAgreement">
<a class="btn text-white" <a class="btn text-white"
routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a> routerLink="info/end-user-agreement" role="link" tabindex="0">{{ 'footer.link.end-user-agreement' | translate}}</a>
</li> </li>
<li *ngIf="showSendFeedback$ | async"> <li *ngIf="showSendFeedback$ | async">
<a class="btn text-white" <a class="btn text-white"
routerLink="info/feedback">{{ 'footer.link.feedback' | translate}}</a> routerLink="info/feedback" role="link" tabindex="0">{{ 'footer.link.feedback' | translate}}</a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@@ -1,7 +1,7 @@
<header> <header>
<div class="container"> <div class="container">
<div class="d-flex flex-row justify-content-between"> <div class="d-flex flex-row justify-content-between">
<a class="navbar-brand my-2" routerLink="/home"> <a class="navbar-brand my-2" routerLink="/home" role="button" tabindex="0">
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/> <img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/>
</a> </a>

View File

@@ -14,7 +14,7 @@
<li>issue permanent urls and trustworthy identifiers, including optional integrations with handle.net and DataCite DOI</li> <li>issue permanent urls and trustworthy identifiers, including optional integrations with handle.net and DataCite DOI</li>
</ul> </ul>
<p>Join an international community of <a href="https://wiki.lyrasis.org/display/DSPACE/DSpace+Positioning" <p>Join an international community of <a href="https://wiki.lyrasis.org/display/DSPACE/DSpace+Positioning"
target="_blank">leading institutions using DSpace</a>. target="_blank" role="link" tabindex="0">leading institutions using DSpace</a>.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { HomePageResolver } from './home-page.resolver';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedHomePageComponent } from './themed-home-page.component'; import { ThemedHomePageComponent } from './themed-home-page.component';
import { MenuItemType } from '../shared/menu/menu-item-type.model'; import { MenuItemType } from '../shared/menu/menu-item-type.model';
@@ -28,15 +27,9 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
} as LinkMenuItemModel, } as LinkMenuItemModel,
}], }],
}, },
},
resolve: {
site: HomePageResolver
} }
} }
]) ])
],
providers: [
HomePageResolver
] ]
}) })
export class HomePageRoutingModule { export class HomePageRoutingModule {

View File

@@ -1,8 +1,5 @@
<ds-themed-home-news></ds-themed-home-news> <ds-themed-home-news></ds-themed-home-news>
<div class="container"> <div class="container">
<ng-container *ngIf="(site$ | async) as site">
<ds-view-tracker [object]="site"></ds-view-tracker>
</ng-container>
<ds-themed-search-form [inPlaceSearch]="false" [searchPlaceholder]="'home.search-form.placeholder' | translate"></ds-themed-search-form> <ds-themed-search-form [inPlaceSearch]="false" [searchPlaceholder]="'home.search-form.placeholder' | translate"></ds-themed-search-form>
<ds-themed-top-level-community-list></ds-themed-top-level-community-list> <ds-themed-top-level-community-list></ds-themed-top-level-community-list>
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list> <ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>

View File

@@ -6,7 +6,7 @@
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode" class="pb-4"> <ds-listable-object-component-loader [object]="item" [viewMode]="viewMode" class="pb-4">
</ds-listable-object-component-loader> </ds-listable-object-component-loader>
</div> </div>
<button (click)="onLoadMore()" class="btn btn-primary search-button mt-4"> {{'vocabulary-treeview.load-more' | translate }} ...</button> <button (click)="onLoadMore()" class="btn btn-primary search-button mt-4" role="button" tabindex="0"> {{'vocabulary-treeview.load-more' | translate }} ...</button>
</div> </div>
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.recent-submissions' | translate}}"></ds-error> <ds-error *ngIf="itemRD?.hasFailed" message="{{'error.recent-submissions' | translate}}"></ds-error>
<ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}"> <ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}">

View File

@@ -6,7 +6,7 @@
<ds-alert [type]="AlertTypeEnum.Warning"> <ds-alert [type]="AlertTypeEnum.Warning">
<div class="d-flex justify-content-between flex-wrap"> <div class="d-flex justify-content-between flex-wrap">
<span class="align-self-center">{{'item.alerts.withdrawn' | translate}}</span> <span class="align-self-center">{{'item.alerts.withdrawn' | translate}}</span>
<a routerLink="/home" class="btn btn-primary btn-sm">{{"404.link.home-page" | translate}}</a> <a routerLink="/home" class="btn btn-primary btn-sm" role="link" tabindex="0">{{"404.link.home-page" | translate}}</a>
</div> </div>
</ds-alert> </ds-alert>
</div> </div>

View File

@@ -3,7 +3,7 @@
<div class="col-12"> <div class="col-12">
<h1 class="border-bottom">{{'item.edit.head' | translate}}</h1> <h1 class="border-bottom">{{'item.edit.head' | translate}}</h1>
<div class="pt-2"> <div class="pt-2">
<ul class="nav nav-tabs justify-content-start" role="tablist"> <ul *ngIf="pages.length > 0" class="nav nav-tabs justify-content-start" role="tablist">
<li *ngFor="let page of pages" class="nav-item" role="presentation"> <li *ngFor="let page of pages" class="nav-item" role="presentation">
<a *ngIf="(page.enabled | async)" <a *ngIf="(page.enabled | async)"
[attr.aria-selected]="page.page === currentPage" [attr.aria-selected]="page.page === currentPage"

View File

@@ -1,9 +1,9 @@
<div class="container"> <div class="container">
<ds-alert [type]="'alert-info'" [content]="'item.edit.authorizations.heading'"></ds-alert> <ds-alert [type]="AlertType.Info" [content]="'item.edit.authorizations.heading'"></ds-alert>
<ds-resource-policies [resourceType]="'item'" [resourceName]="(getItemName() | async)" <ds-resource-policies [resourceType]="'item'" [resourceName]="itemName$ | async"
[resourceUUID]="(getItemUUID() | async)"> [resourceUUID]="(item$ | async)?.id">
</ds-resource-policies> </ds-resource-policies>
<ng-container *ngFor="let bundle of (bundles$ | async); trackById"> <ng-container *ngFor="let bundle of (bundles$ | async)">
<ds-resource-policies [resourceType]="'bundle'" [resourceUUID]="bundle.id" [resourceName]="bundle.name"> <ds-resource-policies [resourceType]="'bundle'" [resourceUUID]="bundle.id" [resourceName]="bundle.name">
</ds-resource-policies> </ds-resource-policies>
<ng-container *ngIf="(bundleBitstreamsMap.get(bundle.id)?.bitstreams | async)?.length > 0"> <ng-container *ngIf="(bundleBitstreamsMap.get(bundle.id)?.bitstreams | async)?.length > 0">
@@ -16,7 +16,7 @@
</div> </div>
<div class="card-body" [id]="bundle.id" [ngbCollapse]="bundleBitstreamsMap.get(bundle.id).isCollapsed"> <div class="card-body" [id]="bundle.id" [ngbCollapse]="bundleBitstreamsMap.get(bundle.id).isCollapsed">
<ng-container <ng-container
*ngFor="let bitstream of (bundleBitstreamsMap.get(bundle.id).bitstreams | async); trackById"> *ngFor="let bitstream of (bundleBitstreamsMap.get(bundle.id).bitstreams | async)">
<ds-resource-policies [resourceType]="'bitstream'" [resourceUUID]="bitstream.id" <ds-resource-policies [resourceType]="'bitstream'" [resourceUUID]="bitstream.id"
[resourceName]="bitstream.name"></ds-resource-policies> [resourceName]="bitstream.name"></ds-resource-policies>
</ng-container> </ng-container>

View File

@@ -147,17 +147,9 @@ describe('ItemAuthorizationsComponent test suite', () => {
})); }));
}); });
it('should get the item UUID', () => {
expect(comp.getItemUUID()).toBeObservable(cold('(a|)', {
a: item.id
}));
});
it('should get the item\'s bundle', () => { it('should get the item\'s bundle', () => {
expect(comp.getItemBundles()).toBeObservable(cold('a', { expect(comp.bundles$).toBeObservable(cold('a', {
a: bundles a: bundles
})); }));

View File

@@ -4,7 +4,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
import { catchError, filter, first, map, mergeMap, take } from 'rxjs/operators'; import { catchError, filter, map, mergeMap, take } from 'rxjs/operators';
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
import { import {
@@ -17,6 +17,7 @@ import { LinkService } from '../../../core/cache/builders/link.service';
import { Bundle } from '../../../core/shared/bundle.model'; import { Bundle } from '../../../core/shared/bundle.model';
import { hasValue, isNotEmpty } from '../../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../../shared/empty.util';
import { Bitstream } from '../../../core/shared/bitstream.model'; import { Bitstream } from '../../../core/shared/bitstream.model';
import { AlertType } from '../../../shared/alert/alert-type';
/** /**
* Interface for a bundle's bitstream map entry * Interface for a bundle's bitstream map entry
@@ -52,7 +53,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
* The target editing item * The target editing item
* @type {Observable<Item>} * @type {Observable<Item>}
*/ */
private item$: Observable<Item>; item$: Observable<Item>;
/** /**
* Array to track all subscriptions and unsubscribe them onDestroy * Array to track all subscriptions and unsubscribe them onDestroy
@@ -91,16 +92,13 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
*/ */
private bitstreamPageSize = 4; private bitstreamPageSize = 4;
/** itemName$: Observable<string>;
* Initialize instance variables
* readonly AlertType = AlertType;
* @param {LinkService} linkService
* @param {ActivatedRoute} route
* @param nameService
*/
constructor( constructor(
private linkService: LinkService, protected linkService: LinkService,
private route: ActivatedRoute, protected route: ActivatedRoute,
public nameService: DSONameService public nameService: DSONameService
) { ) {
} }
@@ -109,37 +107,19 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
* Initialize the component, setting up the bundle and bitstream within the item * Initialize the component, setting up the bundle and bitstream within the item
*/ */
ngOnInit(): void { ngOnInit(): void {
this.getBundlesPerItem(); this.getBundlesPerItem();
this.itemName$ = this.getItemName();
} }
/** /**
* Return the item's UUID * Return the item's name
*/ */
getItemUUID(): Observable<string> { private getItemName(): Observable<string> {
return this.item$.pipe(
map((item: Item) => item.id),
first((UUID: string) => isNotEmpty(UUID))
);
}
/**
* Return the item's name
*/
getItemName(): Observable<string> {
return this.item$.pipe( return this.item$.pipe(
map((item: Item) => this.nameService.getName(item)) map((item: Item) => this.nameService.getName(item))
); );
} }
/**
* Return all item's bundles
*
* @return an observable that emits all item's bundles
*/
getItemBundles(): Observable<Bundle[]> {
return this.bundles$.asObservable();
}
/** /**
* Get all bundles per item * Get all bundles per item
* and all the bitstreams per bundle * and all the bitstreams per bundle

View File

@@ -1,7 +1,7 @@
import { ChangeDetectorRef, Component, NgZone, OnDestroy, HostListener } from '@angular/core'; import { ChangeDetectorRef, Component, NgZone, OnDestroy, HostListener } from '@angular/core';
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
import { map, switchMap, take } from 'rxjs/operators'; import { map, switchMap, take } from 'rxjs/operators';
import { Observable, Subscription, combineLatest, BehaviorSubject, tap } from 'rxjs'; import { Observable, Subscription, combineLatest, BehaviorSubject } from 'rxjs';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@@ -187,15 +187,28 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: this.bundlesOptions})).pipe( this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: this.bundlesOptions})).pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
tap((bundlesPL: PaginatedList<Bundle>) => ).subscribe((bundles: PaginatedList<Bundle>) => {
this.showLoadMoreLink$.next(bundlesPL.pageInfo.currentPage < bundlesPL.pageInfo.totalPages) this.updateBundles(bundles);
),
map((bundlePage: PaginatedList<Bundle>) => bundlePage.page),
).subscribe((bundles: Bundle[]) => {
this.bundlesSubject.next([...this.bundlesSubject.getValue(), ...bundles]);
}); });
} }
/**
* Update the subject containing the bundles with the provided bundles.
* Also updates the showLoadMoreLink observable so it does not show up when it is no longer necessary.
*/
updateBundles(newBundlesPL: PaginatedList<Bundle>) {
const currentBundles = this.bundlesSubject.getValue();
// Only add bundles to the bundle subject if they are not present yet
const bundlesToAdd = newBundlesPL.page
.filter(bundleToAdd => !currentBundles.some(currentBundle => currentBundle.id === bundleToAdd.id));
const updatedBundles = [...currentBundles, ...bundlesToAdd];
this.showLoadMoreLink$.next(updatedBundles.length < newBundlesPL.totalElements);
this.bundlesSubject.next(updatedBundles);
}
/** /**
* Submit the current changes * Submit the current changes

View File

@@ -203,8 +203,8 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy {
switchMap(() => this.bundleService.getBitstreams( switchMap(() => this.bundleService.getBitstreams(
this.bundle.id, this.bundle.id,
paginatedOptions, paginatedOptions,
followLink('format') followLink('format'),
)) )),
); );
}), }),
getAllSucceededRemoteData(), getAllSucceededRemoteData(),

View File

@@ -1,6 +1,6 @@
<ds-metadata-field-wrapper [label]="label | translate"> <ds-metadata-field-wrapper [label]="label | translate">
<div class="collections"> <div class="collections">
<a *ngFor="let collection of (this.collections$ | async); let last=last;" [routerLink]="['/collections', collection.id]"> <a *ngFor="let collection of (this.collections$ | async); let last=last;" [routerLink]="['/collections', collection.id]" role="link" tabindex="0">
<span>{{ dsoNameService.getName(collection) }}</span><span *ngIf="!last" [innerHTML]="separator"></span> <span>{{ dsoNameService.getName(collection) }}</span><span *ngIf="!last" [innerHTML]="separator"></span>
</a> </a>
</div> </div>
@@ -15,6 +15,7 @@
class="load-more-btn btn btn-sm btn-outline-secondary" class="load-more-btn btn btn-sm btn-outline-secondary"
role="button" role="button"
href="javascript:void(0);" href="javascript:void(0);"
tabindex="0"
> >
{{'item.page.collections.load-more' | translate}} {{'item.page.collections.load-more' | translate}}
</a> </a>

View File

@@ -1,5 +1,5 @@
<ds-metadata-field-wrapper [label]="label | translate"> <ds-metadata-field-wrapper [label]="label | translate">
<a class="dont-break-out" *ngFor="let mdValue of mdValues; let last=last;" [href]="mdValue.value" [target]="linkTarget"> <a class="dont-break-out" *ngFor="let mdValue of mdValues; let last=last;" [href]="mdValue.value" [target]="linkTarget" role="link" tabindex="0">
{{ linktext || mdValue.value }}<span *ngIf="!last" [innerHTML]="separator"></span> {{ linktext || mdValue.value }}<span *ngIf="!last" [innerHTML]="separator"></span>
</a> </a>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>

View File

@@ -21,7 +21,9 @@
<a class="dont-break-out ds-simple-metadata-link" <a class="dont-break-out ds-simple-metadata-link"
[href]="value" [href]="value"
[attr.target]="getLinkAttributes(value).target" [attr.target]="getLinkAttributes(value).target"
[attr.rel]="getLinkAttributes(value).rel"> [attr.rel]="getLinkAttributes(value).rel"
role="link"
tabindex="0">
{{value}} {{value}}
</a> </a>
</ng-template> </ng-template>
@@ -35,5 +37,5 @@
<ng-template #browselink let-value="value"> <ng-template #browselink let-value="value">
<a class="dont-break-out preserve-line-breaks ds-browse-link" <a class="dont-break-out preserve-line-breaks ds-browse-link"
[routerLink]="['/browse', browseDefinition.id]" [routerLink]="['/browse', browseDefinition.id]"
[queryParams]="getQueryParams(value)">{{value}}</a> [queryParams]="getQueryParams(value)" role="link" tabindex="0">{{value}}</a>
</ng-template> </ng-template>

View File

@@ -3,7 +3,6 @@
<div *ngIf="itemRD?.payload as item"> <div *ngIf="itemRD?.payload as item">
<ds-themed-item-alerts [item]="item"></ds-themed-item-alerts> <ds-themed-item-alerts [item]="item"></ds-themed-item-alerts>
<ds-item-versions-notice [item]="item"></ds-item-versions-notice> <ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker>
<div *ngIf="!item.isWithdrawn || (isAdmin$|async)" class="full-item-info"> <div *ngIf="!item.isWithdrawn || (isAdmin$|async)" class="full-item-info">
<div class="d-flex flex-row"> <div class="d-flex flex-row">
<ds-themed-item-page-title-field class="mr-auto" [item]="item"></ds-themed-item-page-title-field> <ds-themed-item-page-title-field class="mr-auto" [item]="item"></ds-themed-item-page-title-field>

View File

@@ -19,6 +19,7 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
import { OrcidPageComponent } from './orcid-page/orcid-page.component'; import { OrcidPageComponent } from './orcid-page/orcid-page.component';
import { OrcidPageGuard } from './orcid-page/orcid-page.guard'; import { OrcidPageGuard } from './orcid-page/orcid-page.guard';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
import { ViewTrackerResolverService } from '../statistics/angulartics/dspace/view-tracker-resolver.service';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -37,6 +38,7 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
pathMatch: 'full', pathMatch: 'full',
resolve: { resolve: {
menu: DSOEditMenuResolver, menu: DSOEditMenuResolver,
tracking: ViewTrackerResolverService,
}, },
}, },
{ {
@@ -44,6 +46,7 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
component: ThemedFullItemPageComponent, component: ThemedFullItemPageComponent,
resolve: { resolve: {
menu: DSOEditMenuResolver, menu: DSOEditMenuResolver,
tracking: ViewTrackerResolverService,
}, },
}, },
{ {
@@ -103,7 +106,8 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
LinkService, LinkService,
ItemPageAdministratorGuard, ItemPageAdministratorGuard,
VersionResolver, VersionResolver,
OrcidPageGuard OrcidPageGuard,
ViewTrackerResolverService,
] ]
}) })

View File

@@ -16,12 +16,7 @@
</ng-container> </ng-container>
</div> </div>
<ng-template #showThumbnail> <ng-template #showThumbnail>
<ds-themed-media-viewer-image *ngIf="mediaOptions.image && mediaOptions.video" <ds-themed-thumbnail [thumbnail]="(thumbnailsRD$ | async)?.payload?.page[0]">
[image]="(thumbnailsRD$ | async)?.payload?.page[0]?._links.content.href || thumbnailPlaceholder" </ds-themed-thumbnail>
[preview]="false"
></ds-themed-media-viewer-image>
<ds-thumbnail *ngIf="!(mediaOptions.image && mediaOptions.video)"
[thumbnail]="(thumbnailsRD$ | async)?.payload?.page[0]">
</ds-thumbnail>
</ng-template> </ng-template>
</ng-container> </ng-container>

View File

@@ -139,9 +139,9 @@ describe('MediaViewerComponent', () => {
expect(mediaItem.thumbnail).toBe(null); expect(mediaItem.thumbnail).toBe(null);
}); });
it('should display a default, thumbnail', () => { it('should display a default thumbnail', () => {
const defaultThumbnail = fixture.debugElement.query( const defaultThumbnail = fixture.debugElement.query(
By.css('ds-themed-media-viewer-image') By.css('ds-themed-thumbnail')
); );
expect(defaultThumbnail.nativeElement).toBeDefined(); expect(defaultThumbnail.nativeElement).toBeDefined();
}); });

View File

@@ -1,18 +1,18 @@
<div class="container mb-5"> <div class="container mb-5">
<h1>{{'person.orcid.registry.auth' | translate}}</h1> <h1>{{'person.orcid.registry.auth' | translate}}</h1>
<ng-container *ngIf="(isLinkedToOrcid() | async); then orcidLinked; else orcidNotLinked"></ng-container> <ng-container *ngIf="(isOrcidLinked$ | async); then orcidLinked; else orcidNotLinked"></ng-container>
</div> </div>
<ng-template #orcidLinked> <ng-template #orcidLinked>
<div data-test="orcidLinked"> <div data-test="orcidLinked">
<div class="row"> <div class="row">
<div *ngIf="(hasOrcidAuthorizations() | async)" class="col-sm-6 mb-3" data-test="hasOrcidAuthorizations"> <div *ngIf="(hasOrcidAuthorizations$ | async)" class="col-sm-6 mb-3" data-test="hasOrcidAuthorizations">
<div class="card h-100"> <div class="card h-100">
<div class="card-header">{{ 'person.page.orcid.granted-authorizations'| translate }}</div> <div class="card-header">{{ 'person.page.orcid.granted-authorizations'| translate }}</div>
<div class="card-body"> <div class="card-body">
<div class="container p-0"> <div class="container p-0">
<ul> <ul>
<li *ngFor="let auth of (getOrcidAuthorizations() | async)" data-test="orcidAuthorization"> <li *ngFor="let auth of profileAuthorizationScopes$ | async" data-test="orcidAuthorization">
{{getAuthorizationDescription(auth) | translate}} {{getAuthorizationDescription(auth) | translate}}
</li> </li>
</ul> </ul>
@@ -25,13 +25,13 @@
<div class="card-header">{{ 'person.page.orcid.missing-authorizations'| translate }}</div> <div class="card-header">{{ 'person.page.orcid.missing-authorizations'| translate }}</div>
<div class="card-body"> <div class="card-body">
<div class="container"> <div class="container">
<ds-alert *ngIf="!(hasMissingOrcidAuthorizations() | async)" [type]="'alert-success'" data-test="noMissingOrcidAuthorizations"> <ds-alert *ngIf="!(hasMissingOrcidAuthorizations$ | async)" [type]="AlertType.Success" data-test="noMissingOrcidAuthorizations">
{{'person.page.orcid.no-missing-authorizations-message' | translate}} {{'person.page.orcid.no-missing-authorizations-message' | translate}}
</ds-alert> </ds-alert>
<ds-alert *ngIf="(hasMissingOrcidAuthorizations() | async)" [type]="'alert-warning'" data-test="missingOrcidAuthorizations"> <ds-alert *ngIf="(hasMissingOrcidAuthorizations$ | async)" [type]="AlertType.Warning" data-test="missingOrcidAuthorizations">
{{'person.page.orcid.missing-authorizations-message' | translate}} {{'person.page.orcid.missing-authorizations-message' | translate}}
<ul> <ul>
<li *ngFor="let auth of (getMissingOrcidAuthorizations() | async)" data-test="missingOrcidAuthorization"> <li *ngFor="let auth of profileAuthorizationScopes$ | async" data-test="missingOrcidAuthorization">
{{getAuthorizationDescription(auth) | translate }} {{getAuthorizationDescription(auth) | translate }}
</li> </li>
</ul> </ul>
@@ -41,11 +41,11 @@
</div> </div>
</div> </div>
</div> </div>
<ds-alert *ngIf="(onlyAdminCanDisconnectProfileFromOrcid() | async) && !(ownerCanDisconnectProfileFromOrcid() | async)" <ds-alert *ngIf="(onlyAdminCanDisconnectProfileFromOrcid$ | async) && !(ownerCanDisconnectProfileFromOrcid$ | async)"
[type]="'alert-warning'" data-test="unlinkOnlyAdmin"> [type]="AlertType.Warning" data-test="unlinkOnlyAdmin">
{{ 'person.page.orcid.remove-orcid-message' | translate}} {{ 'person.page.orcid.remove-orcid-message' | translate}}
</ds-alert> </ds-alert>
<div class="row" *ngIf="(ownerCanDisconnectProfileFromOrcid() | async)" data-test="unlinkOwner"> <div class="row" *ngIf="(ownerCanDisconnectProfileFromOrcid$ | async)" data-test="unlinkOwner">
<div class="col"> <div class="col">
<button type="submit" class="btn btn-danger float-right" (click)="unlinkOrcid()" <button type="submit" class="btn btn-danger float-right" (click)="unlinkOrcid()"
[dsBtnDisabled]="(unlinkProcessing | async)"> [dsBtnDisabled]="(unlinkProcessing | async)">
@@ -54,7 +54,7 @@
<span *ngIf="(unlinkProcessing | async)"><i <span *ngIf="(unlinkProcessing | async)"><i
class='fas fa-circle-notch fa-spin'></i> {{'person.page.orcid.unlink.processing' | translate}}</span> class='fas fa-circle-notch fa-spin'></i> {{'person.page.orcid.unlink.processing' | translate}}</span>
</button> </button>
<button *ngIf="(hasMissingOrcidAuthorizations() | async)" type="submit" <button *ngIf="(hasMissingOrcidAuthorizations$ | async)" type="submit"
class="btn btn-primary float-right" (click)="linkOrcid()"> class="btn btn-primary float-right" (click)="linkOrcid()">
<span><i class="fas fa-check"></i> {{ 'person.page.orcid.grant-authorizations' | translate }}</span> <span><i class="fas fa-check"></i> {{ 'person.page.orcid.grant-authorizations' | translate }}</span>
</button> </button>
@@ -68,7 +68,7 @@
<div class="row"> <div class="row">
<div class="col-2"><img alt="orcid-logo" src="../../../../assets/images/orcid.logo.icon.svg"/></div> <div class="col-2"><img alt="orcid-logo" src="../../../../assets/images/orcid.logo.icon.svg"/></div>
<div class="col"> <div class="col">
<ds-alert [type]="'alert-info'">{{ getOrcidNotLinkedMessage() | async }}</ds-alert> <ds-alert [type]="AlertType.Info">{{ getOrcidNotLinkedMessage() }}</ds-alert>
</div> </div>
</div> </div>
<div class="row"> <div class="row">

View File

@@ -12,6 +12,7 @@ import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service'; import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
import { createFailedRemoteDataObject } from '../../../shared/remote-data.utils'; import { createFailedRemoteDataObject } from '../../../shared/remote-data.utils';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { AlertType } from '../../../shared/alert/alert-type';
@Component({ @Component({
selector: 'ds-orcid-auth', selector: 'ds-orcid-auth',
@@ -28,43 +29,49 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
/** /**
* The list of exposed orcid authorization scopes for the orcid profile * The list of exposed orcid authorization scopes for the orcid profile
*/ */
profileAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]); profileAuthorizationScopes$: BehaviorSubject<string[]> = new BehaviorSubject([]);
hasOrcidAuthorizations$: Observable<boolean>;
/** /**
* The list of all orcid authorization scopes missing in the orcid profile * The list of all orcid authorization scopes missing in the orcid profile
*/ */
missingAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]); missingAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject([]);
hasMissingOrcidAuthorizations$: Observable<boolean>;
/** /**
* The list of all orcid authorization scopes available * The list of all orcid authorization scopes available
*/ */
orcidAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]); orcidAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject([]);
/** /**
* A boolean representing if unlink operation is processing * A boolean representing if unlink operation is processing
*/ */
unlinkProcessing: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); unlinkProcessing: BehaviorSubject<boolean> = new BehaviorSubject(false);
/** /**
* A boolean representing if orcid profile is linked * A boolean representing if orcid profile is linked
*/ */
private isOrcidLinked$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); isOrcidLinked$: BehaviorSubject<boolean> = new BehaviorSubject(false);
/** /**
* A boolean representing if only admin can disconnect orcid profile * A boolean representing if only admin can disconnect orcid profile
*/ */
private onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject(false);
/** /**
* A boolean representing if owner can disconnect orcid profile * A boolean representing if owner can disconnect orcid profile
*/ */
private ownerCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); ownerCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject(false);
/** /**
* An event emitted when orcid profile is unliked successfully * An event emitted when orcid profile is unliked successfully
*/ */
@Output() unlink: EventEmitter<void> = new EventEmitter<void>(); @Output() unlink: EventEmitter<void> = new EventEmitter<void>();
readonly AlertType = AlertType;
constructor( constructor(
private orcidAuthService: OrcidAuthService, private orcidAuthService: OrcidAuthService,
private translateService: TranslateService, private translateService: TranslateService,
@@ -78,6 +85,8 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
this.orcidAuthorizationScopes.next(scopes); this.orcidAuthorizationScopes.next(scopes);
this.initOrcidAuthSettings(); this.initOrcidAuthSettings();
}); });
this.hasOrcidAuthorizations$ = this.hasOrcidAuthorizations();
this.hasMissingOrcidAuthorizations$ = this.hasMissingOrcidAuthorizations();
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
@@ -90,18 +99,11 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
* Check if the list of exposed orcid authorization scopes for the orcid profile has values * Check if the list of exposed orcid authorization scopes for the orcid profile has values
*/ */
hasOrcidAuthorizations(): Observable<boolean> { hasOrcidAuthorizations(): Observable<boolean> {
return this.profileAuthorizationScopes.asObservable().pipe( return this.profileAuthorizationScopes$.pipe(
map((scopes: string[]) => scopes.length > 0) map((scopes: string[]) => scopes.length > 0)
); );
} }
/**
* Return the list of exposed orcid authorization scopes for the orcid profile
*/
getOrcidAuthorizations(): Observable<string[]> {
return this.profileAuthorizationScopes.asObservable();
}
/** /**
* Check if the list of exposed orcid authorization scopes for the orcid profile has values * Check if the list of exposed orcid authorization scopes for the orcid profile has values
*/ */
@@ -111,26 +113,12 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
); );
} }
/** getOrcidNotLinkedMessage(): string {
* Return the list of exposed orcid authorization scopes for the orcid profile
*/
getMissingOrcidAuthorizations(): Observable<string[]> {
return this.profileAuthorizationScopes.asObservable();
}
/**
* Return a boolean representing if orcid profile is linked
*/
isLinkedToOrcid(): Observable<boolean> {
return this.isOrcidLinked$.asObservable();
}
getOrcidNotLinkedMessage(): Observable<string> {
const orcid = this.item.firstMetadataValue('person.identifier.orcid'); const orcid = this.item.firstMetadataValue('person.identifier.orcid');
if (orcid) { if (orcid) {
return this.translateService.get('person.page.orcid.orcid-not-linked-message', { 'orcid': orcid }); return this.translateService.instant('person.page.orcid.orcid-not-linked-message', { 'orcid': orcid });
} else { } else {
return this.translateService.get('person.page.orcid.no-orcid-message'); return this.translateService.instant('person.page.orcid.no-orcid-message');
} }
} }
@@ -143,13 +131,6 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
return 'person.page.orcid.scope.' + scope.substring(1).replace('/', '-'); return 'person.page.orcid.scope.' + scope.substring(1).replace('/', '-');
} }
/**
* Return a boolean representing if only admin can disconnect orcid profile
*/
onlyAdminCanDisconnectProfileFromOrcid(): Observable<boolean> {
return this.onlyAdminCanDisconnectProfileFromOrcid$.asObservable();
}
/** /**
* Return a boolean representing if owner can disconnect orcid profile * Return a boolean representing if owner can disconnect orcid profile
*/ */
@@ -215,7 +196,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
} }
private setOrcidAuthorizationsFromItem(): void { private setOrcidAuthorizationsFromItem(): void {
this.profileAuthorizationScopes.next(this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item)); this.profileAuthorizationScopes$.next(this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item));
} }
} }

View File

@@ -3,13 +3,13 @@
<div class="container"> <div class="container">
<h2>{{ 'person.orcid.registry.queue' | translate }}</h2> <h2>{{ 'person.orcid.registry.queue' | translate }}</h2>
<ds-alert *ngIf="!(processing$ | async) && (getList() | async)?.payload?.totalElements == 0" <ds-alert *ngIf="!(processing$ | async) && (list$ | async)?.payload?.totalElements == 0"
[type]="AlertTypeEnum.Info"> [type]="AlertTypeEnum.Info">
{{ 'person.page.orcid.sync-queue.empty-message' | translate}} {{ 'person.page.orcid.sync-queue.empty-message' | translate}}
</ds-alert> </ds-alert>
<ds-pagination *ngIf="!(processing$ | async) && (getList() | async)?.payload?.totalElements > 0" <ds-pagination *ngIf="!(processing$ | async) && (list$ | async)?.payload?.totalElements > 0"
[paginationOptions]="paginationOptions" [paginationOptions]="paginationOptions"
[collectionSize]="(getList() | async)?.payload?.totalElements" [collectionSize]="(list$ | async)?.payload?.totalElements"
[retainScrollPosition]="false" [hideGear]="true" (paginationChange)="updateList()"> [retainScrollPosition]="false" [hideGear]="true" (paginationChange)="updateList()">
<div class="table-responsive"> <div class="table-responsive">
@@ -22,7 +22,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let entry of (getList() | async)?.payload?.page" data-test="orcidQueueElementRow"> <tr *ngFor="let entry of (list$ | async)?.payload?.page" data-test="orcidQueueElementRow">
<td style="width: 15%" class="text-center align-middle"> <td style="width: 15%" class="text-center align-middle">
<i [ngClass]="getIconClass(entry)" [ngbTooltip]="getIconTooltip(entry) | translate" <i [ngClass]="getIconClass(entry)" [ngbTooltip]="getIconTooltip(entry) | translate"
class="fa-2x" aria-hidden="true"></i> class="fa-2x" aria-hidden="true"></i>

View File

@@ -47,13 +47,12 @@ export class OrcidQueueComponent implements OnInit, OnDestroy {
/** /**
* A list of orcid queue records * A list of orcid queue records
*/ */
private list$: BehaviorSubject<RemoteData<PaginatedList<OrcidQueue>>> = new BehaviorSubject<RemoteData<PaginatedList<OrcidQueue>>>({} as any); list$: BehaviorSubject<RemoteData<PaginatedList<OrcidQueue>>> = new BehaviorSubject<RemoteData<PaginatedList<OrcidQueue>>>({} as any);
/** /**
* The AlertType enumeration * The AlertType enumeration
* @type {AlertType}
*/ */
AlertTypeEnum = AlertType; readonly AlertTypeEnum = AlertType;
/** /**
* Array to track all subscriptions and unsubscribe them onDestroy * Array to track all subscriptions and unsubscribe them onDestroy
@@ -99,13 +98,6 @@ export class OrcidQueueComponent implements OnInit, OnDestroy {
); );
} }
/**
* Return the list of orcid queue records
*/
getList(): Observable<RemoteData<PaginatedList<OrcidQueue>>> {
return this.list$.asObservable();
}
/** /**
* Return the icon class for the queue object type * Return the icon class for the queue object type
* *

View File

@@ -3,7 +3,6 @@
<div *ngIf="itemRD?.payload as item"> <div *ngIf="itemRD?.payload as item">
<ds-themed-item-alerts [item]="item"></ds-themed-item-alerts> <ds-themed-item-alerts [item]="item"></ds-themed-item-alerts>
<ds-item-versions-notice [item]="item"></ds-item-versions-notice> <ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker>
<ds-listable-object-component-loader *ngIf="!item.isWithdrawn || (isAdmin$|async)" [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader> <ds-listable-object-component-loader *ngIf="!item.isWithdrawn || (isAdmin$|async)" [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
<ds-item-versions class="mt-2" [item]="item" [displayActions]="false"></ds-item-versions> <ds-item-versions class="mt-2" [item]="item" [displayActions]="false"></ds-item-versions>
</div> </div>

View File

@@ -85,7 +85,7 @@
</ds-item-page-uri-field> </ds-item-page-uri-field>
<ds-item-page-collections [item]="object"></ds-item-page-collections> <ds-item-page-collections [item]="object"></ds-item-page-collections>
<div> <div>
<a class="btn btn-outline-primary" role="button" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" role="button" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
<i class="fas fa-info-circle"></i> {{"item.page.link.full" | translate}} <i class="fas fa-info-circle"></i> {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -71,7 +71,7 @@
</ds-item-page-uri-field> </ds-item-page-uri-field>
<ds-item-page-collections [item]="object"></ds-item-page-collections> <ds-item-page-collections [item]="object"></ds-item-page-collections>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
<i class="fas fa-info-circle"></i> {{"item.page.link.full" | translate}} <i class="fas fa-info-circle"></i> {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -3,6 +3,6 @@
<h2><small><em>{{missingItem}}</em></small></h2> <h2><small><em>{{missingItem}}</em></small></h2>
<br /> <br />
<p class="text-center"> <p class="text-center">
<a routerLink="/home" class="btn btn-primary">{{"404.link.home-page" | translate}}</a> <a routerLink="/home" class="btn btn-primary" role="link" tabindex="0">{{"404.link.home-page" | translate}}</a>
</p> </p>
</div> </div>

View File

@@ -1,5 +1,5 @@
<nav [ngClass]="{'open': !(menuCollapsed | async)}" <nav [ngClass]="{'open': !(menuCollapsed | async)}"
[@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')" [@slideMobileNav]="(isMobile$ | async) !== true ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
class="navbar navbar-light navbar-expand-md px-md-0 pt-md-0 pt-3 navbar-container" role="navigation" class="navbar navbar-light navbar-expand-md px-md-0 pt-md-0 pt-3 navbar-container" role="navigation"
[attr.aria-label]="'nav.main.description' | translate" id="main-navbar"> [attr.aria-label]="'nav.main.description' | translate" id="main-navbar">
<!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed --> <!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed -->

View File

@@ -5,6 +5,6 @@
<p>{{"error-page." + code | translate}}</p> <p>{{"error-page." + code | translate}}</p>
<br/> <br/>
<p class="text-center"> <p class="text-center">
<a href="/home" class="btn btn-primary">{{ status + ".link.home-page" | translate}}</a> <a href="/home" class="btn btn-primary" role="link" tabindex="0">{{ status + ".link.home-page" | translate}}</a>
</p> </p>
</div> </div>

View File

@@ -5,6 +5,6 @@
<p>{{"404.help" | translate}}</p> <p>{{"404.help" | translate}}</p>
<br/> <br/>
<p class="text-center"> <p class="text-center">
<a routerLink="/home" class="btn btn-primary">{{"404.link.home-page" | translate}}</a> <a routerLink="/home" class="btn btn-primary" role="link" tabindex="0">{{"404.link.home-page" | translate}}</a>
</p> </p>
</div> </div>

View File

@@ -71,16 +71,16 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div *ngIf="!(processBulkDeleteService.isProcessing$() |async)">{{'process.overview.delete.body' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</div> <div *ngIf="(isProcessing$ | async) !== true">{{'process.overview.delete.body' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</div>
<div *ngIf="processBulkDeleteService.isProcessing$() |async" class="alert alert-info"> <div *ngIf="(isProcessing$ | async) === true" class="alert alert-info">
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
<span> {{ 'process.overview.delete.processing' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</span> <span> {{ 'process.overview.delete.processing' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</span>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<button class="btn btn-primary mr-2" [dsBtnDisabled]="processBulkDeleteService.isProcessing$() |async" <button class="btn btn-primary mr-2" [dsBtnDisabled]="(isProcessing$ | async) === true"
(click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button> (click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
<button id="delete-confirm" class="btn btn-danger" <button id="delete-confirm" class="btn btn-danger"
[dsBtnDisabled]="processBulkDeleteService.isProcessing$() |async" [dsBtnDisabled]="(isProcessing$ | async) === true"
(click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }} (click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
</button> </button>
</div> </div>

View File

@@ -51,11 +51,12 @@ export class ProcessOverviewComponent implements OnInit, OnDestroy {
*/ */
dateFormat = 'yyyy-MM-dd HH:mm:ss'; dateFormat = 'yyyy-MM-dd HH:mm:ss';
processesToDelete: string[] = [];
private modalRef: any; private modalRef: any;
isProcessingSub: Subscription; isProcessingSub: Subscription;
isProcessing$: Observable<boolean>;
constructor(protected processService: ProcessDataService, constructor(protected processService: ProcessDataService,
protected paginationService: PaginationService, protected paginationService: PaginationService,
protected ePersonService: EPersonDataService, protected ePersonService: EPersonDataService,
@@ -69,6 +70,7 @@ export class ProcessOverviewComponent implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
this.setProcesses(); this.setProcesses();
this.processBulkDeleteService.clearAllProcesses(); this.processBulkDeleteService.clearAllProcesses();
this.isProcessing$ = this.processBulkDeleteService.isProcessing$();
} }
/** /**

View File

@@ -13,12 +13,12 @@
<p>{{'researcher.profile.not.associated' | translate}}</p> <p>{{'researcher.profile.not.associated' | translate}}</p>
</div> </div>
<button *ngIf="!researcherProfile" class="btn btn-primary mr-2" <button *ngIf="!researcherProfile" class="btn btn-primary mr-2"
[dsBtnDisabled]="(isProcessingCreate() | async)" [dsBtnDisabled]="(processingCreate$ | async) === true"
(click)="createProfile()"> (click)="createProfile()">
<span *ngIf="(isProcessingCreate() | async)"> <span *ngIf="(processingCreate$ | async) === true">
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}} <i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
</span> </span>
<span *ngIf="!(isProcessingCreate() | async)"> <span *ngIf="(processingCreate$ | async) !== true">
<i class="fas fa-plus"></i> &nbsp;{{'researcher.profile.create.new' | translate}} <i class="fas fa-plus"></i> &nbsp;{{'researcher.profile.create.new' | translate}}
</span> </span>
</button> </button>
@@ -27,10 +27,10 @@
<i class="fas fa-info-circle"></i> {{'researcher.profile.view' | translate}} <i class="fas fa-info-circle"></i> {{'researcher.profile.view' | translate}}
</button> </button>
<button class="btn btn-danger" [dsBtnDisabled]="!researcherProfile" (click)="deleteProfile(researcherProfile)"> <button class="btn btn-danger" [dsBtnDisabled]="!researcherProfile" (click)="deleteProfile(researcherProfile)">
<span *ngIf="(isProcessingDelete() | async)"> <span *ngIf="(processingDelete$ | async) === true">
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}} <i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
</span> </span>
<span *ngIf="!(isProcessingDelete() | async)"> <span *ngIf="(processingDelete$ | async) !== true">
<i class="fas fa-trash-alt"></i> &nbsp;{{'researcher.profile.delete' | translate}} <i class="fas fa-trash-alt"></i> &nbsp;{{'researcher.profile.delete' | translate}}
</span> </span>
</button> </button>

View File

@@ -3,7 +3,7 @@ import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { map, mergeMap, switchMap, take, tap } from 'rxjs/operators'; import { map, mergeMap, switchMap, take, tap } from 'rxjs/operators';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
@@ -157,24 +157,6 @@ export class ProfilePageResearcherFormComponent implements OnInit {
}); });
} }
/**
* Return a boolean representing if a delete operation is pending.
*
* @return {Observable<boolean>}
*/
isProcessingDelete(): Observable<boolean> {
return this.processingDelete$.asObservable();
}
/**
* Return a boolean representing if a create operation is pending.
*
* @return {Observable<boolean>}
*/
isProcessingCreate(): Observable<boolean> {
return this.processingCreate$.asObservable();
}
/** /**
* Create a new profile related to the current user from scratch. * Create a new profile related to the current user from scratch.
*/ */

View File

@@ -44,13 +44,13 @@
<p class="m-0"><a href="javascript:void(0);" (click)="this.klaroService.showSettings()">{{ MESSAGE_PREFIX + '.google-recaptcha.open-cookie-settings' | translate }}</a></p> <p class="m-0"><a href="javascript:void(0);" (click)="this.klaroService.showSettings()">{{ MESSAGE_PREFIX + '.google-recaptcha.open-cookie-settings' | translate }}</a></p>
</ds-alert> </ds-alert>
<div class="my-3" *ngIf="isRecaptchaCookieAccepted() && (googleRecaptchaService.captchaVersion() | async) === 'v2'"> <div class="my-3" *ngIf="isRecaptchaCookieAccepted() && (captchaVersion$ | async) === 'v2'">
<ds-google-recaptcha [captchaMode]="(googleRecaptchaService.captchaMode() | async)" <ds-google-recaptcha [captchaMode]="(captchaMode$ | async)"
(executeRecaptcha)="register($event)" (checkboxChecked)="onCheckboxChecked($event)" (executeRecaptcha)="register($event)" (checkboxChecked)="onCheckboxChecked($event)"
(showNotification)="showNotification($event)"></ds-google-recaptcha> (showNotification)="showNotification($event)"></ds-google-recaptcha>
</div> </div>
<ng-container *ngIf="!((googleRecaptchaService.captchaVersion() | async) === 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible'); else v2Invisible"> <ng-container *ngIf="!((captchaVersion$ | async) === 'v2' && (captchaMode$ | async) === 'invisible'); else v2Invisible">
<button class="btn btn-primary" [dsBtnDisabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()"> <button class="btn btn-primary" [dsBtnDisabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()">
{{ MESSAGE_PREFIX + '.submit' | translate }} {{ MESSAGE_PREFIX + '.submit' | translate }}
</button> </button>

View File

@@ -66,13 +66,9 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit {
subscriptions: Subscription[] = []; subscriptions: Subscription[] = [];
captchaVersion(): Observable<string> { captchaVersion$: Observable<string>;
return this.googleRecaptchaService.captchaVersion();
}
captchaMode(): Observable<string> { captchaMode$: Observable<string>;
return this.googleRecaptchaService.captchaMode();
}
constructor( constructor(
private epersonRegistrationService: EpersonRegistrationService, private epersonRegistrationService: EpersonRegistrationService,
@@ -94,6 +90,8 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.captchaVersion$ = this.googleRecaptchaService.captchaVersion();
this.captchaMode$ = this.googleRecaptchaService.captchaMode();
const validators: ValidatorFn[] = [ const validators: ValidatorFn[] = [
Validators.required, Validators.required,
Validators.email, Validators.email,
@@ -150,7 +148,7 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit {
register(tokenV2?) { register(tokenV2?) {
if (!this.form.invalid) { if (!this.form.invalid) {
if (this.registrationVerification) { if (this.registrationVerification) {
this.subscriptions.push(combineLatest([this.captchaVersion(), this.captchaMode()]).pipe( this.subscriptions.push(combineLatest([this.captchaVersion$, this.captchaMode$]).pipe(
switchMap(([captchaVersion, captchaMode]) => { switchMap(([captchaVersion, captchaMode]) => {
if (captchaVersion === 'v3') { if (captchaVersion === 'v3') {
return this.googleRecaptchaService.getRecaptchaToken('register_email'); return this.googleRecaptchaService.getRecaptchaToken('register_email');
@@ -213,7 +211,7 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit {
*/ */
disableUntilCheckedFcn(): Observable<boolean> { disableUntilCheckedFcn(): Observable<boolean> {
const checked$ = this.checkboxCheckedSubject$.asObservable(); const checked$ = this.checkboxCheckedSubject$.asObservable();
return combineLatest([this.captchaVersion(), this.captchaMode(), checked$]).pipe( return combineLatest([this.captchaVersion$, this.captchaMode$, checked$]).pipe(
// disable if checkbox is not checked or if reCaptcha is not in v2 checkbox mode // disable if checkbox is not checked or if reCaptcha is not in v2 checkbox mode
switchMap(([captchaVersion, captchaMode, checked]) => captchaVersion === 'v2' && captchaMode === 'checkbox' ? of(!checked) : of(false)), switchMap(([captchaVersion, captchaMode, checked]) => captchaVersion === 'v2' && captchaMode === 'checkbox' ? of(!checked) : of(false)),
startWith(true), startWith(true),

View File

@@ -8,13 +8,15 @@
<div class="btn-group "> <div class="btn-group ">
<a [routerLink]="grantRoute$ | async" <a [routerLink]="grantRoute$ | async"
class="btn btn-outline-primary" class="btn btn-outline-primary"
title="{{'grant-deny-request-copy.grant' | translate }}"> title="{{'grant-deny-request-copy.grant' | translate }}"
role="button" tabindex="0">
{{'grant-deny-request-copy.grant' | translate }} {{'grant-deny-request-copy.grant' | translate }}
</a> </a>
<a [routerLink]="denyRoute$ | async" <a [routerLink]="denyRoute$ | async"
class="btn btn-outline-danger" class="btn btn-outline-danger"
title="{{'grant-deny-request-copy.deny' | translate }}"> title="{{'grant-deny-request-copy.deny' | translate }}"
role="button" tabindex="0">
{{'grant-deny-request-copy.deny' | translate }} {{'grant-deny-request-copy.deny' | translate }}
</a> </a>
</div> </div>
@@ -22,7 +24,7 @@
<div *ngIf="itemRequestRD.payload.decisionDate" class="processed-message"> <div *ngIf="itemRequestRD.payload.decisionDate" class="processed-message">
<p>{{'grant-deny-request-copy.processed' | translate}}</p> <p>{{'grant-deny-request-copy.processed' | translate}}</p>
<p class="text-center"> <p class="text-center">
<a routerLink="/home" class="btn btn-primary">{{'grant-deny-request-copy.home-page' | translate}}</a> <a routerLink="/home" class="btn btn-primary" role="link" tabindex="0">{{'grant-deny-request-copy.home-page' | translate}}</a>
</p> </p>
</div> </div>
</div> </div>

View File

@@ -7,7 +7,7 @@
[class.display]="searchExpanded ? 'inline-block' : 'none'" [class.display]="searchExpanded ? 'inline-block' : 'none'"
[tabIndex]="searchExpanded ? 0 : -1" [tabIndex]="searchExpanded ? 0 : -1"
[attr.data-test]="'header-search-box' | dsBrowserOnly"> [attr.data-test]="'header-search-box' | dsBrowserOnly">
<button class="submit-icon btn btn-link btn-link-inline" [attr.aria-label]="'nav.search.button' | translate" type="button" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" [attr.data-test]="'header-search-icon' | dsBrowserOnly"> <button class="submit-icon btn btn-link btn-link-inline" [attr.aria-label]="'nav.search.button' | translate" type="button" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" [attr.data-test]="'header-search-icon' | dsBrowserOnly" role="button" tabindex="0">
<em class="fas fa-search fa-lg fa-fw"></em> <em class="fas fa-search fa-lg fa-fw"></em>
</button> </button>
</form> </form>

View File

@@ -119,6 +119,10 @@ export class AccessControlArrayFormComponent implements OnInit {
return item.id; return item.id;
} }
isValid() {
return this.ngForm.valid;
}
} }

View File

@@ -156,5 +156,9 @@ export class AccessControlFormContainerComponent<T extends DSpaceObject> impleme
this.selectableListService.deselectAll(ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID); this.selectableListService.deselectAll(ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID);
} }
isValid() {
return this.bitstreamAccessCmp.isValid() || this.itemAccessCmp.isValid();
}
} }

View File

@@ -8,20 +8,19 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<ng-container *ngIf="data$ | async as data"> <ng-container *ngIf="bitstreams$ | async as bitstreams">
<ds-viewable-collection <ds-viewable-collection
*ngIf="data.payload.page.length > 0" *ngIf="bitstreams.payload?.page?.length > 0"
[config]="paginationConfig" [config]="paginationConfig"
[context]="context" [context]="context"
[objects]="data" [objects]="bitstreams"
[selectable]="true" [selectable]="true"
[selectionConfig]="{ repeatable: true, listId: LIST_ID }" [selectionConfig]="{ repeatable: true, listId: LIST_ID }"
[showPaginator]="true" [showPaginator]="true">
(pageChange)="loadForPage($event)">
</ds-viewable-collection> </ds-viewable-collection>
<div *ngIf="data && data.payload.page.length === 0" <div *ngIf="bitstreams && bitstreams.payload?.page?.length === 0"
class="alert alert-info w-100" role="alert"> class="alert alert-info w-100" role="alert">
{{'access-control-select-bitstreams-modal.no-items' | translate}} {{'access-control-select-bitstreams-modal.no-items' | translate}}
</div> </div>

View File

@@ -1,6 +1,6 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { BehaviorSubject } from 'rxjs'; import { Observable } from 'rxjs';
import { PaginatedList } from 'src/app/core/data/paginated-list.model'; import { PaginatedList } from 'src/app/core/data/paginated-list.model';
import { RemoteData } from 'src/app/core/data/remote-data'; import { RemoteData } from 'src/app/core/data/remote-data';
import { Bitstream } from 'src/app/core/shared/bitstream.model'; import { Bitstream } from 'src/app/core/shared/bitstream.model';
@@ -10,8 +10,7 @@ import { Item } from '../../../core/shared/item.model';
import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { hasValue } from '../../empty.util'; import { switchMap } from 'rxjs/operators';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
export const ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID = 'item-access-control-select-bitstreams'; export const ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID = 'item-access-control-select-bitstreams';
@@ -20,19 +19,22 @@ export const ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID = 'item-access-contro
templateUrl: './item-access-control-select-bitstreams-modal.component.html', templateUrl: './item-access-control-select-bitstreams-modal.component.html',
styleUrls: [ './item-access-control-select-bitstreams-modal.component.scss' ] styleUrls: [ './item-access-control-select-bitstreams-modal.component.scss' ]
}) })
export class ItemAccessControlSelectBitstreamsModalComponent implements OnInit { export class ItemAccessControlSelectBitstreamsModalComponent implements OnInit, OnDestroy {
LIST_ID = ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID; LIST_ID = ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID;
@Input() item!: Item; @Input() item!: Item;
@Input() selectedBitstreams: string[] = []; @Input() selectedBitstreams: string[] = [];
data$ = new BehaviorSubject<RemoteData<PaginatedList<Bitstream>> | null>(null); bitstreams$: Observable<RemoteData<PaginatedList<Bitstream>>>;
paginationConfig: PaginationComponentOptions;
pageSize = 5;
context: Context = Context.Bitstream; context: Context = Context.Bitstream;
paginationConfig = Object.assign(new PaginationComponentOptions(), {
id: 'iacsbm',
currentPage: 1,
pageSize: 5
});
constructor( constructor(
private bitstreamService: BitstreamDataService, private bitstreamService: BitstreamDataService,
protected paginationService: PaginationService, protected paginationService: PaginationService,
@@ -40,23 +42,20 @@ export class ItemAccessControlSelectBitstreamsModalComponent implements OnInit {
public activeModal: NgbActiveModal public activeModal: NgbActiveModal
) { } ) { }
ngOnInit() { ngOnInit(): void {
this.loadForPage(1); this.bitstreams$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe(
switchMap((options: PaginationComponentOptions) => this.bitstreamService.findAllByItemAndBundleName(
this.paginationConfig = new PaginationComponentOptions(); this.item,
this.paginationConfig.id = 'iacsbm'; 'ORIGINAL',
this.paginationConfig.currentPage = 1; { elementsPerPage: options.pageSize, currentPage: options.currentPage },
if (hasValue(this.pageSize)) { true,
this.paginationConfig.pageSize = this.pageSize; true,
} ))
);
} }
loadForPage(page: number) { ngOnDestroy(): void {
this.bitstreamService.findAllByItemAndBundleName(this.item, 'ORIGINAL', { currentPage: page}, false) this.paginationService.clearPagination(this.paginationConfig.id);
.pipe(
getFirstCompletedRemoteData(),
)
.subscribe(this.data$);
} }
} }

View File

@@ -6,6 +6,7 @@
<a href="javascript:void(0);" class="dropdownLogin px-0.5" [attr.aria-label]="'nav.login' |translate" <a href="javascript:void(0);" class="dropdownLogin px-0.5" [attr.aria-label]="'nav.login' |translate"
(click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly" (click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly"
role="menuitem" role="menuitem"
tabindex="0"
aria-haspopup="menu" aria-haspopup="menu"
aria-controls="loginDropdownMenu" aria-controls="loginDropdownMenu"
[attr.aria-expanded]="loginDrop.isOpen()" [attr.aria-expanded]="loginDrop.isOpen()"
@@ -22,6 +23,7 @@
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut> <div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="javascript:void(0);" <a href="javascript:void(0);"
role="menuitem" role="menuitem"
tabindex="0"
[attr.aria-label]="'nav.user-profile-menu-and-logout' | translate" [attr.aria-label]="'nav.user-profile-menu-and-logout' | translate"
aria-controls="user-menu-dropdown" aria-controls="user-menu-dropdown"
(click)="$event.preventDefault()" [title]="'nav.user-profile-menu-and-logout' | translate" (click)="$event.preventDefault()" [title]="'nav.user-profile-menu-and-logout' | translate"
@@ -39,10 +41,10 @@
<ng-template #mobileButtons> <ng-template #mobileButtons>
<div data-test="auth-nav"> <div data-test="auth-nav">
<a *ngIf="!(isAuthenticated | async)" routerLink="/login" routerLinkActive="active" class="loginLink px-0.5" role="button"> <a *ngIf="!(isAuthenticated | async)" routerLink="/login" routerLinkActive="active" class="loginLink px-0.5" role="button" tabindex="0">
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span> {{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
</a> </a>
<a *ngIf="(isAuthenticated | async)" role="button" [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="logoutLink px-1"> <a *ngIf="(isAuthenticated | async)" role="button" [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="logoutLink px-1" tabindex="0">
<i class="fas fa-sign-out-alt fa-lg fa-fw"></i> <i class="fas fa-sign-out-alt fa-lg fa-fw"></i>
<span class="sr-only">(current)</span> <span class="sr-only">(current)</span>
</a> </a>

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