From e47b42bc89e0c34271e1a9fac5f9bbf08f77720f Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Mon, 7 Nov 2022 20:56:05 +0530 Subject: [PATCH 01/30] [CST-7216] Angular: Import saf via URL --- .../batch-import-page.component.html | 12 +++ .../batch-import-page.component.spec.ts | 87 ++++++++++++++++++- .../batch-import-page.component.ts | 29 ++++++- src/app/admin/admin.module.ts | 4 +- src/assets/i18n/en.json5 | 6 ++ 5 files changed, 130 insertions(+), 8 deletions(-) diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.html b/src/app/admin/admin-import-batch-page/batch-import-page.component.html index dbc8c74437..190eb0d409 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.html +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.html @@ -20,12 +20,24 @@ + + +
+ +
+
diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts index 36ba1137c9..2c465d3f3d 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts @@ -86,10 +86,18 @@ describe('BatchImportPageComponent', () => { let fileMock: File; beforeEach(() => { + component.isUpload = true; fileMock = new File([''], 'filename.zip', { type: 'application/zip' }); component.setFile(fileMock); }); + it('should show the file dropzone', () => { + const fileDropzone = fixture.debugElement.query(By.css('[data-test="file-dropzone"]')); + const fileUrlInput = fixture.debugElement.query(By.css('[data-test="file-url-input"]')); + expect(fileDropzone).toBeTruthy(); + expect(fileUrlInput).toBeFalsy(); + }); + describe('if proceed button is pressed without validate only', () => { beforeEach(fakeAsync(() => { component.validateOnly = false; @@ -99,9 +107,9 @@ describe('BatchImportPageComponent', () => { })); it('metadata-import script is invoked with --zip fileName and the mockFile', () => { const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), + Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }) ]; - parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add' })); expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); }); it('success notification is shown', () => { @@ -121,8 +129,8 @@ describe('BatchImportPageComponent', () => { })); it('metadata-import script is invoked with --zip fileName and the mockFile and -v validate-only', () => { const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), Object.assign(new ProcessParameter(), { name: '-v', value: true }), ]; expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); @@ -148,4 +156,77 @@ describe('BatchImportPageComponent', () => { }); }); }); + + describe('if url is set', () => { + beforeEach(fakeAsync(() => { + component.isUpload = false; + component.fileURL = 'example.fileURL.com'; + fixture.detectChanges(); + })); + + it('should show the file url input', () => { + const fileDropzone = fixture.debugElement.query(By.css('[data-test="file-dropzone"]')); + const fileUrlInput = fixture.debugElement.query(By.css('[data-test="file-url-input"]')); + expect(fileDropzone).toBeFalsy(); + expect(fileUrlInput).toBeTruthy(); + }); + + describe('if proceed button is pressed without validate only', () => { + beforeEach(fakeAsync(() => { + component.validateOnly = false; + const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; + proceed.click(); + fixture.detectChanges(); + })); + it('metadata-import script is invoked with --u and the file url', () => { + const parameterValues: ProcessParameter[] = [ + Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--u', value: 'example.fileURL.com' }) + ]; + expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [null]); + }); + it('success notification is shown', () => { + expect(notificationService.success).toHaveBeenCalled(); + }); + it('redirected to process page', () => { + expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46'); + }); + }); + + describe('if proceed button is pressed with validate only', () => { + beforeEach(fakeAsync(() => { + component.validateOnly = true; + const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; + proceed.click(); + fixture.detectChanges(); + })); + it('metadata-import script is invoked with --u and the file url and -v validate-only', () => { + const parameterValues: ProcessParameter[] = [ + Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--u', value: 'example.fileURL.com' }), + Object.assign(new ProcessParameter(), { name: '-v', value: true }), + ]; + expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [null]); + }); + it('success notification is shown', () => { + expect(notificationService.success).toHaveBeenCalled(); + }); + it('redirected to process page', () => { + expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46'); + }); + }); + + describe('if proceed is pressed; but script invoke fails', () => { + beforeEach(fakeAsync(() => { + jasmine.getEnv().allowRespy(true); + spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500)); + const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; + proceed.click(); + fixture.detectChanges(); + })); + it('error notification is shown', () => { + expect(notificationService.error).toHaveBeenCalled(); + }); + }); + }); }); diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts index 7171c67585..79da641cc6 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts @@ -8,7 +8,7 @@ import { ProcessParameter } from '../../process-page/processes/process-parameter import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { RemoteData } from '../../core/data/remote-data'; import { Process } from '../../process-page/processes/process.model'; -import { isNotEmpty } from '../../shared/empty.util'; +import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths'; import { ImportBatchSelectorComponent @@ -32,11 +32,22 @@ export class BatchImportPageComponent { * The validate only flag */ validateOnly = true; + /** * dso object for community or collection */ dso: DSpaceObject = null; + /** + * The flag between upload and url + */ + isUpload = true; + + /** + * File URL when flag is for url + */ + fileURL: string; + public constructor(private location: Location, protected translate: TranslateService, protected notificationsService: NotificationsService, @@ -72,13 +83,18 @@ export class BatchImportPageComponent { * Starts import-metadata script with --zip fileName (and the selected file) */ public importMetadata() { - if (this.fileObject == null) { + if (this.fileObject == null && isEmpty(this.fileURL)) { this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile')); } else { const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }), Object.assign(new ProcessParameter(), { name: '--add' }) ]; + if (this.isUpload) { + parameterValues.push(Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name })); + } else { + this.fileObject = null; + parameterValues.push(Object.assign(new ProcessParameter(), { name: '--u', value: this.fileURL })); + } if (this.dso) { parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid })); } @@ -121,4 +137,11 @@ export class BatchImportPageComponent { removeDspaceObject(): void { this.dso = null; } + + /** + * toggle the flag between upload and url + */ + toggleUpload() { + this.isUpload = !this.isUpload; + } } diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts index dff2e506c3..768e3120df 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -10,7 +10,7 @@ import { AdminSearchModule } from './admin-search-page/admin-search.module'; import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component'; import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; -import { UploadModule } from '../shared/upload/upload.module'; +import { UiSwitchModule } from 'ngx-ui-switch'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -27,7 +27,7 @@ const ENTRY_COMPONENTS = [ AdminSearchModule.withEntryComponents(), AdminWorkflowModuleModule.withEntryComponents(), SharedModule, - UploadModule, + UiSwitchModule ], declarations: [ AdminCurationTasksComponent, diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index fc4c6aa74d..5eb069c804 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -586,6 +586,12 @@ "admin.batch-import.page.error.addFile": "Select Zip file first!", + "admin.metadata-import.page.toggle.upload": "Upload", + + "admin.metadata-import.page.toggle.url": "URL", + + "admin.metadata-import.page.urlMsg": "Insert the batch ZIP url to import", + "admin.metadata-import.page.validateOnly": "Validate Only", "admin.metadata-import.page.validateOnly.hint": "When selected, the uploaded CSV will be validated. You will receive a report of detected changes, but no changes will be saved.", From 98da08ead0c0b8c197f0aed998c9a2368915f6a9 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Thu, 10 Nov 2022 19:31:32 +0530 Subject: [PATCH 02/30] [CST-7216] Design fixes and parameter changed --- .../batch-import-page.component.html | 7 ++++++- .../batch-import-page.component.spec.ts | 8 ++++---- .../batch-import-page.component.ts | 8 ++++++-- src/assets/i18n/en.json5 | 4 ++++ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.html b/src/app/admin/admin-import-batch-page/batch-import-page.component.html index 190eb0d409..1092443436 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.html +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.html @@ -20,10 +20,15 @@
- + + {{'admin.batch-import.page.toggle.help' | translate}} + + { proceed.click(); fixture.detectChanges(); })); - it('metadata-import script is invoked with --u and the file url', () => { + it('metadata-import script is invoked with --url and the file url', () => { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '--add' }), - Object.assign(new ProcessParameter(), { name: '--u', value: 'example.fileURL.com' }) + Object.assign(new ProcessParameter(), { name: '--url', value: 'example.fileURL.com' }) ]; expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [null]); }); @@ -200,10 +200,10 @@ describe('BatchImportPageComponent', () => { proceed.click(); fixture.detectChanges(); })); - it('metadata-import script is invoked with --u and the file url and -v validate-only', () => { + it('metadata-import script is invoked with --url and the file url and -v validate-only', () => { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '--add' }), - Object.assign(new ProcessParameter(), { name: '--u', value: 'example.fileURL.com' }), + Object.assign(new ProcessParameter(), { name: '--url', value: 'example.fileURL.com' }), Object.assign(new ProcessParameter(), { name: '-v', value: true }), ]; expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [null]); diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts index 79da641cc6..744b3aecce 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts @@ -84,7 +84,11 @@ export class BatchImportPageComponent { */ public importMetadata() { if (this.fileObject == null && isEmpty(this.fileURL)) { - this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile')); + if (this.isUpload) { + this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile')); + } else { + this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFileUrl')); + } } else { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '--add' }) @@ -93,7 +97,7 @@ export class BatchImportPageComponent { parameterValues.push(Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name })); } else { this.fileObject = null; - parameterValues.push(Object.assign(new ProcessParameter(), { name: '--u', value: this.fileURL })); + parameterValues.push(Object.assign(new ProcessParameter(), { name: '--url', value: this.fileURL })); } if (this.dso) { parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid })); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 5eb069c804..6d7ce0b884 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -568,6 +568,8 @@ "admin.batch-import.page.help": "Select the Collection to import into. Then, drop or browse to a Simple Archive Format (SAF) zip file that includes the Items to import", + "admin.batch-import.page.toggle.help": "It is possible to perform import either with file upload or via URL, use above toggle to set the input source", + "admin.metadata-import.page.dropMsg": "Drop a metadata CSV to import", "admin.batch-import.page.dropMsg": "Drop a batch ZIP to import", @@ -584,6 +586,8 @@ "admin.metadata-import.page.error.addFile": "Select file first!", + "admin.metadata-import.page.error.addFileUrl": "Insert file url first!", + "admin.batch-import.page.error.addFile": "Select Zip file first!", "admin.metadata-import.page.toggle.upload": "Upload", From b31fdf0be6c138f4d114eaa66abfb095cd8863e0 Mon Sep 17 00:00:00 2001 From: Enea Jahollari Date: Thu, 23 Mar 2023 11:59:50 +0100 Subject: [PATCH 03/30] [CST-7216] Imported UploadModule in AdminModule to fix build error --- src/app/admin/admin.module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts index 768e3120df..3dc0036854 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -11,6 +11,7 @@ import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-sect import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; import { UiSwitchModule } from 'ngx-ui-switch'; +import { UploadModule } from '../shared/upload/upload.module'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -27,7 +28,8 @@ const ENTRY_COMPONENTS = [ AdminSearchModule.withEntryComponents(), AdminWorkflowModuleModule.withEntryComponents(), SharedModule, - UiSwitchModule + UiSwitchModule, + UploadModule, ], declarations: [ AdminCurationTasksComponent, From 90a1f25ba9dce010f49c32b6b45ecd2db8fcfadc Mon Sep 17 00:00:00 2001 From: Alban Imami Date: Thu, 27 Apr 2023 16:51:41 +0200 Subject: [PATCH 04/30] Work for signposting --- server.ts | 9 +++ .../metadata-item.service.spec.ts | 16 +++++ .../metadata-item/metadata-item.service.ts | 70 +++++++++++++++++++ src/app/core/metadata/metadata.service.ts | 11 ++- src/app/init.service.ts | 3 + src/modules/app/browser-init.service.ts | 3 + src/modules/app/server-init.service.ts | 5 +- 7 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 src/app/core/metadata-item/metadata-item.service.spec.ts create mode 100644 src/app/core/metadata-item/metadata-item.service.ts diff --git a/server.ts b/server.ts index 3e10677a8b..5a3660e4de 100644 --- a/server.ts +++ b/server.ts @@ -180,6 +180,15 @@ export function app() { changeOrigin: true })); + /** + * Proxy the linksets + */ + router.use('/linksets**', createProxyMiddleware({ + target: `${environment.rest.baseUrl}/linksets`, + pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), + changeOrigin: true + })); + /** * Checks if the rateLimiter property is present * When it is present, the rateLimiter will be enabled. When it is undefined, the rateLimiter will be disabled. diff --git a/src/app/core/metadata-item/metadata-item.service.spec.ts b/src/app/core/metadata-item/metadata-item.service.spec.ts new file mode 100644 index 0000000000..89ca15658d --- /dev/null +++ b/src/app/core/metadata-item/metadata-item.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { MetadataItemService } from './metadata-item.service'; + +describe('MetadataItemService', () => { + let service: MetadataItemService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MetadataItemService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/metadata-item/metadata-item.service.ts b/src/app/core/metadata-item/metadata-item.service.ts new file mode 100644 index 0000000000..a4fbcf587b --- /dev/null +++ b/src/app/core/metadata-item/metadata-item.service.ts @@ -0,0 +1,70 @@ +import { Inject, Injectable } from '@angular/core'; +import { MetadataService } from '../metadata/metadata.service'; +import { ActivatedRoute, NavigationEnd, Event as NavigationEvent, NavigationStart, Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { Meta, Title } from '@angular/platform-browser'; +import { DSONameService } from '../breadcrumbs/dso-name.service'; +import { BundleDataService } from '../data/bundle-data.service'; +import { BitstreamDataService } from '../data/bitstream-data.service'; +import { BitstreamFormatDataService } from '../data/bitstream-format-data.service'; +import { RootDataService } from '../data/root-data.service'; +import { CoreState } from '../core-state.model'; +import { Store } from '@ngrx/store'; +import { HardRedirectService } from '../services/hard-redirect.service'; +import { APP_CONFIG, AppConfig } from 'src/config/app-config.interface'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { filter, map, switchMap, take, mergeMap } from 'rxjs/operators'; +import { DOCUMENT } from '@angular/common'; + +@Injectable({ + providedIn: 'root' +}) +export class MetadataItemService extends MetadataService { + + constructor( + private router1: ActivatedRoute, + router: Router, + translate: TranslateService, + meta: Meta, + title: Title, + dsoNameService: DSONameService, + bundleDataService: BundleDataService, + bitstreamDataService: BitstreamDataService, + bitstreamFormatDataService: BitstreamFormatDataService, + rootService: RootDataService, + store: Store, + hardRedirectService: HardRedirectService, + @Inject(APP_CONFIG) appConfig: AppConfig, + authorizationService: AuthorizationDataService, + @Inject(DOCUMENT) private document: Document + ) { + super(router, translate, meta, title, dsoNameService, bundleDataService, bitstreamDataService, bitstreamFormatDataService, rootService, store, hardRedirectService, appConfig, authorizationService); + } + + public checkCurrentRoute(){ + + console.log(this.router); + + this.router1.url.subscribe(url => { + console.log(url); + console.log(url[0].path); + }); + + // this.router.events.subscribe((event: NavigationEvent) => { + // if(event instanceof NavigationStart) { + // if(event.url.startsWith('/entities')){ + // console.log('We are on ENTITIES!'); + // } + // } + // }); + } + + setLinkTag(){ + this.clearMetaTags(); + + let link: HTMLLinkElement = this.document.createElement('link'); + link.setAttribute('rel', ''); + link.setAttribute('href', ''); + this.document.head.appendChild(link); + } +} diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 204c925e6b..c46f8b1d6e 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -84,7 +84,7 @@ export class MetadataService { ]; constructor( - private router: Router, + protected router: Router, private translate: TranslateService, private meta: Meta, private title: Title, @@ -363,6 +363,15 @@ export class MetadataService { } } + /** + * Add to the + */ + // private setLinkTag(): void { + // const value = this.getMetaTagValue('dc.link'); + // this.meta.addTag({ name: 'Link', content: value }); + // this.addMetaTag('Link', value); + // } + getBitLinkIfDownloadable(bitstream: Bitstream, bitstreamRd: RemoteData>): Observable { return observableOf(bitstream).pipe( getDownloadableBitstream(this.authorizationService), diff --git a/src/app/init.service.ts b/src/app/init.service.ts index 9fef2ca4ed..d5978d782d 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -24,6 +24,7 @@ import { isAuthenticationBlocking } from './core/auth/selectors'; import { distinctUntilChanged, find } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { MenuService } from './shared/menu/menu.service'; +import { MetadataItemService } from './core/metadata-item/metadata-item.service'; /** * Performs the initialization of the app. @@ -50,6 +51,7 @@ export abstract class InitService { protected localeService: LocaleService, protected angulartics2DSpace: Angulartics2DSpace, protected metadata: MetadataService, + protected metadataItem: MetadataItemService, protected breadcrumbsService: BreadcrumbsService, protected themeService: ThemeService, protected menuService: MenuService, @@ -188,6 +190,7 @@ export abstract class InitService { this.breadcrumbsService.listenForRouteChanges(); this.themeService.listenForRouteChanges(); this.menuService.listenForRouteChanges(); + this.metadataItem.checkCurrentRoute(); } /** diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index 61d57f10f9..f38883be1e 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -32,6 +32,7 @@ import { logStartupMessage } from '../../../startup-message'; import { MenuService } from '../../app/shared/menu/menu.service'; import { RootDataService } from '../../app/core/data/root-data.service'; import { firstValueFrom, Subscription } from 'rxjs'; +import { MetadataItemService } from 'src/app/core/metadata-item/metadata-item.service'; /** * Performs client-side initialization. @@ -51,6 +52,7 @@ export class BrowserInitService extends InitService { protected angulartics2DSpace: Angulartics2DSpace, protected googleAnalyticsService: GoogleAnalyticsService, protected metadata: MetadataService, + protected metadataItem: MetadataItemService, protected breadcrumbsService: BreadcrumbsService, protected klaroService: KlaroService, protected authService: AuthService, @@ -66,6 +68,7 @@ export class BrowserInitService extends InitService { localeService, angulartics2DSpace, metadata, + metadataItem, breadcrumbsService, themeService, menuService, diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index d909bb0e8d..074efc31e7 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -21,6 +21,7 @@ import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service'; import { take } from 'rxjs/operators'; import { MenuService } from '../../app/shared/menu/menu.service'; +import { MetadataItemService } from 'src/app/core/metadata-item/metadata-item.service'; /** * Performs server-side initialization. @@ -36,9 +37,10 @@ export class ServerInitService extends InitService { protected localeService: LocaleService, protected angulartics2DSpace: Angulartics2DSpace, protected metadata: MetadataService, + protected metadataItem: MetadataItemService, protected breadcrumbsService: BreadcrumbsService, protected themeService: ThemeService, - protected menuService: MenuService, + protected menuService: MenuService ) { super( store, @@ -48,6 +50,7 @@ export class ServerInitService extends InitService { localeService, angulartics2DSpace, metadata, + metadataItem, breadcrumbsService, themeService, menuService, From e8ff0fbf3638f419a0673ccd7ab6838a53ebe88c Mon Sep 17 00:00:00 2001 From: Alban Imami Date: Thu, 11 May 2023 12:53:18 +0200 Subject: [PATCH 05/30] [CST-5729] implemented functionality to add link tags in head html section when on item page --- server.ts | 12 ++-- .../data/signposting-data.service.spec.ts | 16 +++++ src/app/core/data/signposting-data.service.ts | 52 ++++++++++++++ src/app/core/data/signposting-data.ts | 5 ++ .../core/dspace-rest/dspace-rest.service.ts | 15 +++- .../metadata-item.service.spec.ts | 16 ----- .../metadata-item/metadata-item.service.ts | 70 ------------------- src/app/core/metadata/metadata.service.ts | 56 ++++++++++++--- src/app/init.service.ts | 4 +- src/modules/app/browser-init.service.ts | 3 - src/modules/app/server-init.service.ts | 3 - 11 files changed, 139 insertions(+), 113 deletions(-) create mode 100644 src/app/core/data/signposting-data.service.spec.ts create mode 100644 src/app/core/data/signposting-data.service.ts create mode 100644 src/app/core/data/signposting-data.ts delete mode 100644 src/app/core/metadata-item/metadata-item.service.spec.ts delete mode 100644 src/app/core/metadata-item/metadata-item.service.ts diff --git a/server.ts b/server.ts index 5a3660e4de..b8796eb05d 100644 --- a/server.ts +++ b/server.ts @@ -180,14 +180,14 @@ export function app() { changeOrigin: true })); - /** + /** * Proxy the linksets */ - router.use('/linksets**', createProxyMiddleware({ - target: `${environment.rest.baseUrl}/linksets`, - pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), - changeOrigin: true - })); + router.use('/linkset**', createProxyMiddleware({ + target: `${environment.rest.baseUrl}/signposting/linksets`, + pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), + changeOrigin: true + })); /** * Checks if the rateLimiter property is present diff --git a/src/app/core/data/signposting-data.service.spec.ts b/src/app/core/data/signposting-data.service.spec.ts new file mode 100644 index 0000000000..45c0abb29b --- /dev/null +++ b/src/app/core/data/signposting-data.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SignpostingDataService } from './signposting-data.service'; + +describe('SignpostingDataService', () => { + let service: SignpostingDataService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SignpostingDataService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts new file mode 100644 index 0000000000..25bac49f17 --- /dev/null +++ b/src/app/core/data/signposting-data.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; +// import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { catchError, map } from 'rxjs/operators'; +// import { throwError } from 'rxjs'; +import { Observable, of as observableOf } from 'rxjs'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { HttpHeaders } from '@angular/common/http'; + +@Injectable({ + providedIn: 'root' +}) +export class SignpostingDataService { + + constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { } + + getLinks(uuid: string): Observable { + const url = this.halService.getRootHref().split('/'); + const baseUrl = `${url[0]}//${url[2]}/${url[3]}`; + + return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( + catchError((err ) => { + console.error(err); + return observableOf(false); + }), + map((res: RawRestResponse) => res) + ); + } + + getLinksets(uuid: string): Observable { + const url = this.halService.getRootHref().split('/'); + const baseUrl = `${url[0]}//${url[2]}/${url[3]}`; + + const requestOptions = { + observe: 'response' as any, + headers: new HttpHeaders({ + 'accept': 'application/linkset', + 'Content-Type': 'application/linkset' + }), + responseType: 'text' + } as any; + + return this.restService.getWithHeaders(`${baseUrl}/signposting/linksets/${uuid}`, requestOptions).pipe( + catchError((err ) => { + console.error(err); + return observableOf(false); + }), + map((res: RawRestResponse) => res) + ); + } +} diff --git a/src/app/core/data/signposting-data.ts b/src/app/core/data/signposting-data.ts new file mode 100644 index 0000000000..5734d324ec --- /dev/null +++ b/src/app/core/data/signposting-data.ts @@ -0,0 +1,5 @@ +export interface SignpostingDataLink { + href: string, + rel: string, + type: string +} diff --git a/src/app/core/dspace-rest/dspace-rest.service.ts b/src/app/core/dspace-rest/dspace-rest.service.ts index ea4e8c2831..737714869d 100644 --- a/src/app/core/dspace-rest/dspace-rest.service.ts +++ b/src/app/core/dspace-rest/dspace-rest.service.ts @@ -1,4 +1,4 @@ -import { Observable, throwError as observableThrowError } from 'rxjs'; +import { Observable, throwError as observableThrowError, throwError } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http'; @@ -58,6 +58,19 @@ export class DspaceRestService { })); } + getWithHeaders(absoluteURL: string, reqOptions: any): Observable { + const requestOptions = reqOptions; + + return this.http.get(absoluteURL, requestOptions).pipe( + map((res) => ({ + payload: res + })), + catchError((err) => { + console.log('Error: ', err); + return throwError(() => new Error(err.error)); + })); + } + /** * Performs a request to the REST API. * diff --git a/src/app/core/metadata-item/metadata-item.service.spec.ts b/src/app/core/metadata-item/metadata-item.service.spec.ts deleted file mode 100644 index 89ca15658d..0000000000 --- a/src/app/core/metadata-item/metadata-item.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { MetadataItemService } from './metadata-item.service'; - -describe('MetadataItemService', () => { - let service: MetadataItemService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(MetadataItemService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/core/metadata-item/metadata-item.service.ts b/src/app/core/metadata-item/metadata-item.service.ts deleted file mode 100644 index a4fbcf587b..0000000000 --- a/src/app/core/metadata-item/metadata-item.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Inject, Injectable } from '@angular/core'; -import { MetadataService } from '../metadata/metadata.service'; -import { ActivatedRoute, NavigationEnd, Event as NavigationEvent, NavigationStart, Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; -import { Meta, Title } from '@angular/platform-browser'; -import { DSONameService } from '../breadcrumbs/dso-name.service'; -import { BundleDataService } from '../data/bundle-data.service'; -import { BitstreamDataService } from '../data/bitstream-data.service'; -import { BitstreamFormatDataService } from '../data/bitstream-format-data.service'; -import { RootDataService } from '../data/root-data.service'; -import { CoreState } from '../core-state.model'; -import { Store } from '@ngrx/store'; -import { HardRedirectService } from '../services/hard-redirect.service'; -import { APP_CONFIG, AppConfig } from 'src/config/app-config.interface'; -import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; -import { filter, map, switchMap, take, mergeMap } from 'rxjs/operators'; -import { DOCUMENT } from '@angular/common'; - -@Injectable({ - providedIn: 'root' -}) -export class MetadataItemService extends MetadataService { - - constructor( - private router1: ActivatedRoute, - router: Router, - translate: TranslateService, - meta: Meta, - title: Title, - dsoNameService: DSONameService, - bundleDataService: BundleDataService, - bitstreamDataService: BitstreamDataService, - bitstreamFormatDataService: BitstreamFormatDataService, - rootService: RootDataService, - store: Store, - hardRedirectService: HardRedirectService, - @Inject(APP_CONFIG) appConfig: AppConfig, - authorizationService: AuthorizationDataService, - @Inject(DOCUMENT) private document: Document - ) { - super(router, translate, meta, title, dsoNameService, bundleDataService, bitstreamDataService, bitstreamFormatDataService, rootService, store, hardRedirectService, appConfig, authorizationService); - } - - public checkCurrentRoute(){ - - console.log(this.router); - - this.router1.url.subscribe(url => { - console.log(url); - console.log(url[0].path); - }); - - // this.router.events.subscribe((event: NavigationEvent) => { - // if(event instanceof NavigationStart) { - // if(event.url.startsWith('/entities')){ - // console.log('We are on ENTITIES!'); - // } - // } - // }); - } - - setLinkTag(){ - this.clearMetaTags(); - - let link: HTMLLinkElement = this.document.createElement('link'); - link.setAttribute('rel', ''); - link.setAttribute('href', ''); - this.document.head.appendChild(link); - } -} diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index c46f8b1d6e..6d5ca91b8d 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -45,6 +45,7 @@ import { CoreState } from '../core-state.model'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { getDownloadableBitstream } from '../shared/bitstream.operators'; import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; +import { SignpostingDataService } from '../data/signposting-data.service'; /** * The base selector function to select the metaTag section in the store @@ -96,7 +97,8 @@ export class MetadataService { private store: Store, private hardRedirectService: HardRedirectService, @Inject(APP_CONFIG) private appConfig: AppConfig, - private authorizationService: AuthorizationDataService + private authorizationService: AuthorizationDataService, + private signpostginDataService: SignpostingDataService ) { } @@ -138,7 +140,7 @@ export class MetadataService { } } - private getCurrentRoute(route: ActivatedRoute): ActivatedRoute { + public getCurrentRoute(route: ActivatedRoute): ActivatedRoute { while (route.firstChild) { route = route.firstChild; } @@ -162,6 +164,8 @@ export class MetadataService { this.setCitationAbstractUrlTag(); this.setCitationPdfUrlTag(); this.setCitationPublisherTag(); + this.setSignpostingLinks(); + this.setSignpostingLinksets(); if (this.isDissertation()) { this.setCitationDissertationNameTag(); @@ -184,6 +188,45 @@ export class MetadataService { } + /** + * Add to the + */ + private setSignpostingLinks() { + if (this.currentObject.value instanceof Item){ + const value = this.signpostginDataService.getLinks(this.currentObject.getValue().id); + value.subscribe(links => { + links.payload.forEach(link => { + this.setLinkTag(link.href, link.rel, link.type); + }); + }); + } + } + + setLinkTag(href: string, rel: string, type: string){ + let link: HTMLLinkElement = document.createElement('link'); + link.href = href; + link.rel = rel; + link.type = type; + document.head.appendChild(link); + console.log(link); + } + + private setSignpostingLinksets() { + if (this.currentObject.value instanceof Item){ + const value = this.signpostginDataService.getLinksets(this.currentObject.getValue().id); + value.subscribe(linksets => { + this.setLinkAttribute(linksets); + }); + } + } + + setLinkAttribute(linksets){ + console.log('ANDREA', linksets); + const linkAttribute = `Link: ${linksets.payload.body}`; + const textNode = document.createTextNode(linkAttribute); + document.head.appendChild(textNode); + } + /** * Add to the */ @@ -363,15 +406,6 @@ export class MetadataService { } } - /** - * Add to the - */ - // private setLinkTag(): void { - // const value = this.getMetaTagValue('dc.link'); - // this.meta.addTag({ name: 'Link', content: value }); - // this.addMetaTag('Link', value); - // } - getBitLinkIfDownloadable(bitstream: Bitstream, bitstreamRd: RemoteData>): Observable { return observableOf(bitstream).pipe( getDownloadableBitstream(this.authorizationService), diff --git a/src/app/init.service.ts b/src/app/init.service.ts index d5978d782d..2bbc589cc0 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -24,7 +24,6 @@ import { isAuthenticationBlocking } from './core/auth/selectors'; import { distinctUntilChanged, find } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { MenuService } from './shared/menu/menu.service'; -import { MetadataItemService } from './core/metadata-item/metadata-item.service'; /** * Performs the initialization of the app. @@ -51,7 +50,6 @@ export abstract class InitService { protected localeService: LocaleService, protected angulartics2DSpace: Angulartics2DSpace, protected metadata: MetadataService, - protected metadataItem: MetadataItemService, protected breadcrumbsService: BreadcrumbsService, protected themeService: ThemeService, protected menuService: MenuService, @@ -190,7 +188,7 @@ export abstract class InitService { this.breadcrumbsService.listenForRouteChanges(); this.themeService.listenForRouteChanges(); this.menuService.listenForRouteChanges(); - this.metadataItem.checkCurrentRoute(); + // this.metadataItem.checkCurrentRoute(); } /** diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index f38883be1e..61d57f10f9 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -32,7 +32,6 @@ import { logStartupMessage } from '../../../startup-message'; import { MenuService } from '../../app/shared/menu/menu.service'; import { RootDataService } from '../../app/core/data/root-data.service'; import { firstValueFrom, Subscription } from 'rxjs'; -import { MetadataItemService } from 'src/app/core/metadata-item/metadata-item.service'; /** * Performs client-side initialization. @@ -52,7 +51,6 @@ export class BrowserInitService extends InitService { protected angulartics2DSpace: Angulartics2DSpace, protected googleAnalyticsService: GoogleAnalyticsService, protected metadata: MetadataService, - protected metadataItem: MetadataItemService, protected breadcrumbsService: BreadcrumbsService, protected klaroService: KlaroService, protected authService: AuthService, @@ -68,7 +66,6 @@ export class BrowserInitService extends InitService { localeService, angulartics2DSpace, metadata, - metadataItem, breadcrumbsService, themeService, menuService, diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index 074efc31e7..715f872cd9 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -21,7 +21,6 @@ import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service'; import { take } from 'rxjs/operators'; import { MenuService } from '../../app/shared/menu/menu.service'; -import { MetadataItemService } from 'src/app/core/metadata-item/metadata-item.service'; /** * Performs server-side initialization. @@ -37,7 +36,6 @@ export class ServerInitService extends InitService { protected localeService: LocaleService, protected angulartics2DSpace: Angulartics2DSpace, protected metadata: MetadataService, - protected metadataItem: MetadataItemService, protected breadcrumbsService: BreadcrumbsService, protected themeService: ThemeService, protected menuService: MenuService @@ -50,7 +48,6 @@ export class ServerInitService extends InitService { localeService, angulartics2DSpace, metadata, - metadataItem, breadcrumbsService, themeService, menuService, From fe8bbddac25a40be99193a98232c1bf8ba334968 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 11 May 2023 15:05:35 +0200 Subject: [PATCH 06/30] [CST-5729] Fix proxying for signposting --- server.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/server.ts b/server.ts index b8796eb05d..282b1ce29a 100644 --- a/server.ts +++ b/server.ts @@ -26,7 +26,6 @@ import * as ejs from 'ejs'; import * as compression from 'compression'; import * as expressStaticGzip from 'express-static-gzip'; /* eslint-enable import/no-namespace */ - import axios from 'axios'; import LRU from 'lru-cache'; import isbot from 'isbot'; @@ -34,7 +33,7 @@ import { createCertificate } from 'pem'; import { createServer } from 'https'; import { json } from 'body-parser'; -import { existsSync, readFileSync } from 'fs'; +import { readFileSync } from 'fs'; import { join } from 'path'; import { enableProdMode } from '@angular/core'; @@ -183,8 +182,8 @@ export function app() { /** * Proxy the linksets */ - router.use('/linkset**', createProxyMiddleware({ - target: `${environment.rest.baseUrl}/signposting/linksets`, + router.use('/links**', createProxyMiddleware({ + target: `${environment.rest.baseUrl}/signposting`, pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), changeOrigin: true })); From 558f8f51fb8cd6a8a0d6547b1c660e5f1d8fc917 Mon Sep 17 00:00:00 2001 From: Alban Imami Date: Thu, 11 May 2023 18:47:01 +0200 Subject: [PATCH 07/30] [CST-5729] Implemented functionality to add Links in Response Headers on Item Page --- src/app/core/data/signposting-data.service.ts | 10 +++--- src/app/core/metadata/metadata.service.ts | 32 ++++++++++--------- .../services/server-hard-redirect.service.ts | 12 +++++++ .../core/services/server-response.service.ts | 6 ++++ .../full/full-item-page.component.ts | 8 +++-- .../item-page/simple/item-page.component.ts | 11 ++++++- 6 files changed, 55 insertions(+), 24 deletions(-) diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index 25bac49f17..0ef2b49f0f 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -16,8 +16,7 @@ export class SignpostingDataService { constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { } getLinks(uuid: string): Observable { - const url = this.halService.getRootHref().split('/'); - const baseUrl = `${url[0]}//${url[2]}/${url[3]}`; + const baseUrl = this.halService.getRootHref().replace('/api', ''); return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( catchError((err ) => { @@ -28,9 +27,8 @@ export class SignpostingDataService { ); } - getLinksets(uuid: string): Observable { - const url = this.halService.getRootHref().split('/'); - const baseUrl = `${url[0]}//${url[2]}/${url[3]}`; + getLinksets(uuid: string): Observable { + const baseUrl = this.halService.getRootHref().replace('/api', ''); const requestOptions = { observe: 'response' as any, @@ -46,7 +44,7 @@ export class SignpostingDataService { console.error(err); return observableOf(false); }), - map((res: RawRestResponse) => res) + map((res: RawRestResponse) => res.payload.body) ); } } diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 6d5ca91b8d..3cc678fb15 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -165,7 +165,6 @@ export class MetadataService { this.setCitationPdfUrlTag(); this.setCitationPublisherTag(); this.setSignpostingLinks(); - this.setSignpostingLinksets(); if (this.isDissertation()) { this.setCitationDissertationNameTag(); @@ -211,21 +210,24 @@ export class MetadataService { console.log(link); } - private setSignpostingLinksets() { - if (this.currentObject.value instanceof Item){ - const value = this.signpostginDataService.getLinksets(this.currentObject.getValue().id); - value.subscribe(linksets => { - this.setLinkAttribute(linksets); - }); - } - } + // public setSignpostingLinksets(itemId: string) { + // let linkSet: string; - setLinkAttribute(linksets){ - console.log('ANDREA', linksets); - const linkAttribute = `Link: ${linksets.payload.body}`; - const textNode = document.createTextNode(linkAttribute); - document.head.appendChild(textNode); - } + // const value = this.signpostginDataService.getLinksets(itemId); + + // value.subscribe(linksets => { + // linkSet = linksets.payload.body; + // }); + + // return linkSet; + // } + + // setLinkAttribute(linksets){ + // console.log('ANDREA', linksets); + // const linkAttribute = `Link: ${linksets.payload.body}`; + // const textNode = document.createTextNode(linkAttribute); + // document.head.appendChild(textNode); + // } /** * Add to the diff --git a/src/app/core/services/server-hard-redirect.service.ts b/src/app/core/services/server-hard-redirect.service.ts index 94b9ed6198..a6c0e09aee 100644 --- a/src/app/core/services/server-hard-redirect.service.ts +++ b/src/app/core/services/server-hard-redirect.service.ts @@ -2,6 +2,9 @@ import { Inject, Injectable } from '@angular/core'; import { Request, Response } from 'express'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { HardRedirectService } from './hard-redirect.service'; +import { SignpostingDataService } from '../data/signposting-data.service'; +import { ActivatedRoute } from '@angular/router'; +import { take } from 'rxjs'; /** * Service for performing hard redirects within the server app module @@ -12,6 +15,8 @@ export class ServerHardRedirectService extends HardRedirectService { constructor( @Inject(REQUEST) protected req: Request, @Inject(RESPONSE) protected res: Response, + private signpostginDataService: SignpostingDataService, + protected route: ActivatedRoute ) { super(); } @@ -46,6 +51,13 @@ export class ServerHardRedirectService extends HardRedirectService { } console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`); + + this.route.params.subscribe(params => { + this.signpostginDataService.getLinksets(params.id).pipe(take(1)).subscribe(linksets => { + this.res.setHeader('Link', linksets); + }); + }); + this.res.redirect(status, url); this.res.end(); // I haven't found a way to correctly stop Angular rendering. diff --git a/src/app/core/services/server-response.service.ts b/src/app/core/services/server-response.service.ts index 02e00446bc..6dd50506e9 100644 --- a/src/app/core/services/server-response.service.ts +++ b/src/app/core/services/server-response.service.ts @@ -35,4 +35,10 @@ export class ServerResponseService { setInternalServerError(message = 'Internal Server Error'): this { return this.setStatus(500, message); } + + setLinksetsHeader(linksets: string){ + if (this.response) { + this.response.setHeader('Link', linksets); + } + } } diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts index 118e436004..44766bac7b 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -16,6 +16,8 @@ import { hasValue } from '../../shared/empty.util'; import { AuthService } from '../../core/auth/auth.service'; import { Location } from '@angular/common'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ServerResponseService } from 'src/app/core/services/server-response.service'; +import { SignpostingDataService } from 'src/app/core/data/signposting-data.service'; /** @@ -48,8 +50,10 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, items: ItemDataService, authService: AuthService, authorizationService: AuthorizationDataService, - private _location: Location) { - super(route, router, items, authService, authorizationService); + private _location: Location, + responseService: ServerResponseService, + signpostginDataService: SignpostingDataService) { + super(route, router, items, authService, authorizationService, responseService, signpostginDataService); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index 6e0db386db..058cbc667a 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -15,6 +15,8 @@ import { getItemPageRoute } from '../item-page-routing-paths'; import { redirectOn4xx } from '../../core/shared/authorized.operators'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; /** * This component renders a simple item page. @@ -62,8 +64,15 @@ export class ItemPageComponent implements OnInit { private router: Router, private items: ItemDataService, private authService: AuthService, - private authorizationService: AuthorizationDataService + private authorizationService: AuthorizationDataService, + private responseService: ServerResponseService, + private signpostginDataService: SignpostingDataService ) { + this.route.params.subscribe(params => { + this.signpostginDataService.getLinksets(params.id).subscribe(linksets => { + this.responseService.setLinksetsHeader(linksets); + }); + }); } /** From 6f4b0ad6b1b03af129f1f3c80c3e8b3c2a419d04 Mon Sep 17 00:00:00 2001 From: Alban Imami Date: Fri, 12 May 2023 12:10:57 +0200 Subject: [PATCH 08/30] [CST-5729] fixed the id on the bitstream api request --- .../bitstream-download-page.component.ts | 10 +++++++++- src/app/core/data/signposting-data.service.ts | 9 +++++---- src/app/core/data/signposting-data.ts | 5 ----- src/app/core/data/signposting-links.model.ts | 7 +++++++ src/app/core/services/server-hard-redirect.service.ts | 11 ----------- 5 files changed, 21 insertions(+), 21 deletions(-) delete mode 100644 src/app/core/data/signposting-data.ts create mode 100644 src/app/core/data/signposting-links.model.ts diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts index 51ec762ec3..38e4d01dd5 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts @@ -14,6 +14,8 @@ import { getForbiddenRoute } from '../../app-routing-paths'; import { RemoteData } from '../../core/data/remote-data'; import { redirectOn4xx } from '../../core/shared/authorized.operators'; import { Location } from '@angular/common'; +import { SignpostingDataService } from 'src/app/core/data/signposting-data.service'; +import { ServerResponseService } from 'src/app/core/services/server-response.service'; @Component({ selector: 'ds-bitstream-download-page', @@ -36,8 +38,14 @@ export class BitstreamDownloadPageComponent implements OnInit { private fileService: FileService, private hardRedirectService: HardRedirectService, private location: Location, + private signpostginDataService: SignpostingDataService, + private responseService: ServerResponseService ) { - + this.route.params.subscribe(params => { + this.signpostginDataService.getLinksets(params.id).pipe(take(1)).subscribe(linksets => { + this.responseService.setLinksetsHeader(linksets); + }); + }); } back(): void { diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index 0ef2b49f0f..5e965d1ab5 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -7,6 +7,7 @@ import { Observable, of as observableOf } from 'rxjs'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { HttpHeaders } from '@angular/common/http'; +import { SignpostingLinks } from './signposting-links.model'; @Injectable({ providedIn: 'root' @@ -15,15 +16,15 @@ export class SignpostingDataService { constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { } - getLinks(uuid: string): Observable { + getLinks(uuid: string): Observable { const baseUrl = this.halService.getRootHref().replace('/api', ''); return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( - catchError((err ) => { + catchError((err) => { console.error(err); return observableOf(false); }), - map((res: RawRestResponse) => res) + map((res: SignpostingLinks) => res) ); } @@ -40,7 +41,7 @@ export class SignpostingDataService { } as any; return this.restService.getWithHeaders(`${baseUrl}/signposting/linksets/${uuid}`, requestOptions).pipe( - catchError((err ) => { + catchError((err) => { console.error(err); return observableOf(false); }), diff --git a/src/app/core/data/signposting-data.ts b/src/app/core/data/signposting-data.ts deleted file mode 100644 index 5734d324ec..0000000000 --- a/src/app/core/data/signposting-data.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface SignpostingDataLink { - href: string, - rel: string, - type: string -} diff --git a/src/app/core/data/signposting-links.model.ts b/src/app/core/data/signposting-links.model.ts new file mode 100644 index 0000000000..19d8869ba1 --- /dev/null +++ b/src/app/core/data/signposting-links.model.ts @@ -0,0 +1,7 @@ +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; + +export interface SignpostingLinks extends RawRestResponse { + href?: string, + rel?: string, + type?: string +} diff --git a/src/app/core/services/server-hard-redirect.service.ts b/src/app/core/services/server-hard-redirect.service.ts index a6c0e09aee..de8b45b0e5 100644 --- a/src/app/core/services/server-hard-redirect.service.ts +++ b/src/app/core/services/server-hard-redirect.service.ts @@ -2,9 +2,6 @@ import { Inject, Injectable } from '@angular/core'; import { Request, Response } from 'express'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { HardRedirectService } from './hard-redirect.service'; -import { SignpostingDataService } from '../data/signposting-data.service'; -import { ActivatedRoute } from '@angular/router'; -import { take } from 'rxjs'; /** * Service for performing hard redirects within the server app module @@ -15,8 +12,6 @@ export class ServerHardRedirectService extends HardRedirectService { constructor( @Inject(REQUEST) protected req: Request, @Inject(RESPONSE) protected res: Response, - private signpostginDataService: SignpostingDataService, - protected route: ActivatedRoute ) { super(); } @@ -52,12 +47,6 @@ export class ServerHardRedirectService extends HardRedirectService { console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`); - this.route.params.subscribe(params => { - this.signpostginDataService.getLinksets(params.id).pipe(take(1)).subscribe(linksets => { - this.res.setHeader('Link', linksets); - }); - }); - this.res.redirect(status, url); this.res.end(); // I haven't found a way to correctly stop Angular rendering. From 22fbcbebfa793054bd8f61a95d3c67e8f05940ec Mon Sep 17 00:00:00 2001 From: Alban Imami Date: Fri, 12 May 2023 16:01:19 +0200 Subject: [PATCH 09/30] [CST-5729] Method to remove Link tags from Head when switched page --- src/app/core/metadata/metadata.service.ts | 31 +++++++++-------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 3cc678fb15..60409f17a1 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -64,6 +64,11 @@ const tagsInUseSelector = (state: MetaTagState) => state.tagsInUse, ); +/** + * Link elements added on Item Page + */ +let linkTags = []; + @Injectable() export class MetadataService { @@ -119,6 +124,7 @@ export class MetadataService { private processRouteChange(routeInfo: any): void { this.clearMetaTags(); + this.clearLinkTags(); if (hasValue(routeInfo.data.value.dso) && hasValue(routeInfo.data.value.dso.payload)) { this.currentObject.next(routeInfo.data.value.dso.payload); @@ -207,27 +213,14 @@ export class MetadataService { link.rel = rel; link.type = type; document.head.appendChild(link); - console.log(link); + linkTags.push(link); } - // public setSignpostingLinksets(itemId: string) { - // let linkSet: string; - - // const value = this.signpostginDataService.getLinksets(itemId); - - // value.subscribe(linksets => { - // linkSet = linksets.payload.body; - // }); - - // return linkSet; - // } - - // setLinkAttribute(linksets){ - // console.log('ANDREA', linksets); - // const linkAttribute = `Link: ${linksets.payload.body}`; - // const textNode = document.createTextNode(linkAttribute); - // document.head.appendChild(textNode); - // } + public clearLinkTags(){ + linkTags.forEach(link => { + link.parentNode.removeChild(link); + }); + } /** * Add to the From 93d9b87db16e939308d9faac7462bc57ec5cda7b Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 12 May 2023 19:52:47 +0200 Subject: [PATCH 10/30] [CST-5729] Finalize implementation --- .../bitstream-download-page.component.spec.ts | 35 ++++++--- .../bitstream-download-page.component.ts | 9 ++- .../data/signposting-data.service.spec.ts | 74 ++++++++++++++++++- src/app/core/data/signposting-data.service.ts | 9 +-- .../core/metadata/metadata.service.spec.ts | 24 ++++-- src/app/core/metadata/metadata.service.ts | 39 +++++----- .../full/full-item-page.component.spec.ts | 19 ++++- .../full/full-item-page.component.ts | 5 +- .../simple/item-page.component.spec.ts | 14 ++++ .../item-page/simple/item-page.component.ts | 4 +- 10 files changed, 175 insertions(+), 57 deletions(-) diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts index e84b254eae..71fd74a707 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts @@ -11,6 +11,8 @@ import { ActivatedRoute, Router } from '@angular/router'; import { getForbiddenRoute } from '../../app-routing-paths'; import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { ServerResponseService } from '../../core/services/server-response.service'; describe('BitstreamDownloadPageComponent', () => { let component: BitstreamDownloadPageComponent; @@ -24,6 +26,8 @@ describe('BitstreamDownloadPageComponent', () => { let router; let bitstream: Bitstream; + let serverResponseService: jasmine.SpyObj; + let signpostingDataService: jasmine.SpyObj; function init() { authService = jasmine.createSpyObj('authService', { @@ -44,8 +48,8 @@ describe('BitstreamDownloadPageComponent', () => { bitstream = Object.assign(new Bitstream(), { uuid: 'bitstreamUuid', _links: { - content: {href: 'bitstream-content-link'}, - self: {href: 'bitstream-self-link'}, + content: { href: 'bitstream-content-link' }, + self: { href: 'bitstream-self-link' }, } }); @@ -54,10 +58,21 @@ describe('BitstreamDownloadPageComponent', () => { bitstream: createSuccessfulRemoteDataObject( bitstream ) + }), + params: observableOf({ + id: 'testid' }) }; router = jasmine.createSpyObj('router', ['navigateByUrl']); + + serverResponseService = jasmine.createSpyObj('ServerResponseService', { + setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + }); + + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinksets: observableOf('test'), + }); } function initTestbed() { @@ -65,12 +80,14 @@ describe('BitstreamDownloadPageComponent', () => { imports: [CommonModule, TranslateModule.forRoot()], declarations: [BitstreamDownloadPageComponent], providers: [ - {provide: ActivatedRoute, useValue: activatedRoute}, - {provide: Router, useValue: router}, - {provide: AuthorizationDataService, useValue: authorizationService}, - {provide: AuthService, useValue: authService}, - {provide: FileService, useValue: fileService}, - {provide: HardRedirectService, useValue: hardRedirectService}, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: router }, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: AuthService, useValue: authService }, + { provide: FileService, useValue: fileService }, + { provide: HardRedirectService, useValue: hardRedirectService }, + { provide: ServerResponseService, useValue: serverResponseService }, + { provide: SignpostingDataService, useValue: signpostingDataService } ] }) .compileComponents(); @@ -134,7 +151,7 @@ describe('BitstreamDownloadPageComponent', () => { fixture.detectChanges(); }); it('should navigate to the forbidden route', () => { - expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), {skipLocationChange: true}); + expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), { skipLocationChange: true }); }); }); describe('when the user is not authorized and not logged in', () => { diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts index 38e4d01dd5..4814a9385a 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { filter, map, switchMap, take } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { getRemoteDataPayload} from '../../core/shared/operators'; +import { getRemoteDataPayload } from '../../core/shared/operators'; import { Bitstream } from '../../core/shared/bitstream.model'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; @@ -29,7 +29,6 @@ export class BitstreamDownloadPageComponent implements OnInit { bitstream$: Observable; bitstreamRD$: Observable>; - constructor( private route: ActivatedRoute, protected router: Router, @@ -42,8 +41,10 @@ export class BitstreamDownloadPageComponent implements OnInit { private responseService: ServerResponseService ) { this.route.params.subscribe(params => { - this.signpostginDataService.getLinksets(params.id).pipe(take(1)).subscribe(linksets => { - this.responseService.setLinksetsHeader(linksets); + this.signpostginDataService.getLinks(params.id).pipe(take(1)).subscribe(linksets => { + linksets.forEach(link => { + this.responseService.setLinksetsHeader(link.href); + }); }); }); } diff --git a/src/app/core/data/signposting-data.service.spec.ts b/src/app/core/data/signposting-data.service.spec.ts index 45c0abb29b..091f38de2a 100644 --- a/src/app/core/data/signposting-data.service.spec.ts +++ b/src/app/core/data/signposting-data.service.spec.ts @@ -1,16 +1,84 @@ -import { TestBed } from '@angular/core/testing'; - +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { SignpostingDataService } from './signposting-data.service'; +import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { of } from 'rxjs'; +import { SignpostingLinks } from './signposting-links.model'; +import { map } from 'rxjs/operators'; describe('SignpostingDataService', () => { let service: SignpostingDataService; + let restServiceSpy: jasmine.SpyObj; + let halServiceSpy: jasmine.SpyObj; beforeEach(() => { - TestBed.configureTestingModule({}); + const restSpy = jasmine.createSpyObj('DspaceRestService', ['get', 'getWithHeaders']); + const halSpy = jasmine.createSpyObj('HALEndpointService', ['getRootHref']); + + TestBed.configureTestingModule({ + providers: [ + SignpostingDataService, + { provide: DspaceRestService, useValue: restSpy }, + { provide: HALEndpointService, useValue: halSpy } + ] + }); + service = TestBed.inject(SignpostingDataService); + restServiceSpy = TestBed.inject(DspaceRestService) as jasmine.SpyObj; + halServiceSpy = TestBed.inject(HALEndpointService) as jasmine.SpyObj; }); it('should be created', () => { expect(service).toBeTruthy(); }); + + it('should return signposting links', fakeAsync(() => { + const uuid = '123'; + const baseUrl = 'http://localhost:8080'; + + halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); + + const mockResponse: any = { + self: { + href: `${baseUrl}/signposting/links/${uuid}` + } + }; + + restServiceSpy.get.and.returnValue(of(mockResponse)); + + let result: SignpostingLinks; + + service.getLinks(uuid).subscribe((links) => { + result = links; + }); + + tick(); + + expect(result).toEqual(mockResponse); + expect(halServiceSpy.getRootHref).toHaveBeenCalled(); + expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); + })); + + it('should handle error and return false', fakeAsync(() => { + const uuid = '123'; + const baseUrl = 'http://localhost:8080'; + + halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); + + restServiceSpy.get.and.returnValue(of(null).pipe(map(() => { + throw new Error('Error'); + }))); + + let result: any; + + service.getLinks(uuid).subscribe((data) => { + result = data; + }); + + tick(); + + expect(result).toBeFalse(); + expect(halServiceSpy.getRootHref).toHaveBeenCalled(); + expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); + })); }); diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index 5e965d1ab5..efdf80a381 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -1,8 +1,6 @@ import { Injectable } from '@angular/core'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; -// import { HttpHeaders, HttpResponse } from '@angular/common/http'; import { catchError, map } from 'rxjs/operators'; -// import { throwError } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; @@ -14,9 +12,10 @@ import { SignpostingLinks } from './signposting-links.model'; }) export class SignpostingDataService { - constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { } + constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { + } - getLinks(uuid: string): Observable { + getLinks(uuid: string): Observable { const baseUrl = this.halService.getRootHref().replace('/api', ''); return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( @@ -24,7 +23,7 @@ export class SignpostingDataService { console.error(err); return observableOf(false); }), - map((res: SignpostingLinks) => res) + map((res: RawRestResponse) => res.payload as SignpostingLinks[]) ); } diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 553b437d71..3c5b4adc0c 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -8,12 +8,7 @@ import { Observable, of as observableOf, of } from 'rxjs'; import { RemoteData } from '../data/remote-data'; import { Item } from '../shared/item.model'; -import { - ItemMock, - MockBitstream1, - MockBitstream3, - MockBitstream2 -} from '../../shared/mocks/item.mock'; +import { ItemMock, MockBitstream1, MockBitstream2, MockBitstream3 } from '../../shared/mocks/item.mock'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { PaginatedList } from '../data/paginated-list.model'; import { Bitstream } from '../shared/bitstream.model'; @@ -30,6 +25,7 @@ import { getMockStore } from '@ngrx/store/testing'; import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { AppConfig } from '../../../config/app-config.interface'; +import { SignpostingDataService } from '../data/signposting-data.service'; describe('MetadataService', () => { let metadataService: MetadataService; @@ -46,6 +42,7 @@ describe('MetadataService', () => { let translateService: TranslateService; let hardRedirectService: HardRedirectService; let authorizationService: AuthorizationDataService; + let signpostingDataService: SignpostingDataService; let router: Router; let store; @@ -53,7 +50,12 @@ describe('MetadataService', () => { let appConfig: AppConfig; const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] }}}; - + const mocklink = { + href: 'http://test.org', + rel: 'test', + type: 'test' + }; + const document: any = jasmine.createSpyObj('document', ['head', 'createElement']); beforeEach(() => { rootService = jasmine.createSpyObj({ @@ -90,6 +92,10 @@ describe('MetadataService', () => { isAuthorized: observableOf(true) }); + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinks: observableOf([mocklink]) + }); + // @ts-ignore store = getMockStore({ initialState }); spyOn(store, 'dispatch'); @@ -115,7 +121,9 @@ describe('MetadataService', () => { store, hardRedirectService, appConfig, - authorizationService + document, + authorizationService, + signpostingDataService ); }); diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 60409f17a1..c22f14b680 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Inject } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; @@ -8,12 +8,12 @@ import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, - Observable, - of as observableOf, concat as observableConcat, - EMPTY + EMPTY, + Observable, + of as observableOf } from 'rxjs'; -import { filter, map, switchMap, take, mergeMap } from 'rxjs/operators'; +import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; import { DSONameService } from '../breadcrumbs/dso-name.service'; @@ -25,10 +25,7 @@ import { BitstreamFormat } from '../shared/bitstream-format.model'; import { Bitstream } from '../shared/bitstream.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; -import { - getFirstCompletedRemoteData, - getFirstSucceededRemoteDataPayload -} from '../shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../shared/operators'; import { RootDataService } from '../data/root-data.service'; import { getBitstreamDownloadRoute } from '../../app-routing-paths'; import { BundleDataService } from '../data/bundle-data.service'; @@ -46,6 +43,7 @@ import { AuthorizationDataService } from '../data/feature-authorization/authoriz import { getDownloadableBitstream } from '../shared/bitstream.operators'; import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; import { SignpostingDataService } from '../data/signposting-data.service'; +import { DOCUMENT } from '@angular/common'; /** * The base selector function to select the metaTag section in the store @@ -102,8 +100,9 @@ export class MetadataService { private store: Store, private hardRedirectService: HardRedirectService, @Inject(APP_CONFIG) private appConfig: AppConfig, + @Inject(DOCUMENT) private _document: Document, private authorizationService: AuthorizationDataService, - private signpostginDataService: SignpostingDataService + private signpostingDataService: SignpostingDataService ) { } @@ -198,9 +197,9 @@ export class MetadataService { */ private setSignpostingLinks() { if (this.currentObject.value instanceof Item){ - const value = this.signpostginDataService.getLinks(this.currentObject.getValue().id); + const value = this.signpostingDataService.getLinks(this.currentObject.getValue().id); value.subscribe(links => { - links.payload.forEach(link => { + links.forEach(link => { this.setLinkTag(link.href, link.rel, link.type); }); }); @@ -208,17 +207,19 @@ export class MetadataService { } setLinkTag(href: string, rel: string, type: string){ - let link: HTMLLinkElement = document.createElement('link'); - link.href = href; - link.rel = rel; - link.type = type; - document.head.appendChild(link); - linkTags.push(link); + let link: HTMLLinkElement = this._document.createElement('link'); + if (link) { + link.href = href; + link.rel = rel; + link.type = type; + this._document.head?.appendChild(link); + linkTags.push(link); + } } public clearLinkTags(){ linkTags.forEach(link => { - link.parentNode.removeChild(link); + link.parentNode?.removeChild(link); }); } diff --git a/src/app/item-page/full/full-item-page.component.spec.ts b/src/app/item-page/full/full-item-page.component.spec.ts index 53e36be1d1..6c59ccbc67 100644 --- a/src/app/item-page/full/full-item-page.component.spec.ts +++ b/src/app/item-page/full/full-item-page.component.spec.ts @@ -11,7 +11,7 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { VarDirective } from '../../shared/utils/var.directive'; import { RouterTestingModule } from '@angular/router/testing'; import { Item } from '../../core/shared/item.model'; -import { BehaviorSubject, of as observableOf } from 'rxjs'; +import { BehaviorSubject, of as observableOf, of } from 'rxjs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; @@ -20,6 +20,8 @@ import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { createRelationshipsObservable } from '../simple/item-types/shared/item.component.spec'; import { RemoteData } from '../../core/data/remote-data'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -55,8 +57,8 @@ describe('FullItemPageComponent', () => { let routeStub: ActivatedRouteStub; let routeData; let authorizationDataService: AuthorizationDataService; - - + let serverResponseService: jasmine.SpyObj; + let signpostingDataService: jasmine.SpyObj; beforeEach(waitForAsync(() => { authService = jasmine.createSpyObj('authService', { @@ -76,6 +78,14 @@ describe('FullItemPageComponent', () => { isAuthorized: observableOf(false), }); + serverResponseService = jasmine.createSpyObj('ServerResponseService', { + setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + }); + + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinksets: of('test'), + }); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ loader: { @@ -90,8 +100,9 @@ describe('FullItemPageComponent', () => { { provide: MetadataService, useValue: metadataServiceStub }, { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: ServerResponseService, useValue: serverResponseService }, + { provide: SignpostingDataService, useValue: signpostingDataService }, ], - schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(FullItemPageComponent, { set: { changeDetection: ChangeDetectionStrategy.Default } diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts index 44766bac7b..2570bf70fe 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -19,7 +19,6 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/ import { ServerResponseService } from 'src/app/core/services/server-response.service'; import { SignpostingDataService } from 'src/app/core/data/signposting-data.service'; - /** * This component renders a full item page. * The route parameter 'id' is used to request the item it represents. @@ -52,8 +51,8 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, authorizationService: AuthorizationDataService, private _location: Location, responseService: ServerResponseService, - signpostginDataService: SignpostingDataService) { - super(route, router, items, authService, authorizationService, responseService, signpostginDataService); + signpostingDataService: SignpostingDataService) { + super(route, router, items, authService, authorizationService, responseService, signpostingDataService); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ diff --git a/src/app/item-page/simple/item-page.component.spec.ts b/src/app/item-page/simple/item-page.component.spec.ts index 9b0e87939d..042cac2724 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -22,6 +22,9 @@ import { import { AuthService } from '../../core/auth/auth.service'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { of } from 'rxjs/internal/observable/of'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -41,6 +44,8 @@ describe('ItemPageComponent', () => { let fixture: ComponentFixture; let authService: AuthService; let authorizationDataService: AuthorizationDataService; + let serverResponseService: jasmine.SpyObj; + let signpostingDataService: jasmine.SpyObj; const mockMetadataService = { /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ @@ -60,6 +65,13 @@ describe('ItemPageComponent', () => { authorizationDataService = jasmine.createSpyObj('authorizationDataService', { isAuthorized: observableOf(false), }); + serverResponseService = jasmine.createSpyObj('ServerResponseService', { + setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + }); + + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinksets: of('test'), + }); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ @@ -76,6 +88,8 @@ describe('ItemPageComponent', () => { { provide: Router, useValue: {} }, { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: ServerResponseService, useValue: serverResponseService }, + { provide: SignpostingDataService, useValue: signpostingDataService }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index 058cbc667a..6483769483 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -66,10 +66,10 @@ export class ItemPageComponent implements OnInit { private authService: AuthService, private authorizationService: AuthorizationDataService, private responseService: ServerResponseService, - private signpostginDataService: SignpostingDataService + private signpostingDataService: SignpostingDataService ) { this.route.params.subscribe(params => { - this.signpostginDataService.getLinksets(params.id).subscribe(linksets => { + this.signpostingDataService.getLinksets(params.id).subscribe(linksets => { this.responseService.setLinksetsHeader(linksets); }); }); From 78cf3e1bfc52b43b64023d8a0d75b1354dbc28cf Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 15 May 2023 16:27:38 +0200 Subject: [PATCH 11/30] [CST-5729] Finalize signposting implementation --- .../bitstream-download-page.component.spec.ts | 19 +++++- .../bitstream-download-page.component.ts | 13 ++-- .../data/signposting-data.service.spec.ts | 43 ++++++++----- src/app/core/data/signposting-data.service.ts | 33 +++------- src/app/core/data/signposting-links.model.ts | 7 ++- .../core/metadata/metadata.service.spec.ts | 24 +++---- src/app/core/metadata/metadata.service.ts | 63 ++++--------------- .../core/services/server-response.service.ts | 5 +- .../full/full-item-page.component.spec.ts | 41 +++++++++++- .../full/full-item-page.component.ts | 26 ++++---- .../simple/item-page.component.spec.ts | 41 +++++++++++- .../item-page/simple/item-page.component.ts | 50 +++++++++++---- 12 files changed, 218 insertions(+), 147 deletions(-) diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts index 71fd74a707..66024063cd 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts @@ -29,6 +29,18 @@ describe('BitstreamDownloadPageComponent', () => { let serverResponseService: jasmine.SpyObj; let signpostingDataService: jasmine.SpyObj; + const mocklink = { + href: 'http://test.org', + rel: 'test', + type: 'test' + }; + + const mocklink2 = { + href: 'http://test2.org', + rel: 'test', + type: 'test' + }; + function init() { authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), @@ -67,11 +79,11 @@ describe('BitstreamDownloadPageComponent', () => { router = jasmine.createSpyObj('router', ['navigateByUrl']); serverResponseService = jasmine.createSpyObj('ServerResponseService', { - setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + setHeader: jasmine.createSpy('setHeader'), }); signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { - getLinksets: observableOf('test'), + getLinks: observableOf([mocklink, mocklink2]) }); } @@ -124,6 +136,9 @@ describe('BitstreamDownloadPageComponent', () => { it('should redirect to the content link', () => { expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link'); }); + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + }); }); describe('when the user is authorized and logged in', () => { beforeEach(waitForAsync(() => { diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts index 4814a9385a..14245c4cd1 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts @@ -16,6 +16,7 @@ import { redirectOn4xx } from '../../core/shared/authorized.operators'; import { Location } from '@angular/common'; import { SignpostingDataService } from 'src/app/core/data/signposting-data.service'; import { ServerResponseService } from 'src/app/core/services/server-response.service'; +import { SignpostingLink } from '../../core/data/signposting-links.model'; @Component({ selector: 'ds-bitstream-download-page', @@ -37,14 +38,18 @@ export class BitstreamDownloadPageComponent implements OnInit { private fileService: FileService, private hardRedirectService: HardRedirectService, private location: Location, - private signpostginDataService: SignpostingDataService, + private signpostingDataService: SignpostingDataService, private responseService: ServerResponseService ) { this.route.params.subscribe(params => { - this.signpostginDataService.getLinks(params.id).pipe(take(1)).subscribe(linksets => { - linksets.forEach(link => { - this.responseService.setLinksetsHeader(link.href); + this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { + let links = ''; + + signpostingLinks.forEach((link: SignpostingLink) => { + links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; }); + + this.responseService.setHeader('Link', links); }); }); } diff --git a/src/app/core/data/signposting-data.service.spec.ts b/src/app/core/data/signposting-data.service.spec.ts index 091f38de2a..c76899221e 100644 --- a/src/app/core/data/signposting-data.service.spec.ts +++ b/src/app/core/data/signposting-data.service.spec.ts @@ -3,13 +3,32 @@ import { SignpostingDataService } from './signposting-data.service'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { of } from 'rxjs'; -import { SignpostingLinks } from './signposting-links.model'; -import { map } from 'rxjs/operators'; +import { SignpostingLink } from './signposting-links.model'; describe('SignpostingDataService', () => { let service: SignpostingDataService; let restServiceSpy: jasmine.SpyObj; let halServiceSpy: jasmine.SpyObj; + const mocklink = { + href: 'http://test.org', + rel: 'test', + type: 'test' + }; + + const mocklink2 = { + href: 'http://test2.org', + rel: 'test', + type: 'test' + }; + + const mockResponse: any = { + statusCode: 200, + payload: [mocklink, mocklink2] + }; + + const mockErrResponse: any = { + statusCode: 500 + }; beforeEach(() => { const restSpy = jasmine.createSpyObj('DspaceRestService', ['get', 'getWithHeaders']); @@ -38,15 +57,11 @@ describe('SignpostingDataService', () => { halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); - const mockResponse: any = { - self: { - href: `${baseUrl}/signposting/links/${uuid}` - } - }; - restServiceSpy.get.and.returnValue(of(mockResponse)); - let result: SignpostingLinks; + let result: SignpostingLink[]; + + const expectedResult: SignpostingLink[] = [mocklink, mocklink2]; service.getLinks(uuid).subscribe((links) => { result = links; @@ -54,20 +69,18 @@ describe('SignpostingDataService', () => { tick(); - expect(result).toEqual(mockResponse); + expect(result).toEqual(expectedResult); expect(halServiceSpy.getRootHref).toHaveBeenCalled(); expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); })); - it('should handle error and return false', fakeAsync(() => { + it('should handle error and return an empty array', fakeAsync(() => { const uuid = '123'; const baseUrl = 'http://localhost:8080'; halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); - restServiceSpy.get.and.returnValue(of(null).pipe(map(() => { - throw new Error('Error'); - }))); + restServiceSpy.get.and.returnValue(of(mockErrResponse)); let result: any; @@ -77,7 +90,7 @@ describe('SignpostingDataService', () => { tick(); - expect(result).toBeFalse(); + expect(result).toEqual([]); expect(halServiceSpy.getRootHref).toHaveBeenCalled(); expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); })); diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index efdf80a381..e09d68974c 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -1,11 +1,12 @@ import { Injectable } from '@angular/core'; -import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; + import { catchError, map } from 'rxjs/operators'; import { Observable, of as observableOf } from 'rxjs'; + +import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -import { HttpHeaders } from '@angular/common/http'; -import { SignpostingLinks } from './signposting-links.model'; +import { SignpostingLink } from './signposting-links.model'; @Injectable({ providedIn: 'root' @@ -15,36 +16,16 @@ export class SignpostingDataService { constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { } - getLinks(uuid: string): Observable { + getLinks(uuid: string): Observable { const baseUrl = this.halService.getRootHref().replace('/api', ''); return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( catchError((err) => { console.error(err); - return observableOf(false); + return observableOf([]); }), - map((res: RawRestResponse) => res.payload as SignpostingLinks[]) + map((res: RawRestResponse) => res.statusCode === 200 ? res.payload as SignpostingLink[] : []) ); } - getLinksets(uuid: string): Observable { - const baseUrl = this.halService.getRootHref().replace('/api', ''); - - const requestOptions = { - observe: 'response' as any, - headers: new HttpHeaders({ - 'accept': 'application/linkset', - 'Content-Type': 'application/linkset' - }), - responseType: 'text' - } as any; - - return this.restService.getWithHeaders(`${baseUrl}/signposting/linksets/${uuid}`, requestOptions).pipe( - catchError((err) => { - console.error(err); - return observableOf(false); - }), - map((res: RawRestResponse) => res.payload.body) - ); - } } diff --git a/src/app/core/data/signposting-links.model.ts b/src/app/core/data/signposting-links.model.ts index 19d8869ba1..11d2cafe00 100644 --- a/src/app/core/data/signposting-links.model.ts +++ b/src/app/core/data/signposting-links.model.ts @@ -1,6 +1,7 @@ -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; - -export interface SignpostingLinks extends RawRestResponse { +/** + * Represents the link object received by the signposting endpoint + */ +export interface SignpostingLink { href?: string, rel?: string, type?: string diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 3c5b4adc0c..553b437d71 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -8,7 +8,12 @@ import { Observable, of as observableOf, of } from 'rxjs'; import { RemoteData } from '../data/remote-data'; import { Item } from '../shared/item.model'; -import { ItemMock, MockBitstream1, MockBitstream2, MockBitstream3 } from '../../shared/mocks/item.mock'; +import { + ItemMock, + MockBitstream1, + MockBitstream3, + MockBitstream2 +} from '../../shared/mocks/item.mock'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { PaginatedList } from '../data/paginated-list.model'; import { Bitstream } from '../shared/bitstream.model'; @@ -25,7 +30,6 @@ import { getMockStore } from '@ngrx/store/testing'; import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { AppConfig } from '../../../config/app-config.interface'; -import { SignpostingDataService } from '../data/signposting-data.service'; describe('MetadataService', () => { let metadataService: MetadataService; @@ -42,7 +46,6 @@ describe('MetadataService', () => { let translateService: TranslateService; let hardRedirectService: HardRedirectService; let authorizationService: AuthorizationDataService; - let signpostingDataService: SignpostingDataService; let router: Router; let store; @@ -50,12 +53,7 @@ describe('MetadataService', () => { let appConfig: AppConfig; const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] }}}; - const mocklink = { - href: 'http://test.org', - rel: 'test', - type: 'test' - }; - const document: any = jasmine.createSpyObj('document', ['head', 'createElement']); + beforeEach(() => { rootService = jasmine.createSpyObj({ @@ -92,10 +90,6 @@ describe('MetadataService', () => { isAuthorized: observableOf(true) }); - signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { - getLinks: observableOf([mocklink]) - }); - // @ts-ignore store = getMockStore({ initialState }); spyOn(store, 'dispatch'); @@ -121,9 +115,7 @@ describe('MetadataService', () => { store, hardRedirectService, appConfig, - document, - authorizationService, - signpostingDataService + authorizationService ); }); diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index c22f14b680..204c925e6b 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable, Inject } from '@angular/core'; import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; @@ -8,12 +8,12 @@ import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, - concat as observableConcat, - EMPTY, Observable, - of as observableOf + of as observableOf, + concat as observableConcat, + EMPTY } from 'rxjs'; -import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; +import { filter, map, switchMap, take, mergeMap } from 'rxjs/operators'; import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; import { DSONameService } from '../breadcrumbs/dso-name.service'; @@ -25,7 +25,10 @@ import { BitstreamFormat } from '../shared/bitstream-format.model'; import { Bitstream } from '../shared/bitstream.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; -import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../shared/operators'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload +} from '../shared/operators'; import { RootDataService } from '../data/root-data.service'; import { getBitstreamDownloadRoute } from '../../app-routing-paths'; import { BundleDataService } from '../data/bundle-data.service'; @@ -42,8 +45,6 @@ import { CoreState } from '../core-state.model'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { getDownloadableBitstream } from '../shared/bitstream.operators'; import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; -import { SignpostingDataService } from '../data/signposting-data.service'; -import { DOCUMENT } from '@angular/common'; /** * The base selector function to select the metaTag section in the store @@ -62,11 +63,6 @@ const tagsInUseSelector = (state: MetaTagState) => state.tagsInUse, ); -/** - * Link elements added on Item Page - */ -let linkTags = []; - @Injectable() export class MetadataService { @@ -88,7 +84,7 @@ export class MetadataService { ]; constructor( - protected router: Router, + private router: Router, private translate: TranslateService, private meta: Meta, private title: Title, @@ -100,9 +96,7 @@ export class MetadataService { private store: Store, private hardRedirectService: HardRedirectService, @Inject(APP_CONFIG) private appConfig: AppConfig, - @Inject(DOCUMENT) private _document: Document, - private authorizationService: AuthorizationDataService, - private signpostingDataService: SignpostingDataService + private authorizationService: AuthorizationDataService ) { } @@ -123,7 +117,6 @@ export class MetadataService { private processRouteChange(routeInfo: any): void { this.clearMetaTags(); - this.clearLinkTags(); if (hasValue(routeInfo.data.value.dso) && hasValue(routeInfo.data.value.dso.payload)) { this.currentObject.next(routeInfo.data.value.dso.payload); @@ -145,7 +138,7 @@ export class MetadataService { } } - public getCurrentRoute(route: ActivatedRoute): ActivatedRoute { + private getCurrentRoute(route: ActivatedRoute): ActivatedRoute { while (route.firstChild) { route = route.firstChild; } @@ -169,7 +162,6 @@ export class MetadataService { this.setCitationAbstractUrlTag(); this.setCitationPdfUrlTag(); this.setCitationPublisherTag(); - this.setSignpostingLinks(); if (this.isDissertation()) { this.setCitationDissertationNameTag(); @@ -192,37 +184,6 @@ export class MetadataService { } - /** - * Add to the - */ - private setSignpostingLinks() { - if (this.currentObject.value instanceof Item){ - const value = this.signpostingDataService.getLinks(this.currentObject.getValue().id); - value.subscribe(links => { - links.forEach(link => { - this.setLinkTag(link.href, link.rel, link.type); - }); - }); - } - } - - setLinkTag(href: string, rel: string, type: string){ - let link: HTMLLinkElement = this._document.createElement('link'); - if (link) { - link.href = href; - link.rel = rel; - link.type = type; - this._document.head?.appendChild(link); - linkTags.push(link); - } - } - - public clearLinkTags(){ - linkTags.forEach(link => { - link.parentNode?.removeChild(link); - }); - } - /** * Add to the */ diff --git a/src/app/core/services/server-response.service.ts b/src/app/core/services/server-response.service.ts index 6dd50506e9..ffb6a204c6 100644 --- a/src/app/core/services/server-response.service.ts +++ b/src/app/core/services/server-response.service.ts @@ -36,9 +36,10 @@ export class ServerResponseService { return this.setStatus(500, message); } - setLinksetsHeader(linksets: string){ + setHeader(header: string, content: string) { + console.log(this.response); if (this.response) { - this.response.setHeader('Link', linksets); + this.response.setHeader(header, content); } } } diff --git a/src/app/item-page/full/full-item-page.component.spec.ts b/src/app/item-page/full/full-item-page.component.spec.ts index 6c59ccbc67..ec4054d888 100644 --- a/src/app/item-page/full/full-item-page.component.spec.ts +++ b/src/app/item-page/full/full-item-page.component.spec.ts @@ -11,7 +11,7 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { VarDirective } from '../../shared/utils/var.directive'; import { RouterTestingModule } from '@angular/router/testing'; import { Item } from '../../core/shared/item.model'; -import { BehaviorSubject, of as observableOf, of } from 'rxjs'; +import { BehaviorSubject, of as observableOf } from 'rxjs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; @@ -22,6 +22,7 @@ import { createRelationshipsObservable } from '../simple/item-types/shared/item. import { RemoteData } from '../../core/data/remote-data'; import { ServerResponseService } from '../../core/services/server-response.service'; import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -59,6 +60,19 @@ describe('FullItemPageComponent', () => { let authorizationDataService: AuthorizationDataService; let serverResponseService: jasmine.SpyObj; let signpostingDataService: jasmine.SpyObj; + let linkHeadService: jasmine.SpyObj; + + const mocklink = { + href: 'http://test.org', + rel: 'test', + type: 'test' + }; + + const mocklink2 = { + href: 'http://test2.org', + rel: 'test', + type: 'test' + }; beforeEach(waitForAsync(() => { authService = jasmine.createSpyObj('authService', { @@ -79,11 +93,16 @@ describe('FullItemPageComponent', () => { }); serverResponseService = jasmine.createSpyObj('ServerResponseService', { - setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + setHeader: jasmine.createSpy('setHeader'), }); signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { - getLinksets: of('test'), + getLinks: observableOf([mocklink, mocklink2]), + }); + + linkHeadService = jasmine.createSpyObj('LinkHeadService', { + addTag: jasmine.createSpy('setHeader'), + removeTag: jasmine.createSpy('removeTag'), }); TestBed.configureTestingModule({ @@ -102,6 +121,7 @@ describe('FullItemPageComponent', () => { { provide: AuthorizationDataService, useValue: authorizationDataService }, { provide: ServerResponseService, useValue: serverResponseService }, { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(FullItemPageComponent, { @@ -154,6 +174,11 @@ describe('FullItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); expect(objectLoader.nativeElement).not.toBeNull(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); }); describe('when the item is withdrawn and the user is not an admin', () => { beforeEach(() => { @@ -178,6 +203,11 @@ describe('FullItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); expect(objectLoader).not.toBeNull(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); }); describe('when the item is not withdrawn and the user is not an admin', () => { @@ -190,5 +220,10 @@ describe('FullItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); expect(objectLoader).not.toBeNull(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); }); }); diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts index 2570bf70fe..f0100eed72 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -16,8 +16,9 @@ import { hasValue } from '../../shared/empty.util'; import { AuthService } from '../../core/auth/auth.service'; import { Location } from '@angular/common'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { ServerResponseService } from 'src/app/core/services/server-response.service'; -import { SignpostingDataService } from 'src/app/core/data/signposting-data.service'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; /** * This component renders a full item page. @@ -44,15 +45,18 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, subs = []; - constructor(protected route: ActivatedRoute, - router: Router, - items: ItemDataService, - authService: AuthService, - authorizationService: AuthorizationDataService, - private _location: Location, - responseService: ServerResponseService, - signpostingDataService: SignpostingDataService) { - super(route, router, items, authService, authorizationService, responseService, signpostingDataService); + constructor( + protected route: ActivatedRoute, + protected router: Router, + protected items: ItemDataService, + protected authService: AuthService, + protected authorizationService: AuthorizationDataService, + protected _location: Location, + protected responseService: ServerResponseService, + protected signpostingDataService: SignpostingDataService, + protected linkHeadService: LinkHeadService + ) { + super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ diff --git a/src/app/item-page/simple/item-page.component.spec.ts b/src/app/item-page/simple/item-page.component.spec.ts index 042cac2724..005142e3f1 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -22,9 +22,9 @@ import { import { AuthService } from '../../core/auth/auth.service'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { of } from 'rxjs/internal/observable/of'; import { ServerResponseService } from '../../core/services/server-response.service'; import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -39,6 +39,18 @@ const mockWithdrawnItem: Item = Object.assign(new Item(), { isWithdrawn: true }); +const mocklink = { + href: 'http://test.org', + rel: 'test', + type: 'test' +}; + +const mocklink2 = { + href: 'http://test2.org', + rel: 'test', + type: 'test' +}; + describe('ItemPageComponent', () => { let comp: ItemPageComponent; let fixture: ComponentFixture; @@ -46,6 +58,7 @@ describe('ItemPageComponent', () => { let authorizationDataService: AuthorizationDataService; let serverResponseService: jasmine.SpyObj; let signpostingDataService: jasmine.SpyObj; + let linkHeadService: jasmine.SpyObj; const mockMetadataService = { /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ @@ -66,11 +79,16 @@ describe('ItemPageComponent', () => { isAuthorized: observableOf(false), }); serverResponseService = jasmine.createSpyObj('ServerResponseService', { - setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + setHeader: jasmine.createSpy('setHeader'), }); signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { - getLinksets: of('test'), + getLinks: observableOf([mocklink, mocklink2]), + }); + + linkHeadService = jasmine.createSpyObj('LinkHeadService', { + addTag: jasmine.createSpy('setHeader'), + removeTag: jasmine.createSpy('removeTag'), }); TestBed.configureTestingModule({ @@ -90,6 +108,7 @@ describe('ItemPageComponent', () => { { provide: AuthorizationDataService, useValue: authorizationDataService }, { provide: ServerResponseService, useValue: serverResponseService }, { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, ], schemas: [NO_ERRORS_SCHEMA] @@ -140,6 +159,12 @@ describe('ItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); expect(objectLoader.nativeElement).toBeDefined(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); + }); describe('when the item is withdrawn and the user is not an admin', () => { beforeEach(() => { @@ -164,6 +189,11 @@ describe('ItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); expect(objectLoader.nativeElement).toBeDefined(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); }); describe('when the item is not withdrawn and the user is not an admin', () => { @@ -176,6 +206,11 @@ describe('ItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); expect(objectLoader.nativeElement).toBeDefined(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); }); }); diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index 6483769483..10d8b32886 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -1,8 +1,8 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, take } from 'rxjs/operators'; import { ItemDataService } from '../../core/data/item-data.service'; import { RemoteData } from '../../core/data/remote-data'; @@ -17,6 +17,9 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/ import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { ServerResponseService } from '../../core/services/server-response.service'; import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { SignpostingLink } from '../../core/data/signposting-links.model'; +import { isNotEmpty } from '../../shared/empty.util'; +import { LinkHeadService } from '../../core/services/link-head.service'; /** * This component renders a simple item page. @@ -30,7 +33,7 @@ import { SignpostingDataService } from '../../core/data/signposting-data.service changeDetection: ChangeDetectionStrategy.OnPush, animations: [fadeInOut] }) -export class ItemPageComponent implements OnInit { +export class ItemPageComponent implements OnInit, OnDestroy { /** * The item's id @@ -59,18 +62,36 @@ export class ItemPageComponent implements OnInit { itemUrl: string; + /** + * Contains a list of SignpostingLink related to the item + */ + signpostingLinks: SignpostingLink[]; + constructor( protected route: ActivatedRoute, - private router: Router, - private items: ItemDataService, - private authService: AuthService, - private authorizationService: AuthorizationDataService, - private responseService: ServerResponseService, - private signpostingDataService: SignpostingDataService + protected router: Router, + protected items: ItemDataService, + protected authService: AuthService, + protected authorizationService: AuthorizationDataService, + protected responseService: ServerResponseService, + protected signpostingDataService: SignpostingDataService, + protected linkHeadService: LinkHeadService ) { this.route.params.subscribe(params => { - this.signpostingDataService.getLinksets(params.id).subscribe(linksets => { - this.responseService.setLinksetsHeader(linksets); + this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { + let links = ''; + this.signpostingLinks = signpostingLinks; + + signpostingLinks.forEach((link: SignpostingLink) => { + links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; + this.linkHeadService.addTag({ + href: link.href, + type: link.type, + rel: link.rel + }) + }); + + this.responseService.setHeader('Link', links); }); }); } @@ -91,4 +112,11 @@ export class ItemPageComponent implements OnInit { this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf); } + + + ngOnDestroy(): void { + this.signpostingLinks.forEach((link: SignpostingLink) => { + this.linkHeadService.removeTag(`href='${link.href}'`); + }) + } } From a1b27a5fb361db209721bbc59e955782175699ec Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 15 May 2023 16:34:02 +0200 Subject: [PATCH 12/30] [CST-5729] Revert unused changes --- src/app/core/dspace-rest/dspace-rest.service.ts | 15 +-------------- src/app/core/services/server-response.service.ts | 1 - 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/app/core/dspace-rest/dspace-rest.service.ts b/src/app/core/dspace-rest/dspace-rest.service.ts index 737714869d..ea4e8c2831 100644 --- a/src/app/core/dspace-rest/dspace-rest.service.ts +++ b/src/app/core/dspace-rest/dspace-rest.service.ts @@ -1,4 +1,4 @@ -import { Observable, throwError as observableThrowError, throwError } from 'rxjs'; +import { Observable, throwError as observableThrowError } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http'; @@ -58,19 +58,6 @@ export class DspaceRestService { })); } - getWithHeaders(absoluteURL: string, reqOptions: any): Observable { - const requestOptions = reqOptions; - - return this.http.get(absoluteURL, requestOptions).pipe( - map((res) => ({ - payload: res - })), - catchError((err) => { - console.log('Error: ', err); - return throwError(() => new Error(err.error)); - })); - } - /** * Performs a request to the REST API. * diff --git a/src/app/core/services/server-response.service.ts b/src/app/core/services/server-response.service.ts index ffb6a204c6..2268e9eb03 100644 --- a/src/app/core/services/server-response.service.ts +++ b/src/app/core/services/server-response.service.ts @@ -37,7 +37,6 @@ export class ServerResponseService { } setHeader(header: string, content: string) { - console.log(this.response); if (this.response) { this.response.setHeader(header, content); } From 10c6b03c40af72e70a897d1fd460e8c39ea5ebdb Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 15 May 2023 16:58:20 +0200 Subject: [PATCH 13/30] [CST-5729] Fix lint --- src/app/item-page/simple/item-page.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index 10d8b32886..c1c09e0eb4 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -88,7 +88,7 @@ export class ItemPageComponent implements OnInit, OnDestroy { href: link.href, type: link.type, rel: link.rel - }) + }); }); this.responseService.setHeader('Link', links); @@ -117,6 +117,6 @@ export class ItemPageComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.signpostingLinks.forEach((link: SignpostingLink) => { this.linkHeadService.removeTag(`href='${link.href}'`); - }) + }); } } From 43d78c695c7e6f763a8235361cdade0048a77e5e Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 15 May 2023 17:11:45 +0200 Subject: [PATCH 14/30] [CST-5729] Assign a default value --- src/app/item-page/simple/item-page.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index c1c09e0eb4..f5ee9e1e78 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -65,7 +65,7 @@ export class ItemPageComponent implements OnInit, OnDestroy { /** * Contains a list of SignpostingLink related to the item */ - signpostingLinks: SignpostingLink[]; + signpostingLinks: SignpostingLink[] = []; constructor( protected route: ActivatedRoute, From 220b30bea7661b1e7af58611bd3ff3531cdb0421 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 25 May 2023 23:15:57 +0200 Subject: [PATCH 15/30] [CST-5729] Address review feedback --- .../bitstream-download-page.component.spec.ts | 4 +- .../bitstream-download-page.component.ts | 40 +++++++++------ src/app/core/data/signposting-data.service.ts | 8 +++ .../core/services/server-response.service.ts | 36 +++++++++++++ src/app/init.service.ts | 1 - .../full/full-item-page.component.spec.ts | 3 +- .../full/full-item-page.component.ts | 7 +-- .../simple/item-page.component.spec.ts | 32 +++++++++--- .../item-page/simple/item-page.component.ts | 50 ++++++++++++------- 9 files changed, 136 insertions(+), 45 deletions(-) diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts index 66024063cd..59261e56d2 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts @@ -13,6 +13,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; import { SignpostingDataService } from '../../core/data/signposting-data.service'; import { ServerResponseService } from '../../core/services/server-response.service'; +import { PLATFORM_ID } from '@angular/core'; describe('BitstreamDownloadPageComponent', () => { let component: BitstreamDownloadPageComponent; @@ -99,7 +100,8 @@ describe('BitstreamDownloadPageComponent', () => { { provide: FileService, useValue: fileService }, { provide: HardRedirectService, useValue: hardRedirectService }, { provide: ServerResponseService, useValue: serverResponseService }, - { provide: SignpostingDataService, useValue: signpostingDataService } + { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: PLATFORM_ID, useValue: 'server' } ] }) .compileComponents(); diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts index 14245c4cd1..0becfcf473 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core'; import { filter, map, switchMap, take } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; @@ -13,7 +13,7 @@ import { HardRedirectService } from '../../core/services/hard-redirect.service'; import { getForbiddenRoute } from '../../app-routing-paths'; import { RemoteData } from '../../core/data/remote-data'; import { redirectOn4xx } from '../../core/shared/authorized.operators'; -import { Location } from '@angular/common'; +import { isPlatformServer, Location } from '@angular/common'; import { SignpostingDataService } from 'src/app/core/data/signposting-data.service'; import { ServerResponseService } from 'src/app/core/services/server-response.service'; import { SignpostingLink } from '../../core/data/signposting-links.model'; @@ -39,19 +39,10 @@ export class BitstreamDownloadPageComponent implements OnInit { private hardRedirectService: HardRedirectService, private location: Location, private signpostingDataService: SignpostingDataService, - private responseService: ServerResponseService + private responseService: ServerResponseService, + @Inject(PLATFORM_ID) protected platformId: string ) { - this.route.params.subscribe(params => { - this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { - let links = ''; - - signpostingLinks.forEach((link: SignpostingLink) => { - links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; - }); - - this.responseService.setHeader('Link', links); - }); - }); + this.initPageLinks(); } back(): void { @@ -101,4 +92,25 @@ export class BitstreamDownloadPageComponent implements OnInit { } }); } + + /** + * Create page links if any are retrieved by signposting endpoint + * + * @private + */ + private initPageLinks(): void { + if (isPlatformServer(this.platformId)) { + this.route.params.subscribe(params => { + this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { + let links = ''; + + signpostingLinks.forEach((link: SignpostingLink) => { + links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; + }); + + this.responseService.setHeader('Link', links); + }); + }); + } + } } diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index e09d68974c..fca22ec383 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -8,6 +8,9 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { SignpostingLink } from './signposting-links.model'; +/** + * Service responsible for handling requests related to the Signposting endpoint + */ @Injectable({ providedIn: 'root' }) @@ -16,6 +19,11 @@ export class SignpostingDataService { constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { } + /** + * Retrieve the list of signposting links related to the given resource's id + * + * @param uuid + */ getLinks(uuid: string): Observable { const baseUrl = this.halService.getRootHref().replace('/api', ''); diff --git a/src/app/core/services/server-response.service.ts b/src/app/core/services/server-response.service.ts index 2268e9eb03..0b193d536c 100644 --- a/src/app/core/services/server-response.service.ts +++ b/src/app/core/services/server-response.service.ts @@ -1,7 +1,11 @@ import { RESPONSE } from '@nguniversal/express-engine/tokens'; import { Inject, Injectable, Optional } from '@angular/core'; + import { Response } from 'express'; +/** + * Service responsible to provide method to manage the response object + */ @Injectable() export class ServerResponseService { private response: Response; @@ -10,6 +14,12 @@ export class ServerResponseService { this.response = response; } + /** + * Set a status code to response + * + * @param code + * @param message + */ setStatus(code: number, message?: string): this { if (this.response) { this.response.statusCode = code; @@ -20,22 +30,48 @@ export class ServerResponseService { return this; } + /** + * Set Unauthorized status + * + * @param message + */ setUnauthorized(message = 'Unauthorized'): this { return this.setStatus(401, message); } + /** + * Set Forbidden status + * + * @param message + */ setForbidden(message = 'Forbidden'): this { return this.setStatus(403, message); } + /** + * Set Not found status + * + * @param message + */ setNotFound(message = 'Not found'): this { return this.setStatus(404, message); } + /** + * Set Internal Server Error status + * + * @param message + */ setInternalServerError(message = 'Internal Server Error'): this { return this.setStatus(500, message); } + /** + * Set a response's header + * + * @param header + * @param content + */ setHeader(header: string, content: string) { if (this.response) { this.response.setHeader(header, content); diff --git a/src/app/init.service.ts b/src/app/init.service.ts index 2bbc589cc0..9fef2ca4ed 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -188,7 +188,6 @@ export abstract class InitService { this.breadcrumbsService.listenForRouteChanges(); this.themeService.listenForRouteChanges(); this.menuService.listenForRouteChanges(); - // this.metadataItem.checkCurrentRoute(); } /** diff --git a/src/app/item-page/full/full-item-page.component.spec.ts b/src/app/item-page/full/full-item-page.component.spec.ts index ec4054d888..9fc078c2cd 100644 --- a/src/app/item-page/full/full-item-page.component.spec.ts +++ b/src/app/item-page/full/full-item-page.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/cor import { ItemDataService } from '../../core/data/item-data.service'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core'; import { TruncatePipe } from '../../shared/utils/truncate.pipe'; import { FullItemPageComponent } from './full-item-page.component'; import { MetadataService } from '../../core/metadata/metadata.service'; @@ -122,6 +122,7 @@ describe('FullItemPageComponent', () => { { provide: ServerResponseService, useValue: serverResponseService }, { provide: SignpostingDataService, useValue: signpostingDataService }, { provide: LinkHeadService, useValue: linkHeadService }, + { provide: PLATFORM_ID, useValue: 'server' } ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(FullItemPageComponent, { diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts index f0100eed72..31dd2c5fc2 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -1,5 +1,5 @@ import { filter, map } from 'rxjs/operators'; -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { ActivatedRoute, Data, Router } from '@angular/router'; import { BehaviorSubject, Observable } from 'rxjs'; @@ -54,9 +54,10 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, protected _location: Location, protected responseService: ServerResponseService, protected signpostingDataService: SignpostingDataService, - protected linkHeadService: LinkHeadService + protected linkHeadService: LinkHeadService, + @Inject(PLATFORM_ID) protected platformId: string, ) { - super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService); + super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService, platformId); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ diff --git a/src/app/item-page/simple/item-page.component.spec.ts b/src/app/item-page/simple/item-page.component.spec.ts index 005142e3f1..dfba4bd235 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { ItemDataService } from '../../core/data/item-data.service'; -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core'; import { ItemPageComponent } from './item-page.component'; import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; @@ -24,7 +24,8 @@ import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { ServerResponseService } from '../../core/services/server-response.service'; import { SignpostingDataService } from '../../core/data/signposting-data.service'; -import { LinkHeadService } from '../../core/services/link-head.service'; +import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service'; +import { SignpostingLink } from '../../core/data/signposting-links.model'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -41,16 +42,18 @@ const mockWithdrawnItem: Item = Object.assign(new Item(), { const mocklink = { href: 'http://test.org', - rel: 'test', - type: 'test' + rel: 'rel1', + type: 'type1' }; const mocklink2 = { href: 'http://test2.org', - rel: 'test', - type: 'test' + rel: 'rel2', + type: 'type2' }; +const mockSignpostingLinks: SignpostingLink[] = [mocklink, mocklink2]; + describe('ItemPageComponent', () => { let comp: ItemPageComponent; let fixture: ComponentFixture; @@ -109,6 +112,7 @@ describe('ItemPageComponent', () => { { provide: ServerResponseService, useValue: serverResponseService }, { provide: SignpostingDataService, useValue: signpostingDataService }, { provide: LinkHeadService, useValue: linkHeadService }, + { provide: PLATFORM_ID, useValue: 'server' }, ], schemas: [NO_ERRORS_SCHEMA] @@ -165,6 +169,22 @@ describe('ItemPageComponent', () => { expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); }); + + it('should add link tags correctly', () => { + + expect(comp.signpostingLinks).toEqual([mocklink, mocklink2]); + + // Check if linkHeadService.addTag() was called with the correct arguments + expect(linkHeadService.addTag).toHaveBeenCalledTimes(mockSignpostingLinks.length); + expect(linkHeadService.addTag).toHaveBeenCalledWith(mockSignpostingLinks[0] as LinkDefinition); + expect(linkHeadService.addTag).toHaveBeenCalledWith(mockSignpostingLinks[1] as LinkDefinition); + }); + + it('should set Link header on the server', () => { + + expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', ' ; rel="rel1" ; type="type1" , ; rel="rel2" ; type="type2" '); + }); + }); describe('when the item is withdrawn and the user is not an admin', () => { beforeEach(() => { diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index f5ee9e1e78..a11cb22883 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -1,5 +1,6 @@ -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { isPlatformServer } from '@angular/common'; import { Observable } from 'rxjs'; import { map, take } from 'rxjs/operators'; @@ -75,25 +76,10 @@ export class ItemPageComponent implements OnInit, OnDestroy { protected authorizationService: AuthorizationDataService, protected responseService: ServerResponseService, protected signpostingDataService: SignpostingDataService, - protected linkHeadService: LinkHeadService + protected linkHeadService: LinkHeadService, + @Inject(PLATFORM_ID) protected platformId: string ) { - this.route.params.subscribe(params => { - this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { - let links = ''; - this.signpostingLinks = signpostingLinks; - - signpostingLinks.forEach((link: SignpostingLink) => { - links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; - this.linkHeadService.addTag({ - href: link.href, - type: link.type, - rel: link.rel - }); - }); - - this.responseService.setHeader('Link', links); - }); - }); + this.initPageLinks(); } /** @@ -113,6 +99,32 @@ export class ItemPageComponent implements OnInit, OnDestroy { } + /** + * Create page links if any are retrieved by signposting endpoint + * + * @private + */ + private initPageLinks(): void { + this.route.params.subscribe(params => { + this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { + let links = ''; + this.signpostingLinks = signpostingLinks; + + signpostingLinks.forEach((link: SignpostingLink) => { + links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; + this.linkHeadService.addTag({ + href: link.href, + type: link.type, + rel: link.rel + }); + }); + + if (isPlatformServer(this.platformId)) { + this.responseService.setHeader('Link', links); + } + }); + }); + } ngOnDestroy(): void { this.signpostingLinks.forEach((link: SignpostingLink) => { From 092608f6cbb114a81a34a685fefec8e5cbed2bed Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 5 Jun 2023 15:11:01 +0200 Subject: [PATCH 16/30] [CST-5729] Remove additional error message --- src/app/core/data/signposting-data.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index fca22ec383..d051ecf8db 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -29,7 +29,6 @@ export class SignpostingDataService { return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( catchError((err) => { - console.error(err); return observableOf([]); }), map((res: RawRestResponse) => res.statusCode === 200 ? res.payload as SignpostingLink[] : []) From 3611f375632682fe1137a3b56dbffe903b2577c4 Mon Sep 17 00:00:00 2001 From: Alisa Ismailati Date: Tue, 6 Jun 2023 14:42:49 +0200 Subject: [PATCH 17/30] [DURACOM-151] Fixed EPerson deletion --- .../epeople-registry.component.ts | 17 +++-- .../eperson-form/eperson-form.component.ts | 65 +++++++++++-------- src/app/core/data/request.service.ts | 4 +- 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.ts b/src/app/access-control/epeople-registry/epeople-registry.component.ts index 706dcab690..fb045ebb88 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.ts @@ -287,14 +287,17 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { /** * This method will set everything to stale, which will cause the lists on this page to update. */ - reset() { + reset(): void { this.epersonService.getBrowseEndpoint().pipe( - take(1) - ).subscribe((href: string) => { - this.requestService.setStaleByHrefSubstring(href).pipe(take(1)).subscribe(() => { - this.epersonService.cancelEditEPerson(); - this.isEPersonFormShown = false; - }); + take(1), + switchMap((href: string) => { + return this.requestService.setStaleByHrefSubstring(href).pipe( + take(1), + ); + }) + ).subscribe(()=>{ + this.epersonService.cancelEditEPerson(); + this.isEPersonFormShown = false; }); } } diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts index c60de00aed..1fd0e05ccb 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -8,7 +8,7 @@ import { } from '@ng-dynamic-forms/core'; import { TranslateService } from '@ngx-translate/core'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; -import { debounceTime, switchMap, take } from 'rxjs/operators'; +import { debounceTime, finalize, map, switchMap, take } from 'rxjs/operators'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; @@ -463,31 +463,43 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Deletes the EPerson from the Repository. The EPerson will be the only that this form is showing. * It'll either show a success or error message depending on whether the delete was successful or not. */ - delete() { - this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => { - const modalRef = this.modalService.open(ConfirmationModalComponent); - modalRef.componentInstance.dso = eperson; - modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header'; - modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info'; - modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel'; - modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm'; - modalRef.componentInstance.brandColor = 'danger'; - modalRef.componentInstance.confirmIcon = 'fas fa-trash'; - modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => { - if (confirm) { - if (hasValue(eperson.id)) { - this.epersonService.deleteEPerson(eperson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData) => { - if (restResponse.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) })); - this.submitForm.emit(); - } else { - this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage); - } - this.cancelForm.emit(); - }); - } - } - }); + delete(): void { + this.epersonService.getActiveEPerson().pipe( + take(1), + switchMap((eperson: EPerson) => { + const modalRef = this.modalService.open(ConfirmationModalComponent); + modalRef.componentInstance.dso = eperson; + modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header'; + modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info'; + modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel'; + modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm'; + modalRef.componentInstance.brandColor = 'danger'; + modalRef.componentInstance.confirmIcon = 'fas fa-trash'; + + return modalRef.componentInstance.response.pipe( + take(1), + switchMap((confirm: boolean) => { + if (confirm && hasValue(eperson.id)) { + this.canDelete$ = observableOf(false); + return this.epersonService.deleteEPerson(eperson).pipe( + getFirstCompletedRemoteData(), + map((restResponse: RemoteData) => ({ restResponse, eperson })) + ); + } else { + return observableOf(null); + } + }), + finalize(() => this.canDelete$ = observableOf(true)) + ); + }) + ).subscribe(({ restResponse, eperson }: { restResponse: RemoteData | null, eperson: EPerson }) => { + if (restResponse?.hasSucceeded) { + this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) })); + this.submitForm.emit(); + } else { + this.notificationsService.error(`Error occurred when trying to delete EPerson with id: ${eperson?.id} with code: ${restResponse?.statusCode} and message: ${restResponse?.errorMessage}`); + } + this.cancelForm.emit(); }); } @@ -523,7 +535,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Cancel the current edit when component is destroyed & unsub all subscriptions */ ngOnDestroy(): void { - this.onCancel(); this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); this.paginationService.clearPagination(this.config.id); if (hasValue(this.emailValueChangeSubscribe)) { diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 94a6020975..78f53ffe3a 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -328,10 +328,10 @@ export class RequestService { this.store.dispatch(new RequestStaleAction(uuid)); return this.getByUUID(uuid).pipe( + take(1), map((request: RequestEntry) => isStale(request.state)), filter((stale: boolean) => stale), - take(1), - ); + ); } /** From 1b2d9829edf11c8d4b427981f2e0dad2dbac5abe Mon Sep 17 00:00:00 2001 From: Alisa Ismailati Date: Tue, 6 Jun 2023 15:35:54 +0200 Subject: [PATCH 18/30] [DURACOM-151] unit test fix --- src/app/core/data/request.service.spec.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index 108a588881..8509f60eb7 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -627,11 +627,8 @@ describe('RequestService', () => { it('should return an Observable that emits true as soon as the request is stale', fakeAsync(() => { dispatchSpy.and.callFake(() => { /* empty */ }); // don't actually set as stale - getByUUIDSpy.and.returnValue(cold('a-b--c--d-', { // but fake the state in the cache - a: { state: RequestEntryState.ResponsePending }, - b: { state: RequestEntryState.Success }, - c: { state: RequestEntryState.SuccessStale }, - d: { state: RequestEntryState.Error }, + getByUUIDSpy.and.returnValue(cold('-----(a|)', { // but fake the state in the cache + a: { state: RequestEntryState.SuccessStale }, })); const done$ = service.setStaleByUUID('something'); From b1aa2f3550b5ab2aa01736a99d3eb307f5e7b004 Mon Sep 17 00:00:00 2001 From: Alisa Ismailati Date: Tue, 6 Jun 2023 16:57:17 +0200 Subject: [PATCH 19/30] [DURACOM-151] reverted setStaleByUUID method as it was --- .../eperson-form/eperson-form.component.ts | 1 - src/app/core/data/request.service.spec.ts | 7 +++++-- src/app/core/data/request.service.ts | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts index 1fd0e05ccb..d009d56058 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -495,7 +495,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy { ).subscribe(({ restResponse, eperson }: { restResponse: RemoteData | null, eperson: EPerson }) => { if (restResponse?.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) })); - this.submitForm.emit(); } else { this.notificationsService.error(`Error occurred when trying to delete EPerson with id: ${eperson?.id} with code: ${restResponse?.statusCode} and message: ${restResponse?.errorMessage}`); } diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index 8509f60eb7..108a588881 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -627,8 +627,11 @@ describe('RequestService', () => { it('should return an Observable that emits true as soon as the request is stale', fakeAsync(() => { dispatchSpy.and.callFake(() => { /* empty */ }); // don't actually set as stale - getByUUIDSpy.and.returnValue(cold('-----(a|)', { // but fake the state in the cache - a: { state: RequestEntryState.SuccessStale }, + getByUUIDSpy.and.returnValue(cold('a-b--c--d-', { // but fake the state in the cache + a: { state: RequestEntryState.ResponsePending }, + b: { state: RequestEntryState.Success }, + c: { state: RequestEntryState.SuccessStale }, + d: { state: RequestEntryState.Error }, })); const done$ = service.setStaleByUUID('something'); diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 78f53ffe3a..1f6680203e 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -328,9 +328,9 @@ export class RequestService { this.store.dispatch(new RequestStaleAction(uuid)); return this.getByUUID(uuid).pipe( - take(1), map((request: RequestEntry) => isStale(request.state)), filter((stale: boolean) => stale), + take(1), ); } From 02bb7db1190c54be1f2a00608c624fdf67804f23 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 8 Jun 2023 13:15:02 +0200 Subject: [PATCH 20/30] [CST-5729] Fix issue with undefined link type --- .../bitstream-download-page.component.ts | 1 + .../item-page/simple/item-page.component.spec.ts | 13 +++++++++---- src/app/item-page/simple/item-page.component.ts | 15 ++++++++++----- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts index 4d05511ca0..cf8d8e7767 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts @@ -107,6 +107,7 @@ export class BitstreamDownloadPageComponent implements OnInit { let links = ''; signpostingLinks.forEach((link: SignpostingLink) => { + links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}"` + (isNotEmpty(link.type) ? ` ; type="${link.type}" ` : ' '); links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; }); diff --git a/src/app/item-page/simple/item-page.component.spec.ts b/src/app/item-page/simple/item-page.component.spec.ts index dfba4bd235..b3202108f4 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -49,7 +49,7 @@ const mocklink = { const mocklink2 = { href: 'http://test2.org', rel: 'rel2', - type: 'type2' + type: undefined }; const mockSignpostingLinks: SignpostingLink[] = [mocklink, mocklink2]; @@ -176,13 +176,18 @@ describe('ItemPageComponent', () => { // Check if linkHeadService.addTag() was called with the correct arguments expect(linkHeadService.addTag).toHaveBeenCalledTimes(mockSignpostingLinks.length); - expect(linkHeadService.addTag).toHaveBeenCalledWith(mockSignpostingLinks[0] as LinkDefinition); - expect(linkHeadService.addTag).toHaveBeenCalledWith(mockSignpostingLinks[1] as LinkDefinition); + let expected: LinkDefinition = mockSignpostingLinks[0] as LinkDefinition; + expect(linkHeadService.addTag).toHaveBeenCalledWith(expected); + expected = { + href: 'http://test2.org', + rel: 'rel2' + }; + expect(linkHeadService.addTag).toHaveBeenCalledWith(expected); }); it('should set Link header on the server', () => { - expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', ' ; rel="rel1" ; type="type1" , ; rel="rel2" ; type="type2" '); + expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', ' ; rel="rel1" ; type="type1" , ; rel="rel2" '); }); }); diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index a11cb22883..b9be6bebfb 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -20,7 +20,7 @@ import { ServerResponseService } from '../../core/services/server-response.servi import { SignpostingDataService } from '../../core/data/signposting-data.service'; import { SignpostingLink } from '../../core/data/signposting-links.model'; import { isNotEmpty } from '../../shared/empty.util'; -import { LinkHeadService } from '../../core/services/link-head.service'; +import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service'; /** * This component renders a simple item page. @@ -111,12 +111,17 @@ export class ItemPageComponent implements OnInit, OnDestroy { this.signpostingLinks = signpostingLinks; signpostingLinks.forEach((link: SignpostingLink) => { - links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; - this.linkHeadService.addTag({ + links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}"` + (isNotEmpty(link.type) ? ` ; type="${link.type}" ` : ' '); + let tag: LinkDefinition = { href: link.href, - type: link.type, rel: link.rel - }); + }; + if (isNotEmpty(link.type)) { + tag = Object.assign(tag, { + type: link.type + }); + } + this.linkHeadService.addTag(tag); }); if (isPlatformServer(this.platformId)) { From 96903d89dedfc6191fc71b5b1b81253783459d74 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 9 Jun 2023 19:24:16 +0200 Subject: [PATCH 21/30] [CST-5729] fix signposting proxy url --- server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.ts b/server.ts index 282b1ce29a..d64b80b4ab 100644 --- a/server.ts +++ b/server.ts @@ -182,8 +182,8 @@ export function app() { /** * Proxy the linksets */ - router.use('/links**', createProxyMiddleware({ - target: `${environment.rest.baseUrl}/signposting`, + router.use('/signposting**', createProxyMiddleware({ + target: `${environment.rest.baseUrl}`, pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), changeOrigin: true })); From b8d282ebe4ba14765e2cb5d38817934e12f1bb98 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Sun, 11 Jun 2023 16:17:14 +0200 Subject: [PATCH 22/30] Fix proxy timeout error for browse by pages --- src/app/browse-by/browse-by-guard.spec.ts | 35 +++++++++++++++-- src/app/browse-by/browse-by-guard.ts | 48 ++++++++++++++--------- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/app/browse-by/browse-by-guard.spec.ts b/src/app/browse-by/browse-by-guard.spec.ts index fc483d87e2..7f57c17ac1 100644 --- a/src/app/browse-by/browse-by-guard.spec.ts +++ b/src/app/browse-by/browse-by-guard.spec.ts @@ -1,10 +1,10 @@ import { first } from 'rxjs/operators'; import { BrowseByGuard } from './browse-by-guard'; import { of as observableOf } from 'rxjs'; -import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service'; -import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { BrowseDefinition } from '../core/shared/browse-definition.model'; import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator'; +import { RouterStub } from '../shared/testing/router.stub'; describe('BrowseByGuard', () => { describe('canActivate', () => { @@ -12,6 +12,7 @@ describe('BrowseByGuard', () => { let dsoService: any; let translateService: any; let browseDefinitionService: any; + let router: any; const name = 'An interesting DSO'; const title = 'Author'; @@ -34,7 +35,9 @@ describe('BrowseByGuard', () => { findById: () => createSuccessfulRemoteDataObject$(browseDefinition) }; - guard = new BrowseByGuard(dsoService, translateService, browseDefinitionService); + router = new RouterStub() as any; + + guard = new BrowseByGuard(dsoService, translateService, browseDefinitionService, router); }); it('should return true, and sets up the data correctly, with a scope and value', () => { @@ -64,6 +67,7 @@ describe('BrowseByGuard', () => { value: '"' + value + '"' }; expect(scopedRoute.data).toEqual(result); + expect(router.navigate).not.toHaveBeenCalled(); expect(canActivate).toEqual(true); } ); @@ -96,6 +100,7 @@ describe('BrowseByGuard', () => { value: '' }; expect(scopedNoValueRoute.data).toEqual(result); + expect(router.navigate).not.toHaveBeenCalled(); expect(canActivate).toEqual(true); } ); @@ -127,9 +132,33 @@ describe('BrowseByGuard', () => { value: '"' + value + '"' }; expect(route.data).toEqual(result); + expect(router.navigate).not.toHaveBeenCalled(); expect(canActivate).toEqual(true); } ); }); + + it('should return false, and sets up the data correctly, without a scope and with a value', () => { + jasmine.getEnv().allowRespy(true); + spyOn(browseDefinitionService, 'findById').and.returnValue(createFailedRemoteDataObject$()); + const scopedRoute = { + data: { + title: field, + }, + params: { + id, + }, + queryParams: { + scope, + value + } + }; + guard.canActivate(scopedRoute as any, undefined) + .pipe(first()) + .subscribe((canActivate) => { + expect(router.navigate).toHaveBeenCalled(); + expect(canActivate).toEqual(false); + }); + }); }); }); diff --git a/src/app/browse-by/browse-by-guard.ts b/src/app/browse-by/browse-by-guard.ts index e4582cb77a..ed6a627558 100644 --- a/src/app/browse-by/browse-by-guard.ts +++ b/src/app/browse-by/browse-by-guard.ts @@ -1,13 +1,15 @@ -import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; import { Injectable } from '@angular/core'; import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service'; import { hasNoValue, hasValue } from '../shared/empty.util'; import { map, switchMap } from 'rxjs/operators'; -import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, } from '../core/shared/operators'; import { TranslateService } from '@ngx-translate/core'; import { Observable, of as observableOf } from 'rxjs'; import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service'; import { BrowseDefinition } from '../core/shared/browse-definition.model'; +import { RemoteData } from '../core/data/remote-data'; +import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths'; @Injectable() /** @@ -17,15 +19,20 @@ export class BrowseByGuard implements CanActivate { constructor(protected dsoService: DSpaceObjectDataService, protected translate: TranslateService, - protected browseDefinitionService: BrowseDefinitionDataService) { + protected browseDefinitionService: BrowseDefinitionDataService, + protected router: Router, + ) { } - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { const title = route.data.title; const id = route.params.id || route.queryParams.id || route.data.id; - let browseDefinition$: Observable; + let browseDefinition$: Observable; if (hasNoValue(route.data.browseDefinition) && hasValue(id)) { - browseDefinition$ = this.browseDefinitionService.findById(id).pipe(getFirstSucceededRemoteDataPayload()); + browseDefinition$ = this.browseDefinitionService.findById(id).pipe( + getFirstCompletedRemoteData(), + map((browseDefinitionRD: RemoteData) => browseDefinitionRD.payload), + ); } else { browseDefinition$ = observableOf(route.data.browseDefinition); } @@ -33,19 +40,24 @@ export class BrowseByGuard implements CanActivate { const value = route.queryParams.value; const metadataTranslated = this.translate.instant('browse.metadata.' + id); return browseDefinition$.pipe( - switchMap((browseDefinition) => { - if (hasValue(scope)) { - const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteData()); - return dsoAndMetadata$.pipe( - map((dsoRD) => { - const name = dsoRD.payload.name; - route.data = this.createData(title, id, browseDefinition, name, metadataTranslated, value, route); - return true; - }) - ); + switchMap((browseDefinition: BrowseDefinition | undefined) => { + if (hasValue(browseDefinition)) { + if (hasValue(scope)) { + const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteData()); + return dsoAndMetadata$.pipe( + map((dsoRD) => { + const name = dsoRD.payload.name; + route.data = this.createData(title, id, browseDefinition, name, metadataTranslated, value, route); + return true; + }) + ); + } else { + route.data = this.createData(title, id, browseDefinition, '', metadataTranslated, value, route); + return observableOf(true); + } } else { - route.data = this.createData(title, id, browseDefinition, '', metadataTranslated, value, route); - return observableOf(true); + void this.router.navigate([PAGE_NOT_FOUND_PATH]); + return observableOf(false); } }) ); From 58a3ec397293b57b030f15ffc4d89bbc7ef3f89d Mon Sep 17 00:00:00 2001 From: enea4science <127771679+enea4science@users.noreply.github.com> Date: Mon, 12 Jun 2023 09:50:26 +0200 Subject: [PATCH 23/30] Update orcid-sync-settings.component.ts --- .../orcid-sync-settings/orcid-sync-settings.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts index 1aec416d62..0bcbc295ac 100644 --- a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts @@ -156,8 +156,7 @@ export class OrcidSyncSettingsComponent implements OnInit { } }), ).subscribe((remoteData: RemoteData) => { - // hasSucceeded is true if the response is success or successStale - if (remoteData.hasSucceeded) { + if (remoteData.isSuccess) { this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success')); this.settingsUpdated.emit(); } else { From e85f9f2b255ccbd6169b6394d93efe692917dd95 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 12 Jun 2023 14:40:21 +0200 Subject: [PATCH 24/30] [CST-5729] fix issue with signposting endpoint url replace --- src/app/core/data/signposting-data.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index d051ecf8db..34d3ffdab9 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -25,7 +25,8 @@ export class SignpostingDataService { * @param uuid */ getLinks(uuid: string): Observable { - const baseUrl = this.halService.getRootHref().replace('/api', ''); + const regex = /\/api$/gm; + const baseUrl = this.halService.getRootHref().replace(regex, ''); return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( catchError((err) => { From 5d6edade22ff116c980465bc322ef4bf56772475 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 12 Jun 2023 15:22:33 +0200 Subject: [PATCH 25/30] [CST-5729] turn to use app config baseurl --- .../data/signposting-data.service.spec.ts | 26 +++++++++---------- src/app/core/data/signposting-data.service.ts | 9 +++---- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/app/core/data/signposting-data.service.spec.ts b/src/app/core/data/signposting-data.service.spec.ts index c76899221e..f34ce6538f 100644 --- a/src/app/core/data/signposting-data.service.spec.ts +++ b/src/app/core/data/signposting-data.service.spec.ts @@ -1,14 +1,16 @@ import { fakeAsync, TestBed, tick } from '@angular/core/testing'; + +import { of } from 'rxjs'; + import { SignpostingDataService } from './signposting-data.service'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { of } from 'rxjs'; import { SignpostingLink } from './signposting-links.model'; +import { APP_CONFIG } from '../../../config/app-config.interface'; describe('SignpostingDataService', () => { let service: SignpostingDataService; let restServiceSpy: jasmine.SpyObj; - let halServiceSpy: jasmine.SpyObj; + const mocklink = { href: 'http://test.org', rel: 'test', @@ -30,21 +32,25 @@ describe('SignpostingDataService', () => { statusCode: 500 }; + const environmentRest = { + rest: { + baseUrl: 'http://localhost:8080' + } + }; + beforeEach(() => { const restSpy = jasmine.createSpyObj('DspaceRestService', ['get', 'getWithHeaders']); - const halSpy = jasmine.createSpyObj('HALEndpointService', ['getRootHref']); TestBed.configureTestingModule({ providers: [ SignpostingDataService, - { provide: DspaceRestService, useValue: restSpy }, - { provide: HALEndpointService, useValue: halSpy } + { provide: APP_CONFIG, useValue: environmentRest }, + { provide: DspaceRestService, useValue: restSpy } ] }); service = TestBed.inject(SignpostingDataService); restServiceSpy = TestBed.inject(DspaceRestService) as jasmine.SpyObj; - halServiceSpy = TestBed.inject(HALEndpointService) as jasmine.SpyObj; }); it('should be created', () => { @@ -55,8 +61,6 @@ describe('SignpostingDataService', () => { const uuid = '123'; const baseUrl = 'http://localhost:8080'; - halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); - restServiceSpy.get.and.returnValue(of(mockResponse)); let result: SignpostingLink[]; @@ -70,7 +74,6 @@ describe('SignpostingDataService', () => { tick(); expect(result).toEqual(expectedResult); - expect(halServiceSpy.getRootHref).toHaveBeenCalled(); expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); })); @@ -78,8 +81,6 @@ describe('SignpostingDataService', () => { const uuid = '123'; const baseUrl = 'http://localhost:8080'; - halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); - restServiceSpy.get.and.returnValue(of(mockErrResponse)); let result: any; @@ -91,7 +92,6 @@ describe('SignpostingDataService', () => { tick(); expect(result).toEqual([]); - expect(halServiceSpy.getRootHref).toHaveBeenCalled(); expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); })); }); diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index 34d3ffdab9..638b04dfdd 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -1,12 +1,12 @@ -import { Injectable } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { catchError, map } from 'rxjs/operators'; import { Observable, of as observableOf } from 'rxjs'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { SignpostingLink } from './signposting-links.model'; +import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; /** * Service responsible for handling requests related to the Signposting endpoint @@ -16,7 +16,7 @@ import { SignpostingLink } from './signposting-links.model'; }) export class SignpostingDataService { - constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { + constructor(@Inject(APP_CONFIG) protected appConfig: AppConfig, private restService: DspaceRestService) { } /** @@ -25,8 +25,7 @@ export class SignpostingDataService { * @param uuid */ getLinks(uuid: string): Observable { - const regex = /\/api$/gm; - const baseUrl = this.halService.getRootHref().replace(regex, ''); + const baseUrl = `${this.appConfig.rest.baseUrl}`; return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( catchError((err) => { From de0d7bf33ae0fb59c971f88da037f7e8d948af04 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 4 May 2023 15:01:23 -0500 Subject: [PATCH 26/30] Replace lorem ipsum with text donated by DSpaceDirect --- .../end-user-agreement-content.component.html | 132 +++++++++++++----- .../privacy-content.component.html | 128 ++++++++++++----- src/assets/i18n/en.json5 | 6 +- 3 files changed, 190 insertions(+), 76 deletions(-) diff --git a/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html b/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html index 1ee8712444..3ae0d0efbe 100644 --- a/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html +++ b/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html @@ -1,37 +1,95 @@ -

{{ 'info.end-user-agreement.head' | translate }}

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Nunc sed velit dignissim sodales ut eu. In ante metus dictum at tempor. Diam phasellus vestibulum lorem sed risus. Sed cras ornare arcu dui vivamus. Sit amet consectetur adipiscing elit pellentesque. Id velit ut tortor pretium viverra suspendisse potenti. Sed euismod nisi porta lorem mollis aliquam ut. Justo laoreet sit amet cursus sit amet dictum sit. Ullamcorper morbi tincidunt ornare massa eget egestas. -

-

- In iaculis nunc sed augue lacus. Curabitur vitae nunc sed velit dignissim sodales ut eu sem. Tellus id interdum velit laoreet id donec ultrices tincidunt arcu. Quis vel eros donec ac odio tempor. Viverra accumsan in nisl nisi scelerisque eu ultrices vitae. Varius quam quisque id diam vel quam. Nisl tincidunt eget nullam non nisi est sit. Nunc aliquet bibendum enim facilisis. Aenean sed adipiscing diam donec adipiscing. Convallis tellus id interdum velit laoreet. Massa placerat duis ultricies lacus sed turpis tincidunt. Sed cras ornare arcu dui vivamus arcu. Egestas integer eget aliquet nibh praesent tristique. Sit amet purus gravida quis blandit turpis cursus in hac. Porta non pulvinar neque laoreet suspendisse. Quis risus sed vulputate odio ut. Dignissim enim sit amet venenatis urna cursus. -

-

- Interdum velit laoreet id donec ultrices tincidunt arcu non sodales. Massa sapien faucibus et molestie. Dictumst vestibulum rhoncus est pellentesque elit ullamcorper. Metus dictum at tempor commodo ullamcorper. Tincidunt lobortis feugiat vivamus at augue eget. Non diam phasellus vestibulum lorem sed risus ultricies. Neque aliquam vestibulum morbi blandit cursus risus at ultrices mi. Euismod lacinia at quis risus sed. Lorem mollis aliquam ut porttitor leo a diam. Ipsum dolor sit amet consectetur. Ante in nibh mauris cursus mattis molestie a iaculis at. Commodo ullamcorper a lacus vestibulum. Pellentesque elit eget gravida cum sociis. Sit amet commodo nulla facilisi nullam vehicula. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean. -

-

- Ac turpis egestas maecenas pharetra convallis. Lacus sed viverra tellus in. Nullam eget felis eget nunc lobortis mattis aliquam faucibus purus. Id aliquet risus feugiat in ante metus dictum at. Quis enim lobortis scelerisque fermentum dui faucibus. Eu volutpat odio facilisis mauris sit amet massa vitae tortor. Tellus elementum sagittis vitae et leo. Cras sed felis eget velit aliquet sagittis. Proin fermentum leo vel orci porta non pulvinar neque laoreet. Dui sapien eget mi proin sed libero enim. Ultrices mi tempus imperdiet nulla malesuada. Mattis molestie a iaculis at. Turpis massa sed elementum tempus egestas. -

-

- Dui faucibus in ornare quam viverra orci sagittis eu volutpat. Cras adipiscing enim eu turpis. Ac felis donec et odio pellentesque. Iaculis nunc sed augue lacus viverra vitae congue eu consequat. Posuere lorem ipsum dolor sit amet consectetur adipiscing elit duis. Elit eget gravida cum sociis natoque penatibus. Id faucibus nisl tincidunt eget nullam non. Sagittis aliquam malesuada bibendum arcu vitae. Fermentum leo vel orci porta. Aliquam ultrices sagittis orci a scelerisque purus semper. Diam maecenas sed enim ut sem viverra aliquet eget sit. Et ultrices neque ornare aenean euismod. Eu mi bibendum neque egestas congue quisque egestas diam. Eget lorem dolor sed viverra. Ut lectus arcu bibendum at. Rutrum tellus pellentesque eu tincidunt tortor. Vitae congue eu consequat ac. Elit ullamcorper dignissim cras tincidunt. Sit amet volutpat consequat mauris nunc congue nisi. -

-

- Cursus in hac habitasse platea dictumst quisque sagittis purus. Placerat duis ultricies lacus sed turpis tincidunt. In egestas erat imperdiet sed euismod nisi porta lorem mollis. Non nisi est sit amet facilisis magna. In massa tempor nec feugiat nisl pretium fusce. Pulvinar neque laoreet suspendisse interdum consectetur. Ullamcorper morbi tincidunt ornare massa eget egestas purus viverra accumsan. Fringilla urna porttitor rhoncus dolor purus non enim. Mauris nunc congue nisi vitae suscipit. Commodo elit at imperdiet dui accumsan sit amet nulla. Tempor id eu nisl nunc mi ipsum faucibus. Porta non pulvinar neque laoreet suspendisse. Nec nam aliquam sem et tortor consequat. -

-

- Eget nunc lobortis mattis aliquam faucibus purus. Odio tempor orci dapibus ultrices. Sed nisi lacus sed viverra tellus. Elit ullamcorper dignissim cras tincidunt. Porttitor rhoncus dolor purus non enim praesent elementum facilisis. Viverra orci sagittis eu volutpat odio. Pharetra massa massa ultricies mi quis. Lectus vestibulum mattis ullamcorper velit sed ullamcorper. Pulvinar neque laoreet suspendisse interdum consectetur. Vitae auctor eu augue ut. Arcu dictum varius duis at consectetur lorem donec. Massa sed elementum tempus egestas sed sed. Risus viverra adipiscing at in tellus integer. Vulputate enim nulla aliquet porttitor lacus luctus accumsan. Pharetra massa massa ultricies mi. Elementum eu facilisis sed odio morbi quis commodo odio. Tincidunt lobortis feugiat vivamus at. Felis donec et odio pellentesque diam volutpat commodo sed. Risus feugiat in ante metus dictum at tempor commodo ullamcorper. Fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate. -

-

- Lectus proin nibh nisl condimentum id venenatis a condimentum. Id consectetur purus ut faucibus pulvinar elementum integer enim. Non pulvinar neque laoreet suspendisse interdum consectetur. Est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus. Suscipit tellus mauris a diam maecenas sed enim ut sem. Dolor purus non enim praesent elementum facilisis. Non enim praesent elementum facilisis leo vel. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Nulla porttitor massa id neque aliquam vestibulum. Erat velit scelerisque in dictum non consectetur. Amet cursus sit amet dictum. Nec tincidunt praesent semper feugiat nibh. Rutrum quisque non tellus orci ac auctor. Sagittis aliquam malesuada bibendum arcu vitae elementum. Massa tincidunt dui ut ornare lectus sit amet est. Aliquet porttitor lacus luctus accumsan tortor posuere ac. Quis hendrerit dolor magna eget est lorem ipsum dolor sit. Lectus mauris ultrices eros in. -

-

- Massa massa ultricies mi quis hendrerit dolor magna. Est ullamcorper eget nulla facilisi etiam dignissim diam. Vulputate sapien nec sagittis aliquam malesuada. Nisi porta lorem mollis aliquam ut porttitor leo a diam. Tempus quam pellentesque nec nam. Faucibus vitae aliquet nec ullamcorper sit amet risus nullam eget. Gravida arcu ac tortor dignissim convallis aenean et tortor. A scelerisque purus semper eget duis at tellus at. Viverra ipsum nunc aliquet bibendum enim. Semper feugiat nibh sed pulvinar proin gravida hendrerit. Et ultrices neque ornare aenean euismod. Consequat semper viverra nam libero justo laoreet. Nunc mattis enim ut tellus elementum sagittis. Consectetur lorem donec massa sapien faucibus et. Vel risus commodo viverra maecenas accumsan lacus vel facilisis. Diam sollicitudin tempor id eu nisl nunc. Dolor magna eget est lorem ipsum dolor. Adipiscing elit pellentesque habitant morbi tristique. -

-

- Nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur. Egestas fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate sapien. Porttitor leo a diam sollicitudin tempor. Pellentesque dignissim enim sit amet venenatis urna cursus eget nunc. Posuere sollicitudin aliquam ultrices sagittis orci a scelerisque. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus. Leo urna molestie at elementum. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Libero id faucibus nisl tincidunt eget nullam. Tellus elementum sagittis vitae et leo duis ut diam. Sodales ut etiam sit amet nisl purus in mollis. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus. Lacus laoreet non curabitur gravida arcu ac tortor dignissim convallis. Aliquam malesuada bibendum arcu vitae elementum. Leo vel orci porta non pulvinar neque laoreet. Ipsum suspendisse ultrices gravida dictum fusce. -

-

- Egestas erat imperdiet sed euismod nisi porta lorem. Venenatis a condimentum vitae sapien pellentesque habitant. Sit amet luctus venenatis lectus magna fringilla urna porttitor. Orci sagittis eu volutpat odio facilisis mauris sit amet massa. Ut enim blandit volutpat maecenas volutpat blandit aliquam. Libero volutpat sed cras ornare. Molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit sed. Diam quis enim lobortis scelerisque fermentum dui. Pellentesque habitant morbi tristique senectus et netus. Auctor urna nunc id cursus metus aliquam eleifend. Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique. Sed risus ultricies tristique nulla aliquet enim tortor. Tincidunt arcu non sodales neque sodales ut. Sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt. -

-

- Pulvinar etiam non quam lacus suspendisse faucibus. Eu mi bibendum neque egestas congue. Egestas purus viverra accumsan in nisl nisi scelerisque eu. Vulputate enim nulla aliquet porttitor lacus luctus accumsan. Eu non diam phasellus vestibulum. Semper feugiat nibh sed pulvinar. Ante in nibh mauris cursus mattis molestie a. Maecenas accumsan lacus vel facilisis volutpat. Non quam lacus suspendisse faucibus. Quis commodo odio aenean sed adipiscing. Vel elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi. Sed cras ornare arcu dui vivamus arcu felis. Tortor vitae purus faucibus ornare suspendisse sed. Morbi tincidunt ornare massa eget egestas purus viverra. Nibh cras pulvinar mattis nunc. Luctus venenatis lectus magna fringilla urna porttitor. Enim blandit volutpat maecenas volutpat blandit aliquam etiam erat. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Felis eget nunc lobortis mattis aliquam faucibus purus in. Vivamus arcu felis bibendum ut. -

+

{{ 'info.end-user-agreement.head' | translate }}

+

Last updated May 4, 2023

+ +

Agreement to terms

+

These Terms of Use constitute a legally binding agreement made between you, whether personally or on behalf of an entity ("you") and {{ 'repository.title' | translate }} ("Company", "we", "us", or "our"), concerning your access to and use of this website as well as any other media form, media channel, mobile website or mobile application related, linked, or otherwise connected thereto (collectively, the "Site"). You agree that by accessing the Site, you have read, understood, and agreed to be bound by all of these Terms of Use and any future amendments thereof.

+

Supplemental terms and conditions or documents that may be posted on the Site from time to time are hereby expressly incorporated herein by reference. We reserve the right, in our sole discretion, to make changes or modifications to these Terms of Use at any time and for any reason. We will alert you about any changes by updating the "Last updated" date of these Terms of Use, and you waive any right to receive specific notice of each such change. Please ensure that you check the applicable Terms every time you use our Site so that you understand which Terms apply. You will be subject to, and will be deemed to have been made aware of and to have accepted, the changes in any revised Terms of Use by your continued use of the Site after the date such revised Terms of Use are posted.

+

The information provided on the Site is not intended for distribution to or use by any person or entity in any jurisdiction or country where such distribution or use would be contrary to law or regulation or which would subject us to any registration requirement within such jurisdiction or country. Accordingly, those persons who choose to access the Site from other locations do so on their own initiative and are solely responsible for compliance with local laws, if and to the extent local laws are applicable.

+ +

Intellectual property rights

+

Unless otherwise indicated, the Site is our proprietary property and all source code, databases, functionality, software, website designs, audio, video, text, photographs, and graphics on the Site (collectively, the "Content") and the trademarks, service marks, and logos contained therein (the "Marks") are owned or controlled by us or licensed to us, and are protected by copyright and trademark laws and various other intellectual property rights and unfair competition laws of {{ 'info.end-user-agreement.hosting-country' | translate }}, international copyright laws, and international conventions. The Content and the Marks are provided on the Site "AS IS" for your information and personal use only. Except as expressly provided in these Terms of Use, no part of the Site and no Content[a] or Marks may be copied, reproduced, aggregated, republished, uploaded, posted, publicly displayed, encoded, translated, transmitted, distributed, sold, licensed, or otherwise exploited for any commercial purpose whatsoever, without our express prior written permission.

+

Provided that you are eligible to use the Site, you are granted a limited license to access and use the Site and to download or print a copy of any portion of the Content to which you have properly gained access solely for your personal, non-commercial use. We reserve all rights not expressly granted to you in and to the Site, the Content and the Marks.

+ +

User representations

+

By using the Site, you represent and warrant that: (1) all registration information you submit will be true, accurate, current, and complete; (2) you will maintain the accuracy of such information and promptly update such registration information as necessary; (3) you have the legal capacity and you agree to comply with these Terms of Use; (4) you will not use the Site for any illegal or unauthorized purpose; and (5) your use of the Site will not violate any applicable law or regulation.

+

If you provide any information that is untrue, inaccurate, not current, or incomplete, we have the right to suspend or terminate your account and refuse any and all current or future use of the Site (or any portion thereof).

+ +

User registration

+

You may be required to register with the Site. You agree to keep your password confidential and will be responsible for all use of your account and password. We reserve the right to remove, reclaim, or change a username you select if we determine, in our sole discretion, that such username is inappropriate, obscene, or otherwise objectionable.

+ +

Prohibited activities

+

You may not access or use the Site for any purpose other than that for which we make the Site available. The Site may not be used in connection with any commercial endeavors except those that are specifically endorsed or approved by us.

+

As a user of the Site, you agree not to:

+
    +
  • Systematically retrieve data or other content from the Site to create or compile, directly or indirectly, a collection, compilation, database, or directory without written permission from us.
  • +
  • Trick, defraud, or mislead us and other users, especially in any attempt to learn sensitive account information such as user passwords.
  • +
  • Circumvent, disable, or otherwise interfere with security-related features of the Site, including features that prevent or restrict the use or copying of any Content or enforce limitations on the use of the Site and/or the Content contained therein.
  • +
  • Disparage, tarnish, or otherwise harm, in our opinion, us and/or the Site.
  • +
  • Use any information obtained from the Site in order to harass, abuse, or harm another person.
  • +
  • Make improper use of our support services or submit false reports of abuse or misconduct.
  • +
  • Use the Site in a manner inconsistent with any applicable laws or regulations.
  • +
  • Engage in unauthorized framing of or linking to the Site.
  • +
  • Upload or transmit (or attempt to upload or to transmit) viruses, Trojan horses, or other material, including excessive use of capital letters and spamming (continuous posting of repetitive text), that interferes with any party's uninterrupted use and enjoyment of the Site or modifies, impairs, disrupts, alters, or interferes with the use, features, functions, operation, or maintenance of the Site.
  • +
  • Delete the copyright or other proprietary rights notice from any Content.
  • +
  • Attempt to impersonate another user or person or use the username and password of another user.
  • +
  • Upload or transmit (or attempt to upload or to transmit) any material that acts as a passive or active information collection or transmission mechanism, including without limitation, clear graphics interchange formats ("gifs"), 1x1 pixels, web bugs, cookies, or other similar devices (sometimes referred to as "spyware" or "passive collection mechanisms" or "pcms").
  • +
  • Interfere with, disrupt, or create an undue burden on the Site or the networks or services connected to the Site.
  • +
  • Harass, annoy, intimidate, or threaten any of our employees or agents engaged in providing any portion of the Site to you.
  • +
  • Attempt to bypass any measures of the Site designed to prevent or restrict access to the Site, or any portion of the Site.
  • +
  • Make any unauthorized use of the Site, including collecting usernames and/or email addresses of users by electronic or other means for the purpose of sending unsolicited email or other forms of electronic communication, or creating user accounts by automated means or under false pretenses.
  • +
  • Use the Site as part of any effort to compete with us or otherwise use the Site and/or the Content for any revenue-generating endeavor or commercial enterprise.
  • +
+ +

User generated contributions

+

The Site may provide you with the opportunity to create, submit, post, display, transmit, perform, publish, distribute, or broadcast content and materials to us or on the Site, including but not limited to text, writings, video, audio, photographs, graphics, comments, suggestions, or personal information or other material (collectively, "Contributions"). Contributions may be viewable by other users of the Site and through third-party websites. As such, any Contributions you transmit may be treated as non-confidential and non-proprietary. When you create or make available any Contributions, you thereby represent and warrant that:

+
    +
  • The creation, distribution, transmission, public display, or performance, and the accessing, downloading, or copying of your Contributions do not and will not infringe the proprietary rights, including but not limited to the copyright, patent, trademark, trade secret, or moral rights of any third party.
  • +
  • You are the creator and owner of or have the necessary licenses, rights, consents, releases, and permissions to use and to authorize us, the Site, and other users of the Site to use your Contributions in any manner contemplated by the Site and these Terms of Use.
  • +
  • You have the written consent, release, and/or permission of each and every identifiable individual person in your Contributions to use the name or likeness of each and every such identifiable individual person to enable inclusion and use of your Contributions in any manner contemplated by the Site and these Terms of Use.
  • +
  • Your Contributions are not false, inaccurate, or misleading.
  • +
  • Your Contributions are not unsolicited or unauthorized advertising, promotional materials, pyramid schemes, chain letters, spam, mass mailings, or other forms of solicitation.
  • +
  • Your Contributions are not obscene, lewd, lascivious, filthy, violent, harassing, libelous, slanderous, or otherwise objectionable (as determined by us).
  • +
  • Your Contributions do not ridicule, mock, disparage, intimidate, or abuse anyone.
  • +
  • Your Contributions are not used to harass or threaten (in the legal sense of those terms) any other person, do not create and are not used to promote violence against a specific person or class of people.
  • +
  • Your Contributions do not violate any applicable law, regulation, or rule.
  • +
  • Your Contributions do not violate the privacy or publicity rights of any third party.
  • +
  • Your Contributions do not violate any applicable law concerning child pornography, or otherwise intended to protect the health or well-being of minors.
  • +
  • Your Contributions do not include any offensive comments that are connected to race, national origin, gender, sexual preference, colour, religion, creed or physical handicap.
  • +
  • Your Contributions do not otherwise violate, or link to material that violates, any provision of these Terms of Use, or any applicable law or regulation.
  • +
+

Any use of the Site in violation of the foregoing violates these Terms of Use and may result in, among other things, termination or suspension of your rights to use the Site.

+ +

Site management

+

We reserve the right, but not the obligation, to: (1) monitor the Site for violations of these Terms of Use; (2) take appropriate legal action against anyone who, in our sole discretion, violates the law or these Terms of Use, including without limitation, reporting such user to law enforcement authorities; (3) in our sole discretion and without limitation, refuse, restrict access to, limit the availability of, or disable (to the extent technologically feasible) any of your Contributions or any portion thereof; (4) in our sole discretion and without limitation, notice, or liability, to remove from the Site or otherwise disable all files and content that are excessive in size or are in any way burdensome to our systems; and (5) otherwise manage the Site in a manner designed to protect our rights and property and to facilitate the proper functioning of the Site.

+ +

Privacy policy

+

We care about data privacy and security. Please review our Privacy Policy. By using the Site, you agree to be bound by our Privacy Policy, which is incorporated into these Terms of Use.

+

Please be advised the Site is hosted in {{ 'info.end-user-agreement.hosting-country' | translate }}. If you access the Site from any other region of the world with laws or other requirements governing personal data collection, use, or disclosure that differ from applicable laws in {{ 'info.end-user-agreement.hosting-country' | translate }}, then through your continued use of the Site, you are transferring your data to {{ 'info.end-user-agreement.hosting-country' | translate }}, and you agree to have your data transferred to and processed in {{ 'info.end-user-agreement.hosting-country' | translate }}.

+ +

Term and termination

+

These Terms of Use shall remain in full force and effect while you use the Site. WITHOUT LIMITING ANY OTHER PROVISION OF THESE TERMS OF USE, WE RESERVE THE RIGHT TO, IN OUR SOLE DISCRETION AND WITHOUT NOTICE OR LIABILITY, DENY ACCESS TO AND USE OF THE SITE (INCLUDING BLOCKING CERTAIN IP ADDRESSES), TO ANY PERSON FOR ANY REASON OR FOR NO REASON, INCLUDING WITHOUT LIMITATION FOR BREACH OF ANY REPRESENTATION, WARRANTY, OR COVENANT CONTAINED IN THESE TERMS OF USE OR OF ANY APPLICABLE LAW OR REGULATION. WE MAY TERMINATE YOUR USE OR PARTICIPATION IN THE SITE OR DELETE YOUR ACCOUNT AND ANY CONTENT OR INFORMATION THAT YOU POSTED AT ANY TIME, WITHOUT WARNING, IN OUR SOLE DISCRETION.

+

If we terminate or suspend your account for any reason, you are prohibited from registering and creating a new account under your name, a fake or borrowed name, or the name of any third party, even if you may be acting on behalf of the third party. In addition to terminating or suspending your account, we reserve the right to take appropriate legal action, including without limitation pursuing civil, criminal, and injunctive redress.

+ +

Modifications and interruptions

+

We reserve the right to change, modify, or remove the contents of the Site at any time or for any reason at our sole discretion without notice. However, we have no obligation to update any information on our Site. We also reserve the right to modify or discontinue all or part of the Site without notice at any time. We will not be liable to you or any third party for any modification, change, suspension, or discontinuance of the Site.

+

We cannot guarantee the Site will be available at all times. We may experience hardware, software, or other problems or need to perform maintenance related to the Site, resulting in interruptions, delays, or errors. We reserve the right to change, revise, update, suspend, discontinue, or otherwise modify the Site at any time or for any reason without notice to you. You agree that we have no liability whatsoever for any loss, damage, or inconvenience caused by your inability to access or use the Site during any downtime or discontinuance of the Site. Nothing in these Terms of Use will be construed to obligate us to maintain and support the Site or to supply any corrections, updates, or releases in connection therewith.

+ +

Corrections

+

There may be information on the Site that contains typographical errors, inaccuracies, or omissions. We reserve the right to correct any errors, inaccuracies, or omissions and to change or update the information on the Site at any time, without prior notice.

+ +

Disclaimer

+

THE SITE IS PROVIDED ON AN AS-IS AND AS-AVAILABLE BASIS. YOU AGREE THAT YOUR USE OF THE SITE AND OUR SERVICES WILL BE AT YOUR SOLE RISK. TO THE FULLEST EXTENT PERMITTED BY LAW, WE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, IN CONNECTION WITH THE SITE AND YOUR USE THEREOF, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. WE MAKE NO WARRANTIES OR REPRESENTATIONS ABOUT THE ACCURACY OR COMPLETENESS OF THE SITE'S CONTENT OR THE CONTENT OF ANY WEBSITES LINKED TO THE SITE AND WE WILL ASSUME NO LIABILITY OR RESPONSIBILITY FOR ANY (1) ERRORS, MISTAKES, OR INACCURACIES OF CONTENT AND MATERIALS, (2) PERSONAL INJURY OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM YOUR ACCESS TO AND USE OF THE SITE, (3) ANY UNAUTHORIZED ACCESS TO OR USE OF OUR SECURE SERVERS AND/OR ANY AND ALL PERSONAL INFORMATION AND/OR FINANCIAL INFORMATION STORED THEREIN, (4) ANY INTERRUPTION OR CESSATION OF TRANSMISSION TO OR FROM THE SITE, (5) ANY BUGS, VIRUSES, TROJAN HORSES, OR THE LIKE WHICH MAY BE TRANSMITTED TO OR THROUGH THE SITE BY ANY THIRD PARTY, AND/OR (6) ANY ERRORS OR OMISSIONS IN ANY CONTENT AND MATERIALS OR FOR ANY LOSS OR DAMAGE OF ANY KIND INCURRED AS A RESULT OF THE USE OF ANY CONTENT POSTED, TRANSMITTED, OR OTHERWISE MADE AVAILABLE VIA THE SITE. WE DO NOT WARRANT, ENDORSE, GUARANTEE, OR ASSUME RESPONSIBILITY FOR ANY PRODUCT OR SERVICE ADVERTISED OR OFFERED BY A THIRD PARTY THROUGH THE SITE, ANY HYPERLINKED WEBSITE, OR ANY WEBSITE OR MOBILE APPLICATION FEATURED IN ANY BANNER OR OTHER ADVERTISING, AND WE WILL NOT BE A PARTY TO OR IN ANY WAY BE RESPONSIBLE FOR MONITORING ANY TRANSACTION BETWEEN YOU AND ANY THIRD-PARTY PROVIDERS OF PRODUCTS OR SERVICES. AS WITH THE PURCHASE OF A PRODUCT OR SERVICE THROUGH ANY MEDIUM OR IN ANY ENVIRONMENT, YOU SHOULD USE YOUR BEST JUDGMENT AND EXERCISE CAUTION WHERE APPROPRIATE.

+ +

Limitations of liability

+

IN NO EVENT WILL WE OR OUR DIRECTORS, EMPLOYEES, OR AGENTS BE LIABLE TO YOU OR ANY THIRD PARTY FOR ANY DIRECT, INDIRECT, CONSEQUENTIAL, EXEMPLARY, INCIDENTAL, SPECIAL, OR PUNITIVE DAMAGES, INCLUDING LOST PROFIT, LOST REVENUE, LOSS OF DATA, OR OTHER DAMAGES ARISING FROM YOUR USE OF THE SITE, EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

+ +

Indemnification

+

You agree to defend, indemnify, and hold us harmless, including our subsidiaries, affiliates, and all of our respective officers, agents, partners, and employees, from and against any loss, damage, liability, claim, or demand, including reasonable attorneys' fees and expenses, made by any third party due to or arising out of: (1) your Contributions; (2) use of the Site; (3) breach of these Terms of Use; (4) any breach of your representations and warranties set forth in these Terms of Use; (5) your violation of the rights of a third party, including but not limited to intellectual property rights; or (6) any overt harmful act toward any other user of the Site with whom you connected via the Site. Notwithstanding the foregoing, we reserve the right, at your expense, to assume the exclusive defense and control of any matter for which you are required to indemnify us, and you agree to cooperate, at your expense, with our defense of such claims. We will use reasonable efforts to notify you of any such claim, action, or proceeding which is subject to this indemnification upon becoming aware of it.

+ +

User Data

+

We will maintain certain data that you transmit to the Site for the purpose of managing the performance of the Site, as well as data relating to your use of the Site. Although we perform regular routine backups of data, you are solely responsible for all data that you transmit or that relates to any activity you have undertaken using the Site. You agree that we shall have no liability to you for any loss or corruption of any such data, and you hereby waive any right of action against us arising from any such loss or corruption of such data.

+ +

Miscellaneous

+

These Terms of Use and any policies or operating rules posted by us on the Site or in respect to the Site constitute the entire agreement and understanding between you and us. Our failure to exercise or enforce any right or provision of these Terms of Use shall not operate as a waiver of such right or provision. These Terms of Use operate to the fullest extent permissible by law. We may assign any or all of our rights and obligations to others at any time. We shall not be responsible or liable for any loss, damage, delay, or failure to act caused by any cause beyond our reasonable control. If any provision or part of a provision of these Terms of Use is determined to be unlawful, void, or unenforceable, that provision or part of the provision is deemed severable from these Terms of Use and does not affect the validity and enforceability of any remaining provisions. There is no joint venture, partnership, employment or agency relationship created between you and us as a result of these Terms of Use or use of the Site. You agree that these Terms of Use will not be construed against us by virtue of having drafted them. You hereby waive any and all defenses you may have based on the electronic form of these Terms of Use and the lack of signing by the parties hereto to execute these Terms of Use.

+ +

[a] The DSpace software used to run this site is open source. Options for reuse and reproduction of the DSpace software is governed by its open source license: https://github.com/DSpace/DSpace/blob/main/LICENSE

\ No newline at end of file diff --git a/src/app/info/privacy/privacy-content/privacy-content.component.html b/src/app/info/privacy/privacy-content/privacy-content.component.html index a5bbb3fe10..33504b1522 100644 --- a/src/app/info/privacy/privacy-content/privacy-content.component.html +++ b/src/app/info/privacy/privacy-content/privacy-content.component.html @@ -1,37 +1,91 @@ -

{{ 'info.privacy.head' | translate }}

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Nunc sed velit dignissim sodales ut eu. In ante metus dictum at tempor. Diam phasellus vestibulum lorem sed risus. Sed cras ornare arcu dui vivamus. Sit amet consectetur adipiscing elit pellentesque. Id velit ut tortor pretium viverra suspendisse potenti. Sed euismod nisi porta lorem mollis aliquam ut. Justo laoreet sit amet cursus sit amet dictum sit. Ullamcorper morbi tincidunt ornare massa eget egestas. -

-

- In iaculis nunc sed augue lacus. Curabitur vitae nunc sed velit dignissim sodales ut eu sem. Tellus id interdum velit laoreet id donec ultrices tincidunt arcu. Quis vel eros donec ac odio tempor. Viverra accumsan in nisl nisi scelerisque eu ultrices vitae. Varius quam quisque id diam vel quam. Nisl tincidunt eget nullam non nisi est sit. Nunc aliquet bibendum enim facilisis. Aenean sed adipiscing diam donec adipiscing. Convallis tellus id interdum velit laoreet. Massa placerat duis ultricies lacus sed turpis tincidunt. Sed cras ornare arcu dui vivamus arcu. Egestas integer eget aliquet nibh praesent tristique. Sit amet purus gravida quis blandit turpis cursus in hac. Porta non pulvinar neque laoreet suspendisse. Quis risus sed vulputate odio ut. Dignissim enim sit amet venenatis urna cursus. -

-

- Interdum velit laoreet id donec ultrices tincidunt arcu non sodales. Massa sapien faucibus et molestie. Dictumst vestibulum rhoncus est pellentesque elit ullamcorper. Metus dictum at tempor commodo ullamcorper. Tincidunt lobortis feugiat vivamus at augue eget. Non diam phasellus vestibulum lorem sed risus ultricies. Neque aliquam vestibulum morbi blandit cursus risus at ultrices mi. Euismod lacinia at quis risus sed. Lorem mollis aliquam ut porttitor leo a diam. Ipsum dolor sit amet consectetur. Ante in nibh mauris cursus mattis molestie a iaculis at. Commodo ullamcorper a lacus vestibulum. Pellentesque elit eget gravida cum sociis. Sit amet commodo nulla facilisi nullam vehicula. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean. -

-

- Ac turpis egestas maecenas pharetra convallis. Lacus sed viverra tellus in. Nullam eget felis eget nunc lobortis mattis aliquam faucibus purus. Id aliquet risus feugiat in ante metus dictum at. Quis enim lobortis scelerisque fermentum dui faucibus. Eu volutpat odio facilisis mauris sit amet massa vitae tortor. Tellus elementum sagittis vitae et leo. Cras sed felis eget velit aliquet sagittis. Proin fermentum leo vel orci porta non pulvinar neque laoreet. Dui sapien eget mi proin sed libero enim. Ultrices mi tempus imperdiet nulla malesuada. Mattis molestie a iaculis at. Turpis massa sed elementum tempus egestas. -

-

- Dui faucibus in ornare quam viverra orci sagittis eu volutpat. Cras adipiscing enim eu turpis. Ac felis donec et odio pellentesque. Iaculis nunc sed augue lacus viverra vitae congue eu consequat. Posuere lorem ipsum dolor sit amet consectetur adipiscing elit duis. Elit eget gravida cum sociis natoque penatibus. Id faucibus nisl tincidunt eget nullam non. Sagittis aliquam malesuada bibendum arcu vitae. Fermentum leo vel orci porta. Aliquam ultrices sagittis orci a scelerisque purus semper. Diam maecenas sed enim ut sem viverra aliquet eget sit. Et ultrices neque ornare aenean euismod. Eu mi bibendum neque egestas congue quisque egestas diam. Eget lorem dolor sed viverra. Ut lectus arcu bibendum at. Rutrum tellus pellentesque eu tincidunt tortor. Vitae congue eu consequat ac. Elit ullamcorper dignissim cras tincidunt. Sit amet volutpat consequat mauris nunc congue nisi. -

-

- Cursus in hac habitasse platea dictumst quisque sagittis purus. Placerat duis ultricies lacus sed turpis tincidunt. In egestas erat imperdiet sed euismod nisi porta lorem mollis. Non nisi est sit amet facilisis magna. In massa tempor nec feugiat nisl pretium fusce. Pulvinar neque laoreet suspendisse interdum consectetur. Ullamcorper morbi tincidunt ornare massa eget egestas purus viverra accumsan. Fringilla urna porttitor rhoncus dolor purus non enim. Mauris nunc congue nisi vitae suscipit. Commodo elit at imperdiet dui accumsan sit amet nulla. Tempor id eu nisl nunc mi ipsum faucibus. Porta non pulvinar neque laoreet suspendisse. Nec nam aliquam sem et tortor consequat. -

-

- Eget nunc lobortis mattis aliquam faucibus purus. Odio tempor orci dapibus ultrices. Sed nisi lacus sed viverra tellus. Elit ullamcorper dignissim cras tincidunt. Porttitor rhoncus dolor purus non enim praesent elementum facilisis. Viverra orci sagittis eu volutpat odio. Pharetra massa massa ultricies mi quis. Lectus vestibulum mattis ullamcorper velit sed ullamcorper. Pulvinar neque laoreet suspendisse interdum consectetur. Vitae auctor eu augue ut. Arcu dictum varius duis at consectetur lorem donec. Massa sed elementum tempus egestas sed sed. Risus viverra adipiscing at in tellus integer. Vulputate enim nulla aliquet porttitor lacus luctus accumsan. Pharetra massa massa ultricies mi. Elementum eu facilisis sed odio morbi quis commodo odio. Tincidunt lobortis feugiat vivamus at. Felis donec et odio pellentesque diam volutpat commodo sed. Risus feugiat in ante metus dictum at tempor commodo ullamcorper. Fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate. -

-

- Lectus proin nibh nisl condimentum id venenatis a condimentum. Id consectetur purus ut faucibus pulvinar elementum integer enim. Non pulvinar neque laoreet suspendisse interdum consectetur. Est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus. Suscipit tellus mauris a diam maecenas sed enim ut sem. Dolor purus non enim praesent elementum facilisis. Non enim praesent elementum facilisis leo vel. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Nulla porttitor massa id neque aliquam vestibulum. Erat velit scelerisque in dictum non consectetur. Amet cursus sit amet dictum. Nec tincidunt praesent semper feugiat nibh. Rutrum quisque non tellus orci ac auctor. Sagittis aliquam malesuada bibendum arcu vitae elementum. Massa tincidunt dui ut ornare lectus sit amet est. Aliquet porttitor lacus luctus accumsan tortor posuere ac. Quis hendrerit dolor magna eget est lorem ipsum dolor sit. Lectus mauris ultrices eros in. -

-

- Massa massa ultricies mi quis hendrerit dolor magna. Est ullamcorper eget nulla facilisi etiam dignissim diam. Vulputate sapien nec sagittis aliquam malesuada. Nisi porta lorem mollis aliquam ut porttitor leo a diam. Tempus quam pellentesque nec nam. Faucibus vitae aliquet nec ullamcorper sit amet risus nullam eget. Gravida arcu ac tortor dignissim convallis aenean et tortor. A scelerisque purus semper eget duis at tellus at. Viverra ipsum nunc aliquet bibendum enim. Semper feugiat nibh sed pulvinar proin gravida hendrerit. Et ultrices neque ornare aenean euismod. Consequat semper viverra nam libero justo laoreet. Nunc mattis enim ut tellus elementum sagittis. Consectetur lorem donec massa sapien faucibus et. Vel risus commodo viverra maecenas accumsan lacus vel facilisis. Diam sollicitudin tempor id eu nisl nunc. Dolor magna eget est lorem ipsum dolor. Adipiscing elit pellentesque habitant morbi tristique. -

-

- Nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur. Egestas fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate sapien. Porttitor leo a diam sollicitudin tempor. Pellentesque dignissim enim sit amet venenatis urna cursus eget nunc. Posuere sollicitudin aliquam ultrices sagittis orci a scelerisque. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus. Leo urna molestie at elementum. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Libero id faucibus nisl tincidunt eget nullam. Tellus elementum sagittis vitae et leo duis ut diam. Sodales ut etiam sit amet nisl purus in mollis. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus. Lacus laoreet non curabitur gravida arcu ac tortor dignissim convallis. Aliquam malesuada bibendum arcu vitae elementum. Leo vel orci porta non pulvinar neque laoreet. Ipsum suspendisse ultrices gravida dictum fusce. -

-

- Egestas erat imperdiet sed euismod nisi porta lorem. Venenatis a condimentum vitae sapien pellentesque habitant. Sit amet luctus venenatis lectus magna fringilla urna porttitor. Orci sagittis eu volutpat odio facilisis mauris sit amet massa. Ut enim blandit volutpat maecenas volutpat blandit aliquam. Libero volutpat sed cras ornare. Molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit sed. Diam quis enim lobortis scelerisque fermentum dui. Pellentesque habitant morbi tristique senectus et netus. Auctor urna nunc id cursus metus aliquam eleifend. Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique. Sed risus ultricies tristique nulla aliquet enim tortor. Tincidunt arcu non sodales neque sodales ut. Sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt. -

-

- Pulvinar etiam non quam lacus suspendisse faucibus. Eu mi bibendum neque egestas congue. Egestas purus viverra accumsan in nisl nisi scelerisque eu. Vulputate enim nulla aliquet porttitor lacus luctus accumsan. Eu non diam phasellus vestibulum. Semper feugiat nibh sed pulvinar. Ante in nibh mauris cursus mattis molestie a. Maecenas accumsan lacus vel facilisis volutpat. Non quam lacus suspendisse faucibus. Quis commodo odio aenean sed adipiscing. Vel elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi. Sed cras ornare arcu dui vivamus arcu felis. Tortor vitae purus faucibus ornare suspendisse sed. Morbi tincidunt ornare massa eget egestas purus viverra. Nibh cras pulvinar mattis nunc. Luctus venenatis lectus magna fringilla urna porttitor. Enim blandit volutpat maecenas volutpat blandit aliquam etiam erat. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Felis eget nunc lobortis mattis aliquam faucibus purus in. Vivamus arcu felis bibendum ut. -

+

{{ 'info.privacy.head' | translate }}

+

Last updated May 4, 2023

+ +

Introduction

+

{{ 'repository.title' | translate }} ("Company" or "We") respects your privacy and is committed to protecting it through our compliance with this policy.

+

This policy describes the types of information we may collect from you or that you may provide when you visit this website (our "Website") and our practices for collecting, using, maintaining, protecting, and disclosing that information.

+

This policy applies to information we collect:

+
    +
  • On this Website.
  • +
  • In email, text, and other electronic messages between you and this Website.
  • +
+

It does not apply to information collected by:

+
    +
  • us offline or through any other means, including on any other website operated by Company or any third party; or
  • +
  • any third party, including through any application or content (including advertising) that may link to or be accessible from or on the Website.
  • +
+

Please read this policy carefully to understand our policies and practices regarding your information and how we will treat it. If you do not agree with our policies and practices, your choice is not to use our Website. By accessing or using this Website, you agree to this privacy policy. This policy may change from time to time. Your continued use of this Website after we make changes is deemed to be acceptance of those changes, so please check the policy periodically for updates.

+ +

Children under the age of 13

+

Our Website is not intended for children under 13 years of age. No one under age 13 may provide any personal information to or on the Website. We do not knowingly collect personal information from children under 13. If you are under 13, do not use or provide any information on this Website or provide any information about yourself to us, including your name, address, telephone number, email address, or any screen name or username you may use. If we learn we have collected or received personal information from a child under 13 without verification of parental consent, we will delete that information.

+ +

Information we collect about you and how we collect it

+

We collect several types of information from and about users of our Website, including information:

+
    +
  • by which you may be personally identified, such as name, e-mail address, telephone number, or any other identifier by which you may be contacted online or offline ("personal information"); and/or
  • +
  • about your internet connection, the equipment you use to access our Website and usage details.
  • +
+

We collect this information:

+
    +
  • directly from you when you provide it to us.
  • +
  • automatically as you navigate through the site. Information collected automatically may include usage details, IP addresses, and information collected through cookies, web beacons, and other tracking technologies; and/or
  • +
  • from third parties, for example, our business partners.
  • +
+ +

Information you provide to us

+

The information we collect on or through our Website may include:

+
    +
  • Information that you provide by filling in forms on our Website. We may also ask you for information when you report a problem with our Website.
  • +
  • Records and copies of your correspondence (including email addresses), if you contact us.
  • +
  • Your responses to surveys that we might ask you to complete for research purpose.
  • +
+ +

Information we collect through automatic data collection technologies

+

As you navigate through and interact with our Website, we may use automatic data collection technologies to collect certain information about your equipment, browsing actions, and patterns, including:

+
    +
  • Details of your visits to our Website, including traffic data, location data, logs, and other communication data and the resources that you access and use on the Website.
  • +
  • Information about your computer and internet connection, including your IP address, operating system, and browser type.
  • +
+

The information we collect automatically is statistical data and does not include personal information, but we may maintain it or associate it with personal information we collect in other ways or receive from third parties. It helps us to improve our Website and to deliver a better and more personalized service, including by enabling us to:

+
    +
  • Estimate our audience size and usage patterns.
  • +
  • Store information about your preferences, allowing us to customize our Website according to your individual interests.
  • +
  • Speed up your searches.
  • +
  • Recognize you when you return to our Website.
  • +
+

The technologies we use for this automatic data collection may include:

+
    +
  • Cookies (or browser cookies). A cookie is a small file placed on the hard drive of your computer. You may refuse to accept browser cookies by activating the appropriate setting on your browser. However, if you select this setting you may be unable to access certain parts of our Website. Unless you have adjusted your browser setting so that it will refuse cookies, our system will issue cookies when you direct your browser to our Website.
  • +
+ +

How we use your information

+

We use information that we collect about you or that you provide to us, including any personal information:

+
    +
  • To present our Website and its contents to you.
  • +
  • To provide you with information, products, or services that you request from us.
  • +
  • To fulfill any other purpose for which you provide it.
  • +
  • To carry out our obligations and enforce our rights arising from any contracts entered into between you and us, including for billing and collection.
  • +
  • To notify you about changes to our Website or any products or services we offer or provide through it.
  • +
  • In any other way we may describe when you provide the information.
  • +
  • For any other purpose with your consent.
  • +
+ +

Disclosure of your information

+

We may disclose aggregated information about our users, and information that does not identify any individual, without restriction.

+

We may disclose personal information that we collect or you provide as described in this privacy policy:

+
    +
  • To contractors, service providers, and other third parties we use to support our business and who are bound by contractual obligations to keep personal information confidential and use it only for the purposes for which we disclose it to them.
  • +
  • To a buyer or other successor in the event of a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Company's assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which personal information held by Company about our Website users is among the assets transferred.
  • +
  • To fulfill the purpose for which you provide it.
  • +
  • For any other purpose disclosed by us when you provide the information.
  • +
  • With your consent.
  • +
+

We may also disclose your personal information:

+
    +
  • To comply with any court order, law, or legal process, including to respond to any government or regulatory request.
  • +
  • To enforce or apply our End User Agreement and other agreements, including for billing and collection purposes.
  • +
  • If we believe disclosure is necessary or appropriate to protect the rights, property, or safety of the Company, our customers, or others.
  • +
+ +

Changes to our privacy policy

+

It is our policy to post any changes we make to our privacy policy on this page. The date the privacy policy was last revised is identified at the top of the page. You are responsible for periodically visiting our Website and this privacy policy to check for any changes.

\ No newline at end of file diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 2df03302d7..b8dbd20459 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1916,6 +1916,8 @@ "info.end-user-agreement.title": "End User Agreement", + "info.end-user-agreement.hosting-country": "the United States", + "info.privacy.breadcrumbs": "Privacy Statement", "info.privacy.head": "Privacy Statement", @@ -3649,9 +3651,9 @@ "repository.image.logo": "Repository logo", - "repository.title.prefix": "DSpace Angular :: ", + "repository.title": "DSpace Repository", - "repository.title.prefixDSpace": "DSpace Angular ::", + "repository.title.prefix": "DSpace Repository :: ", "resource-policies.add.button": "Add", From c3854355fd4150c93349d998e0fad3edbbc047d1 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 4 May 2023 16:02:04 -0500 Subject: [PATCH 27/30] Fix e2e test to check for new title prefix --- cypress/e2e/homepage.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/homepage.cy.ts b/cypress/e2e/homepage.cy.ts index 8fdf61dbf7..a387c31a2a 100644 --- a/cypress/e2e/homepage.cy.ts +++ b/cypress/e2e/homepage.cy.ts @@ -6,8 +6,8 @@ describe('Homepage', () => { cy.visit('/'); }); - it('should display translated title "DSpace Angular :: Home"', () => { - cy.title().should('eq', 'DSpace Angular :: Home'); + it('should display translated title "DSpace Repository :: Home"', () => { + cy.title().should('eq', 'DSpace Repository :: Home'); }); it('should contain a news section', () => { From 685fbf630a7e6c5dbeadc99df52ae9c7fac63e9b Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 12 Jun 2023 17:04:02 -0500 Subject: [PATCH 28/30] Address feedback: Make links open in new window/tab. --- .../end-user-agreement-content.component.html | 4 ++-- .../privacy/privacy-content/privacy-content.component.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html b/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html index 3ae0d0efbe..d5e6de85d4 100644 --- a/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html +++ b/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html @@ -63,7 +63,7 @@

We reserve the right, but not the obligation, to: (1) monitor the Site for violations of these Terms of Use; (2) take appropriate legal action against anyone who, in our sole discretion, violates the law or these Terms of Use, including without limitation, reporting such user to law enforcement authorities; (3) in our sole discretion and without limitation, refuse, restrict access to, limit the availability of, or disable (to the extent technologically feasible) any of your Contributions or any portion thereof; (4) in our sole discretion and without limitation, notice, or liability, to remove from the Site or otherwise disable all files and content that are excessive in size or are in any way burdensome to our systems; and (5) otherwise manage the Site in a manner designed to protect our rights and property and to facilitate the proper functioning of the Site.

Privacy policy

-

We care about data privacy and security. Please review our Privacy Policy. By using the Site, you agree to be bound by our Privacy Policy, which is incorporated into these Terms of Use.

+

We care about data privacy and security. Please review our Privacy Policy. By using the Site, you agree to be bound by our Privacy Policy, which is incorporated into these Terms of Use.

Please be advised the Site is hosted in {{ 'info.end-user-agreement.hosting-country' | translate }}. If you access the Site from any other region of the world with laws or other requirements governing personal data collection, use, or disclosure that differ from applicable laws in {{ 'info.end-user-agreement.hosting-country' | translate }}, then through your continued use of the Site, you are transferring your data to {{ 'info.end-user-agreement.hosting-country' | translate }}, and you agree to have your data transferred to and processed in {{ 'info.end-user-agreement.hosting-country' | translate }}.

Term and termination

@@ -92,4 +92,4 @@

Miscellaneous

These Terms of Use and any policies or operating rules posted by us on the Site or in respect to the Site constitute the entire agreement and understanding between you and us. Our failure to exercise or enforce any right or provision of these Terms of Use shall not operate as a waiver of such right or provision. These Terms of Use operate to the fullest extent permissible by law. We may assign any or all of our rights and obligations to others at any time. We shall not be responsible or liable for any loss, damage, delay, or failure to act caused by any cause beyond our reasonable control. If any provision or part of a provision of these Terms of Use is determined to be unlawful, void, or unenforceable, that provision or part of the provision is deemed severable from these Terms of Use and does not affect the validity and enforceability of any remaining provisions. There is no joint venture, partnership, employment or agency relationship created between you and us as a result of these Terms of Use or use of the Site. You agree that these Terms of Use will not be construed against us by virtue of having drafted them. You hereby waive any and all defenses you may have based on the electronic form of these Terms of Use and the lack of signing by the parties hereto to execute these Terms of Use.

-

[a] The DSpace software used to run this site is open source. Options for reuse and reproduction of the DSpace software is governed by its open source license: https://github.com/DSpace/DSpace/blob/main/LICENSE

\ No newline at end of file +

[a] The DSpace software used to run this site is open source. Options for reuse and reproduction of the DSpace software is governed by its open source license: https://github.com/DSpace/DSpace/blob/main/LICENSE

\ No newline at end of file diff --git a/src/app/info/privacy/privacy-content/privacy-content.component.html b/src/app/info/privacy/privacy-content/privacy-content.component.html index 33504b1522..f29a786e8b 100644 --- a/src/app/info/privacy/privacy-content/privacy-content.component.html +++ b/src/app/info/privacy/privacy-content/privacy-content.component.html @@ -83,7 +83,7 @@

We may also disclose your personal information:

  • To comply with any court order, law, or legal process, including to respond to any government or regulatory request.
  • -
  • To enforce or apply our End User Agreement and other agreements, including for billing and collection purposes.
  • +
  • To enforce or apply our End User Agreement and other agreements, including for billing and collection purposes.
  • If we believe disclosure is necessary or appropriate to protect the rights, property, or safety of the Company, our customers, or others.
From adebc30d3bf61a1266124c61003b29fa62a37790 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Wed, 14 Jun 2023 10:49:49 +0300 Subject: [PATCH 29/30] src/assets/i18n/en.json5: lint Normalize whitespace in i18n/en.json5. This file should be clean, as it is the "upstream" of all other language assets and produces a lot of whitespace changes every time strings are merged. --- src/assets/i18n/en.json5 | 362 +++++++-------------------------------- 1 file changed, 65 insertions(+), 297 deletions(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index b8dbd20459..480bf8834e 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -6,8 +6,6 @@ "401.unauthorized": "unauthorized", - - "403.help": "You don't have permission to access this page. You can use the button below to get back to the home page.", "403.link.home-page": "Take me to the home page", @@ -20,7 +18,6 @@ "500.link.home-page": "Take me to the home page", - "404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ", "404.link.home-page": "Take me to the home page", @@ -130,6 +127,7 @@ "admin.registries.bitstream-formats.table.mimetype": "MIME Type", "admin.registries.bitstream-formats.table.name": "Name", + "admin.registries.bitstream-formats.table.id": "ID", "admin.registries.bitstream-formats.table.return": "Back", @@ -144,8 +142,6 @@ "admin.registries.bitstream-formats.title": "Bitstream Format Registry", - - "admin.registries.metadata.breadcrumbs": "Metadata registry", "admin.registries.metadata.description": "The metadata registry maintains a list of all metadata fields available in the repository. These fields may be divided amongst multiple schemas. However, DSpace requires the qualified Dublin Core schema.", @@ -172,8 +168,6 @@ "admin.registries.metadata.title": "Metadata Registry", - - "admin.registries.schema.breadcrumbs": "Metadata schema", "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".", @@ -185,6 +179,7 @@ "admin.registries.schema.fields.table.delete": "Delete selected", "admin.registries.schema.fields.table.field": "Field", + "admin.registries.schema.fields.table.id": "ID", "admin.registries.schema.fields.table.scopenote": "Scope Note", @@ -225,8 +220,6 @@ "admin.registries.schema.title": "Metadata Schema Registry", - - "admin.access-control.epeople.actions.delete": "Delete EPerson", "admin.access-control.epeople.actions.impersonate": "Impersonate EPerson", @@ -321,8 +314,6 @@ "admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"", - - "admin.access-control.groups.title": "Groups", "admin.access-control.groups.breadcrumbs": "Groups", @@ -369,8 +360,6 @@ "admin.access-control.groups.notification.deleted.failure.content": "Cause: \"{{cause}}\"", - - "admin.access-control.groups.form.alert.permanent": "This group is permanent, so it can't be edited or deleted. You can still add and remove group members using this page.", "admin.access-control.groups.form.alert.workflowGroup": "This group can’t be modified or deleted because it corresponds to a role in the submission and workflow process in the \"{{name}}\" {{comcol}}. You can delete it from the \"assign roles\" tab on the edit {{comcol}} page. You can still add and remove group members using this page.", @@ -531,9 +520,6 @@ "administrativeView.search.results.head": "Administrative Search", - - - "admin.workflow.breadcrumbs": "Administer Workflow", "admin.workflow.title": "Administer Workflow", @@ -550,8 +536,6 @@ "admin.workflow.item.supervision": "Supervision", - - "admin.metadata-import.breadcrumbs": "Import Metadata", "admin.batch-import.breadcrumbs": "Import Batch", @@ -612,12 +596,10 @@ "advanced-workflow-action.rating.description-requiredDescription": "Please select a rating below and also add a review", - "advanced-workflow-action.select-reviewer.description-single": "Please select a single reviewer below before submitting", "advanced-workflow-action.select-reviewer.description-multiple": "Please select one or more reviewers below before submitting", - "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.head": "EPeople", "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.head": "Add EPeople", @@ -674,12 +656,9 @@ "auth.messages.token-refresh-failed": "Refreshing your session token failed. Please log in again.", + "bitstream.download.page": "Now downloading {{bitstream}}...", - - "bitstream.download.page": "Now downloading {{bitstream}}..." , - - "bitstream.download.page.back": "Back" , - + "bitstream.download.page.back": "Back", "bitstream.edit.authorizations.link": "Edit bitstream's Policies", @@ -731,7 +710,6 @@ "bitstream.edit.form.iiifHeight.hint": "The canvas height should usually match the image height.", - "bitstream.edit.notifications.saved.content": "Your changes to this bitstream were saved.", "bitstream.edit.notifications.saved.title": "Bitstream saved", @@ -747,6 +725,7 @@ "bitstream-request-a-copy.intro": "Enter the following information to request a copy for the following item: ", "bitstream-request-a-copy.intro.bitstream.one": "Requesting the following file: ", + "bitstream-request-a-copy.intro.bitstream.all": "Requesting all files. ", "bitstream-request-a-copy.name.label": "Name *", @@ -775,8 +754,6 @@ "bitstream-request-a-copy.submit.error": "Something went wrong with submitting the item request.", - - "browse.back.all-results": "All browse results", "browse.comcol.by.author": "By Author", @@ -871,20 +848,16 @@ "browse.title.page": "Browsing {{ collection }} by {{ field }} {{ value }}", - "search.browse.item-back": "Back to Results", - "chips.remove": "Remove chip", - "claimed-approved-search-result-list-element.title": "Approved", "claimed-declined-search-result-list-element.title": "Rejected, sent back to submitter", "claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow", - "collection.create.head": "Create a Collection", "collection.create.notifications.success": "Successfully created the Collection", @@ -907,16 +880,12 @@ "collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"", - - "collection.edit.delete": "Delete this collection", "collection.edit.head": "Edit Collection", "collection.edit.breadcrumbs": "Edit Collection", - - "collection.edit.tabs.mapper.head": "Item Mapper", "collection.edit.tabs.item-mapper.title": "Collection Edit - Item Mapper", @@ -957,7 +926,6 @@ "collection.edit.item-mapper.tabs.map": "Map new items", - "collection.edit.logo.delete.title": "Delete logo", "collection.edit.logo.delete-undo.title": "Undo delete", @@ -976,14 +944,10 @@ "collection.edit.logo.upload": "Drop a Collection Logo to upload", - - "collection.edit.notifications.success": "Successfully edited the Collection", "collection.edit.return": "Back", - - "collection.edit.tabs.curate.head": "Curate", "collection.edit.tabs.curate.title": "Collection Edit - Curate", @@ -1042,8 +1006,6 @@ "collection.edit.tabs.source.title": "Collection Edit - Content Source", - - "collection.edit.template.add-button": "Add", "collection.edit.template.breadcrumbs": "Item template", @@ -1068,8 +1030,6 @@ "collection.edit.template.title": "Edit Template Item", - - "collection.form.abstract": "Short Description", "collection.form.description": "Introductory text (HTML)", @@ -1088,12 +1048,8 @@ "collection.form.entityType": "Entity Type", - - "collection.listelement.badge": "Collection", - - "collection.page.browse.recent.head": "Recent Submissions", "collection.page.browse.recent.empty": "No items to show", @@ -1106,46 +1062,62 @@ "collection.page.news": "News", - - "collection.select.confirm": "Confirm selected", "collection.select.empty": "No collections to show", "collection.select.table.title": "Title", - "collection.source.controls.head": "Harvest Controls", - "collection.source.controls.test.submit.error": "Something went wrong with initiating the testing of the settings", - "collection.source.controls.test.failed": "The script to test the settings has failed", - "collection.source.controls.test.completed": "The script to test the settings has successfully finished", - "collection.source.controls.test.submit": "Test configuration", - "collection.source.controls.test.running": "Testing configuration...", - "collection.source.controls.import.submit.success": "The import has been successfully initiated", - "collection.source.controls.import.submit.error": "Something went wrong with initiating the import", - "collection.source.controls.import.submit": "Import now", - "collection.source.controls.import.running": "Importing...", - "collection.source.controls.import.failed": "An error occurred during the import", - "collection.source.controls.import.completed": "The import completed", - "collection.source.controls.reset.submit.success": "The reset and reimport has been successfully initiated", - "collection.source.controls.reset.submit.error": "Something went wrong with initiating the reset and reimport", - "collection.source.controls.reset.failed": "An error occurred during the reset and reimport", - "collection.source.controls.reset.completed": "The reset and reimport completed", - "collection.source.controls.reset.submit": "Reset and reimport", - "collection.source.controls.reset.running": "Resetting and reimporting...", - "collection.source.controls.harvest.status": "Harvest status:", - "collection.source.controls.harvest.start": "Harvest start time:", - "collection.source.controls.harvest.last": "Last time harvested:", - "collection.source.controls.harvest.message": "Harvest info:", - "collection.source.controls.harvest.no-information": "N/A", + "collection.source.controls.test.submit.error": "Something went wrong with initiating the testing of the settings", + + "collection.source.controls.test.failed": "The script to test the settings has failed", + + "collection.source.controls.test.completed": "The script to test the settings has successfully finished", + + "collection.source.controls.test.submit": "Test configuration", + + "collection.source.controls.test.running": "Testing configuration...", + + "collection.source.controls.import.submit.success": "The import has been successfully initiated", + + "collection.source.controls.import.submit.error": "Something went wrong with initiating the import", + + "collection.source.controls.import.submit": "Import now", + + "collection.source.controls.import.running": "Importing...", + + "collection.source.controls.import.failed": "An error occurred during the import", + + "collection.source.controls.import.completed": "The import completed", + + "collection.source.controls.reset.submit.success": "The reset and reimport has been successfully initiated", + + "collection.source.controls.reset.submit.error": "Something went wrong with initiating the reset and reimport", + + "collection.source.controls.reset.failed": "An error occurred during the reset and reimport", + + "collection.source.controls.reset.completed": "The reset and reimport completed", + + "collection.source.controls.reset.submit": "Reset and reimport", + + "collection.source.controls.reset.running": "Resetting and reimporting...", + + "collection.source.controls.harvest.status": "Harvest status:", + + "collection.source.controls.harvest.start": "Harvest start time:", + + "collection.source.controls.harvest.last": "Last time harvested:", + + "collection.source.controls.harvest.message": "Harvest info:", + + "collection.source.controls.harvest.no-information": "N/A", "collection.source.update.notifications.error.content": "The provided settings have been tested and didn't work.", "collection.source.update.notifications.error.title": "Server Error", - - "communityList.breadcrumbs": "Community List", "communityList.tabTitle": "Community List", @@ -1154,8 +1126,6 @@ "communityList.showMore": "Show More", - - "community.create.head": "Create a Community", "community.create.notifications.success": "Successfully created the Community", @@ -1184,7 +1154,6 @@ "community.edit.breadcrumbs": "Edit Community", - "community.edit.logo.delete.title": "Delete logo", "community.edit.logo.delete-undo.title": "Undo delete", @@ -1203,8 +1172,6 @@ "community.edit.logo.upload": "Drop a Community Logo to upload", - - "community.edit.notifications.success": "Successfully edited the Community", "community.edit.notifications.unauthorized": "You do not have privileges to make this change", @@ -1213,8 +1180,6 @@ "community.edit.return": "Back", - - "community.edit.tabs.curate.head": "Curate", "community.edit.tabs.curate.title": "Community Edit - Curate", @@ -1231,12 +1196,8 @@ "community.edit.tabs.authorizations.title": "Community Edit - Authorizations", - - "community.listelement.badge": "Community", - - "comcol-role.edit.no-group": "None", "comcol-role.edit.create": "Create", @@ -1249,57 +1210,46 @@ "comcol-role.edit.delete.error.title": "Failed to delete the '{{ role }}' role's group", - "comcol-role.edit.community-admin.name": "Administrators", "comcol-role.edit.collection-admin.name": "Administrators", - "comcol-role.edit.community-admin.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).", "comcol-role.edit.collection-admin.description": "Collection administrators decide who can submit items to the collection, edit item metadata (after submission), and add (map) existing items from other collections to this collection (subject to authorization for that collection).", - "comcol-role.edit.submitters.name": "Submitters", "comcol-role.edit.submitters.description": "The E-People and Groups that have permission to submit new items to this collection.", - "comcol-role.edit.item_read.name": "Default item read access", "comcol-role.edit.item_read.description": "E-People and Groups that can read new items submitted to this collection. Changes to this role are not retroactive. Existing items in the system will still be viewable by those who had read access at the time of their addition.", "comcol-role.edit.item_read.anonymous-group": "Default read for incoming items is currently set to Anonymous.", - "comcol-role.edit.bitstream_read.name": "Default bitstream read access", "comcol-role.edit.bitstream_read.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).", "comcol-role.edit.bitstream_read.anonymous-group": "Default read for incoming bitstreams is currently set to Anonymous.", - "comcol-role.edit.editor.name": "Editors", "comcol-role.edit.editor.description": "Editors are able to edit the metadata of incoming submissions, and then accept or reject them.", - "comcol-role.edit.finaleditor.name": "Final editors", "comcol-role.edit.finaleditor.description": "Final editors are able to edit the metadata of incoming submissions, but will not be able to reject them.", - "comcol-role.edit.reviewer.name": "Reviewers", "comcol-role.edit.reviewer.description": "Reviewers are able to accept or reject incoming submissions. However, they are not able to edit the submission's metadata.", - "comcol-role.edit.scorereviewers.name": "Score Reviewers", "comcol-role.edit.scorereviewers.description": "Reviewers are able to give a score to incoming submissions, this will define whether the submission will be rejected or not.", - - "community.form.abstract": "Short Description", "community.form.description": "Introductory text (HTML)", @@ -1326,8 +1276,6 @@ "community.sub-community-list.head": "Communities of this Community", - - "cookies.consent.accept-all": "Accept all", "cookies.consent.accept-selected": "Accept selected", @@ -1380,30 +1328,22 @@ "cookies.consent.app.description.authentication": "Required for signing you in", - "cookies.consent.app.title.preferences": "Preferences", "cookies.consent.app.description.preferences": "Required for saving your preferences", - - "cookies.consent.app.title.acknowledgement": "Acknowledgement", "cookies.consent.app.description.acknowledgement": "Required for saving your acknowledgements and consents", - - "cookies.consent.app.title.google-analytics": "Google Analytics", "cookies.consent.app.description.google-analytics": "Allows us to track statistical data", - - "cookies.consent.app.title.google-recaptcha": "Google reCaptcha", "cookies.consent.app.description.google-recaptcha": "We use google reCAPTCHA service during registration and password recovery", - "cookies.consent.purpose.functional": "Functional", "cookies.consent.purpose.statistical": "Statistical", @@ -1428,8 +1368,6 @@ "curation-task.task.register-doi.label": "Register DOI", - - "curation.form.task-select.label": "Task:", "curation.form.submit": "Start", @@ -1448,8 +1386,6 @@ "curation.form.handle.hint": "Hint: Enter [your-handle-prefix]/0 to run a task across entire site (not all tasks may support this capability)", - - "deny-request-copy.email.message": "Dear {{ recipientName }},\nIn response to your request I regret to inform you that it's not possible to send you a copy of the file(s) you have requested, concerning the document: \"{{ itemUrl }}\" ({{ itemName }}), of which I am an author.\n\nBest regards,\n{{ authorName }} <{{ authorEmail }}>", "deny-request-copy.email.subject": "Request copy of document", @@ -1462,14 +1398,10 @@ "deny-request-copy.success": "Successfully denied item request", - - "dso.name.untitled": "Untitled", "dso.name.unnamed": "Unnamed", - - "dso-selector.create.collection.head": "New collection", "dso-selector.create.collection.sub-level": "Create a new collection in", @@ -1656,11 +1588,8 @@ "feed.description": "Syndication feed", - "file-section.error.header": "Error obtaining files for this item", - - "footer.copyright": "copyright © 2002-{{ year }}", "footer.link.dspace": "DSpace software", @@ -1675,8 +1604,6 @@ "footer.link.feedback": "Send Feedback", - - "forgot-email.form.header": "Forgot Password", "forgot-email.form.info": "Enter the email address associated with the account.", @@ -1699,8 +1626,6 @@ "forgot-email.form.error.content": "An error occured when attempting to reset the password for the account associated with the following email address: {{ email }}", - - "forgot-password.title": "Forgot Password", "forgot-password.form.head": "Forgot Password", @@ -1729,7 +1654,6 @@ "forgot-password.form.submit": "Submit password", - "form.add": "Add more", "form.add-help": "Click here to add the current entry and to add another one", @@ -1798,8 +1722,6 @@ "form.repeatable.sort.tip": "Drop the item in the new position", - - "grant-deny-request-copy.deny": "Don't send copy", "grant-deny-request-copy.email.back": "Back", @@ -1830,8 +1752,6 @@ "grant-deny-request-copy.processed": "This request has already been processed. You can use the button below to get back to the home page.", - - "grant-request-copy.email.message": "Dear {{ recipientName }},\nIn response to your request I have the pleasure to send you in attachment a copy of the file(s) concerning the document: \"{{ itemUrl }}\" ({{ itemName }}), of which I am an author.\n\nBest regards,\n{{ authorName }} <{{ authorEmail }}>", "grant-request-copy.email.subject": "Request copy of document", @@ -1844,7 +1764,6 @@ "grant-request-copy.success": "Successfully granted item request", - "health.breadcrumbs": "Health", "health-page.heading": "Health", @@ -1885,7 +1804,6 @@ "health-page.section.no-issues": "No issues detected", - "home.description": "", "home.breadcrumbs": "Home", @@ -1898,8 +1816,6 @@ "home.top-level-communities.help": "Select a community to browse its collections.", - - "info.end-user-agreement.accept": "I have read and I agree to the End User Agreement", "info.end-user-agreement.accept.error": "An error occurred accepting the End User Agreement", @@ -1950,26 +1866,18 @@ "info.feedback.page_help": "Tha page related to your feedback", - - "item.alerts.private": "This item is non-discoverable", "item.alerts.withdrawn": "This item has been withdrawn", - - "item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.", "item.edit.authorizations.title": "Edit item's Policies", - - "item.badge.private": "Non-discoverable", "item.badge.withdrawn": "Withdrawn", - - "item.bitstreams.upload.bundle": "Bundle", "item.bitstreams.upload.bundle.placeholder": "Select a bundle or input new bundle name", @@ -1992,8 +1900,6 @@ "item.bitstreams.upload.title": "Upload bitstream", - - "item.edit.bitstreams.bundle.edit.buttons.upload": "Upload", "item.edit.bitstreams.bundle.displaying": "Currently displaying {{ amount }} bitstreams of {{ total }}.", @@ -2054,8 +1960,6 @@ "item.edit.bitstreams.upload-button": "Upload", - - "item.edit.delete.cancel": "Cancel", "item.edit.delete.confirm": "Delete", @@ -2074,7 +1978,6 @@ "item.edit.tabs.disabled.tooltip": "You're not authorized to access this tab", - "item.edit.tabs.mapper.head": "Collection Mapper", "item.edit.tabs.item-mapper.title": "Item Edit - Collection Mapper", @@ -2157,8 +2060,6 @@ "item.edit.item-mapper.tabs.map": "Map new collections", - - "item.edit.metadata.add-button": "Add", "item.edit.metadata.discard-button": "Discard", @@ -2215,16 +2116,12 @@ "item.edit.metadata.save-button": "Save", - - "item.edit.modify.overview.field": "Field", "item.edit.modify.overview.language": "Language", "item.edit.modify.overview.value": "Value", - - "item.edit.move.cancel": "Back", "item.edit.move.save-button": "Save", @@ -2251,8 +2148,6 @@ "item.edit.move.title": "Move item", - - "item.edit.private.cancel": "Cancel", "item.edit.private.confirm": "Make it non-discoverable", @@ -2265,8 +2160,6 @@ "item.edit.private.success": "The item is now non-discoverable", - - "item.edit.public.cancel": "Cancel", "item.edit.public.confirm": "Make it discoverable", @@ -2279,8 +2172,6 @@ "item.edit.public.success": "The item is now discoverable", - - "item.edit.reinstate.cancel": "Cancel", "item.edit.reinstate.confirm": "Reinstate", @@ -2293,8 +2184,6 @@ "item.edit.reinstate.success": "The item was reinstated successfully", - - "item.edit.relationships.discard-button": "Discard", "item.edit.relationships.edit.buttons.add": "Add", @@ -2325,10 +2214,8 @@ "item.edit.relationships.no-entity-type": "Add 'dspace.entity.type' metadata to enable relationships for this item", - "item.edit.return": "Back", - "item.edit.tabs.bitstreams.head": "Bitstreams", "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", @@ -2336,6 +2223,7 @@ "item.edit.tabs.curate.head": "Curate", "item.edit.tabs.curate.title": "Item Edit - Curate", + "item.edit.curate.title": "Curate Item: {{item}}", "item.edit.tabs.metadata.head": "Metadata", @@ -2404,8 +2292,6 @@ "item.edit.tabs.view.title": "Item Edit - View", - - "item.edit.withdraw.cancel": "Cancel", "item.edit.withdraw.confirm": "Withdraw", @@ -2420,7 +2306,6 @@ "item.orcid.return": "Back", - "item.listelement.badge": "Item", "item.page.description": "Description", @@ -2459,8 +2344,6 @@ "workflow-item.search.result.list.element.supervised.remove-tooltip": "Remove supervision group", - - "item.page.abstract": "Abstract", "item.page.author": "Authors", @@ -2587,8 +2470,6 @@ "item.preview.oaire.fundingStream": "Funding Stream:", - - "item.select.confirm": "Confirm selected", "item.select.empty": "No items to show", @@ -2599,7 +2480,6 @@ "item.select.table.title": "Title", - "item.version.history.empty": "There are no other versions for this item yet.", "item.version.history.head": "Version History", @@ -2640,10 +2520,8 @@ "item.version.history.table.action.hasDraft": "A new version cannot be created because there is an inprogress submission in the version history", - "item.version.notice": "This is not the latest version of this item. The latest version can be found here.", - "item.version.create.modal.header": "New version", "item.version.create.modal.text": "Create a new version for this item", @@ -2672,7 +2550,6 @@ "item.version.create.notification.inProgress": "A new version cannot be created because there is an inprogress submission in the version history", - "item.version.delete.modal.header": "Delete version", "item.version.delete.modal.text": "Do you want to delete version {{version}}?", @@ -2689,13 +2566,10 @@ "item.version.delete.notification.failure": "Version number {{version}} has not been deleted", - "item.version.edit.notification.success": "The summary of version number {{version}} has been changed", "item.version.edit.notification.failure": "The summary of version number {{version}} has not been changed", - - "itemtemplate.edit.metadata.add-button": "Add", "itemtemplate.edit.metadata.discard-button": "Discard", @@ -2750,8 +2624,6 @@ "itemtemplate.edit.metadata.save-button": "Save", - - "journal.listelement.badge": "Journal", "journal.page.description": "Description", @@ -2772,8 +2644,6 @@ "journal.search.title": "Journal Search", - - "journalissue.listelement.badge": "Journal Issue", "journalissue.page.description": "Description", @@ -2792,8 +2662,6 @@ "journalissue.page.titleprefix": "Journal Issue: ", - - "journalvolume.listelement.badge": "Journal Volume", "journalvolume.page.description": "Description", @@ -2806,7 +2674,6 @@ "journalvolume.page.volume": "Volume", - "iiifsearchable.listelement.badge": "Document Media", "iiifsearchable.page.titleprefix": "Document: ", @@ -2829,7 +2696,6 @@ "iiif.page.description": "Description: ", - "loading.bitstream": "Loading bitstream...", "loading.bitstreams": "Loading bitstreams...", @@ -2866,8 +2732,6 @@ "loading.top-level-communities": "Loading top-level communities...", - - "login.form.email": "Email address", "login.form.forgot-password": "Have you forgotten your password?", @@ -2892,24 +2756,18 @@ "login.breadcrumbs": "Login", - - "logout.form.header": "Log out from DSpace", "logout.form.submit": "Log out", "logout.title": "Logout", - - "menu.header.admin": "Management", "menu.header.image.logo": "Repository logo", "menu.header.admin.description": "Management menu", - - "menu.section.access_control": "Access Control", "menu.section.access_control_authorizations": "Authorizations", @@ -2918,12 +2776,8 @@ "menu.section.access_control_people": "People", - - "menu.section.admin_search": "Admin Search", - - "menu.section.browse_community": "This Community", "menu.section.browse_community_by_author": "By Author", @@ -2946,14 +2800,10 @@ "menu.section.browse_global_communities_and_collections": "Communities & Collections", - - "menu.section.control_panel": "Control Panel", "menu.section.curation_task": "Curation Task", - - "menu.section.edit": "Edit", "menu.section.edit_collection": "Collection", @@ -2962,8 +2812,6 @@ "menu.section.edit_item": "Item", - - "menu.section.export": "Export", "menu.section.export_collection": "Collection", @@ -2976,7 +2824,6 @@ "menu.section.export_batch": "Batch Export (ZIP)", - "menu.section.icon.access_control": "Access Control menu section", "menu.section.icon.admin_search": "Admin search menu section", @@ -3009,16 +2856,12 @@ "menu.section.icon.unpin": "Unpin sidebar", - - "menu.section.import": "Import", "menu.section.import_batch": "Batch Import (ZIP)", "menu.section.import_metadata": "Metadata", - - "menu.section.new": "New", "menu.section.new_collection": "Collection", @@ -3031,34 +2874,24 @@ "menu.section.new_process": "Process", - - "menu.section.pin": "Pin sidebar", "menu.section.unpin": "Unpin sidebar", - - "menu.section.processes": "Processes", "menu.section.health": "Health", - - "menu.section.registries": "Registries", "menu.section.registries_format": "Format", "menu.section.registries_metadata": "Metadata", - - "menu.section.statistics": "Statistics", "menu.section.statistics_task": "Statistics Task", - - "menu.section.toggle.access_control": "Toggle Access Control section", "menu.section.toggle.control_panel": "Toggle Control Panel section", @@ -3079,14 +2912,13 @@ "menu.section.toggle.statistics_task": "Toggle Statistics Task section", - "menu.section.workflow": "Administer Workflow", - "metadata-export-search.tooltip": "Export search results as CSV", - "metadata-export-search.submit.success": "The export was started successfully", - "metadata-export-search.submit.error": "Starting the export has failed", + "metadata-export-search.submit.success": "The export was started successfully", + + "metadata-export-search.submit.error": "Starting the export has failed", "mydspace.breadcrumbs": "MyDSpace", @@ -3172,8 +3004,6 @@ "mydspace.view-btn": "View", - - "nav.browse.header": "All of DSpace", "nav.community-browse.header": "By Community", @@ -3198,7 +3028,6 @@ "nav.search.button": "Submit search", - "nav.statistics.header": "Statistics", "nav.stop-impersonating": "Stop impersonating EPerson", @@ -3211,7 +3040,6 @@ "none.listelement.badge": "Item", - "orgunit.listelement.badge": "Organizational Unit", "orgunit.listelement.no-title": "Untitled", @@ -3230,8 +3058,6 @@ "orgunit.page.titleprefix": "Organizational Unit: ", - - "pagination.options.description": "Pagination options", "pagination.results-per-page": "Results Per Page", @@ -3242,8 +3068,6 @@ "pagination.sort-direction": "Sort Options", - - "person.listelement.badge": "Person", "person.listelement.no-title": "No name found", @@ -3276,8 +3100,6 @@ "person.search.title": "Person Search", - - "process.new.select-parameters": "Parameters", "process.new.cancel": "Cancel", @@ -3318,8 +3140,6 @@ "process.new.breadcrumbs": "Create a new process", - - "process.detail.arguments": "Arguments", "process.detail.arguments.empty": "This process doesn't contain any arguments", @@ -3366,8 +3186,6 @@ "process.detail.delete.error": "Something went wrong when deleting the process", - - "process.overview.table.finish": "Finish time (UTC)", "process.overview.table.id": "Process ID", @@ -3404,8 +3222,6 @@ "process.bulk.delete.success": "{{count}} process(es) have been succesfully deleted", - - "profile.breadcrumbs": "Update Profile", "profile.card.identify": "Identify", @@ -3492,8 +3308,6 @@ "project-relationships.search.results.head": "Project Search Results", - - "publication.listelement.badge": "Publication", "publication.page.description": "Description", @@ -3516,14 +3330,12 @@ "publication.search.title": "Publication Search", - "media-viewer.next": "Next", "media-viewer.previous": "Previous", "media-viewer.playlist": "Playlist", - "register-email.title": "New user registration", "register-page.create-profile.header": "Create Profile", @@ -3566,7 +3378,6 @@ "register-page.create-profile.submit.success.head": "Registration completed", - "register-page.registration.header": "New user registration", "register-page.registration.info": "Register an account to subscribe to collections for email updates, and submit new items to DSpace.", @@ -3594,8 +3405,8 @@ "register-page.registration.error.recaptcha": "Error when trying to authenticate with recaptcha", "register-page.registration.google-recaptcha.must-accept-cookies": "In order to register you must accept the Registration and Password recovery (Google reCaptcha) cookies.", - "register-page.registration.error.maildomain": "This email address is not on the list of domains who can register. Allowed domains are {{ domains }}", + "register-page.registration.error.maildomain": "This email address is not on the list of domains who can register. Allowed domains are {{ domains }}", "register-page.registration.google-recaptcha.open-cookie-settings": "Open cookie settings", @@ -3604,6 +3415,7 @@ "register-page.registration.google-recaptcha.notification.message.error": "An error occurred during reCaptcha verification", "register-page.registration.google-recaptcha.notification.message.expired": "Verification expired. Please verify again.", + "register-page.registration.info.maildomain": "Accounts can be registered for mail addresses of the domains", "relationships.add.error.relationship-type.content": "No suitable match could be found for relationship type {{ type }} between the two items", @@ -3648,14 +3460,12 @@ "relationships.isFundingAgencyOf.OrgUnit": "Funder", - "repository.image.logo": "Repository logo", "repository.title": "DSpace Repository", "repository.title.prefix": "DSpace Repository :: ", - "resource-policies.add.button": "Add", "resource-policies.add.for.": "Add a new policy", @@ -3770,8 +3580,6 @@ "resource-policies.table.headers.title.for.collection": "Policies for Collection", - - "search.description": "", "search.switch-configuration.title": "Show", @@ -3782,7 +3590,6 @@ "search.search-form.placeholder": "Search the repository ...", - "search.filters.applied.f.author": "Author", "search.filters.applied.f.dateIssued.max": "End date", @@ -3815,8 +3622,6 @@ "search.filters.applied.f.withdrawn": "Withdrawn", - - "search.filters.filter.author.head": "Author", "search.filters.filter.author.placeholder": "Author name", @@ -3963,8 +3768,6 @@ "search.filters.filter.supervisedBy.label": "Search Supervised By", - - "search.filters.entityType.JournalIssue": "Journal Issue", "search.filters.entityType.JournalVolume": "Journal Volume", @@ -3983,23 +3786,18 @@ "search.filters.withdrawn.false": "No", - "search.filters.head": "Filters", "search.filters.reset": "Reset filters", "search.filters.search.submit": "Submit", - - "search.form.search": "Search", "search.form.search_dspace": "All repository", "search.form.scope.all": "All of DSpace", - - "search.results.head": "Search Results", "search.results.no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting", @@ -4016,7 +3814,6 @@ "default-relationships.search.results.head": "Search Results", - "search.sidebar.close": "Back to results", "search.sidebar.filters.title": "Filters", @@ -4031,16 +3828,12 @@ "search.sidebar.settings.title": "Settings", - - "search.view-switch.show-detail": "Show detail", "search.view-switch.show-grid": "Show as grid", "search.view-switch.show-list": "Show as list", - - "sorting.ASC": "Ascending", "sorting.DESC": "Descending", @@ -4065,7 +3858,6 @@ "sorting.lastModified.DESC": "Last modified Descending", - "statistics.title": "Statistics", "statistics.header": "Statistics for {{ scope }}", @@ -4090,8 +3882,6 @@ "statistics.table.no-name": "(object name could not be loaded)", - - "submission.edit.breadcrumbs": "Edit Submission", "submission.edit.title": "Edit Submission", @@ -4120,7 +3910,6 @@ "submission.general.save-later": "Save for later", - "submission.import-external.page.title": "Import metadata from an external source", "submission.import-external.title": "Import metadata from an external source", @@ -4328,6 +4117,7 @@ "submission.sections.describe.relationship-lookup.search-tab.tab-title.isAuthorOfPublication": "Local Authors ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalOfPublication": "Local Journals ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Project": "Local Projects ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.Publication": "Local Publications ({{ count }})", @@ -4343,9 +4133,11 @@ "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalIssueOfPublication": "Local Journal Issues ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalIssue": "Local Journal Issues ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalVolumeOfPublication": "Local Journal Volumes ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalVolume": "Local Journal Volumes ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Journals ({{ count }})", @@ -4382,17 +4174,16 @@ "submission.sections.describe.relationship-lookup.title.isFundingAgencyOfProject": "Funder of the Project", - - - "submission.sections.describe.relationship-lookup.selection-tab.search-form.placeholder": "Search...", "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Current Selection ({{ count }})", "submission.sections.describe.relationship-lookup.title.isJournalIssueOfPublication": "Journal Issues", + "submission.sections.describe.relationship-lookup.title.JournalIssue": "Journal Issues", "submission.sections.describe.relationship-lookup.title.isJournalVolumeOfPublication": "Journal Volumes", + "submission.sections.describe.relationship-lookup.title.JournalVolume": "Journal Volumes", "submission.sections.describe.relationship-lookup.title.isJournalOfPublication": "Journals", @@ -4400,6 +4191,7 @@ "submission.sections.describe.relationship-lookup.title.isAuthorOfPublication": "Authors", "submission.sections.describe.relationship-lookup.title.isFundingAgencyOfPublication": "Funding Agency", + "submission.sections.describe.relationship-lookup.title.Project": "Projects", "submission.sections.describe.relationship-lookup.title.Publication": "Publications", @@ -4431,6 +4223,7 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalOfPublication": "Selected Journals", "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalVolumeOfPublication": "Selected Journal Volume", + "submission.sections.describe.relationship-lookup.selection-tab.title.Project": "Selected Projects", "submission.sections.describe.relationship-lookup.selection-tab.title.Publication": "Selected Publications", @@ -4446,11 +4239,13 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Selected Journals", "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalIssueOfPublication": "Selected Issue", + "submission.sections.describe.relationship-lookup.selection-tab.title.JournalVolume": "Selected Journal Volume", "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingAgencyOfPublication": "Selected Funding Agency", "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingOfPublication": "Selected Funding", + "submission.sections.describe.relationship-lookup.selection-tab.title.JournalIssue": "Selected Issue", "submission.sections.describe.relationship-lookup.selection-tab.title.isChildOrgUnitOf": "Selected Organizational Unit", @@ -4567,7 +4362,6 @@ "submission.sections.submit.progressbar.sherpaPolicies": "Publisher open access policy information", - "submission.sections.sherpa-policy.title-empty": "No publisher policy information available. If your work has an associated ISSN, please enter it above to see any related publisher open access policies.", "submission.sections.status.errors.title": "Errors", @@ -4690,7 +4484,6 @@ "submission.sections.license.notgranted": "You must accept the license", - "submission.sections.sherpa.publication.information": "Publication information", "submission.sections.sherpa.publication.information.title": "Title", @@ -4743,14 +4536,10 @@ "submission.sections.sherpa.error.message": "There was an error retrieving sherpa informations", - - "submission.submit.breadcrumbs": "New submission", "submission.submit.title": "New submission", - - "submission.workflow.generic.delete": "Delete", "submission.workflow.generic.delete-help": "Select this option to discard this item. You will then be asked to confirm it.", @@ -4763,17 +4552,14 @@ "submission.workflow.generic.view-help": "Select this option to view the item's metadata.", - "submission.workflow.generic.submit_select_reviewer": "Select Reviewer", "submission.workflow.generic.submit_select_reviewer-help": "", - "submission.workflow.generic.submit_score": "Rate", "submission.workflow.generic.submit_score-help": "", - "submission.workflow.tasks.claimed.approve": "Approve", "submission.workflow.tasks.claimed.approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".", @@ -4802,8 +4588,6 @@ "submission.workflow.tasks.claimed.return_help": "Return the task to the pool so that another user may perform the task.", - - "submission.workflow.tasks.generic.error": "Error occurred during operation...", "submission.workflow.tasks.generic.processing": "Processing...", @@ -4812,8 +4596,6 @@ "submission.workflow.tasks.generic.success": "Operation successful", - - "submission.workflow.tasks.pool.claim": "Claim", "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", @@ -4822,7 +4604,6 @@ "submission.workflow.tasks.pool.show-detail": "Show detail", - "submission.workspace.generic.view": "View", "submission.workspace.generic.view-help": "Select this option to view the item's metadata.", @@ -4899,7 +4680,6 @@ "subscriptions.table.empty.message": "You do not have any subscriptions at this time. To subscribe to email updates for a Community or Collection, use the subscription button on the object's page.", - "thumbnail.default.alt": "Thumbnail Image", "thumbnail.default.placeholder": "No Thumbnail Available", @@ -4916,12 +4696,8 @@ "thumbnail.person.placeholder": "No Profile Picture Available", - - "title": "DSpace", - - "vocabulary-treeview.header": "Hierarchical tree view", "vocabulary-treeview.load-more": "Load more", @@ -4956,8 +4732,6 @@ "virtual-metadata.delete-relationship.modal-head": "Select the items for which you want to save the virtual metadata as real metadata", - - "supervisedWorkspace.search.results.head": "Supervised Items", "workspace.search.results.head": "Your submissions", @@ -4968,8 +4742,6 @@ "supervision.search.results.head": "Workflow and Workspace tasks", - - "workflow-item.edit.breadcrumbs": "Edit workflowitem", "workflow-item.edit.title": "Edit workflowitem", @@ -4990,7 +4762,6 @@ "workflow-item.delete.button.confirm": "Delete", - "workflow-item.send-back.notification.success.title": "Sent back to submitter", "workflow-item.send-back.notification.success.content": "This workflow item was successfully sent back to the submitter", @@ -5047,7 +4818,6 @@ "workflow-item.selectrevieweraction.button.confirm": "Confirm", - "workflow-item.scorereviewaction.notification.success.title": "Rating review", "workflow-item.scorereviewaction.notification.success.content": "The rating for this item workflow item has been successfully submitted", @@ -5315,11 +5085,11 @@ "person.orcid.registry.queue": "ORCID Registry Queue", "person.orcid.registry.auth": "ORCID Authorizations", + "home.recent-submissions.head": "Recent Submissions", "listable-notification-object.default-message": "This object couldn't be retrieved", - "system-wide-alert-banner.retrieval.error": "Something went wrong retrieving the system-wide alert banner", "system-wide-alert-banner.countdown.prefix": "In", @@ -5330,8 +5100,6 @@ "system-wide-alert-banner.countdown.minutes": "{{minutes}} minute(s):", - - "menu.section.system-wide-alert": "System-wide Alert", "system-wide-alert.form.header": "System-wide Alert", @@ -5367,4 +5135,4 @@ "admin.system-wide-alert.breadcrumbs": "System-wide Alerts", "admin.system-wide-alert.title": "System-wide Alerts", -} +} \ No newline at end of file From 134eac5f39791db671bca4460f973cbbb7b622ed Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Wed, 14 Jun 2023 08:53:41 +0300 Subject: [PATCH 30/30] src/assets: update fi.json5 Add a few Finnish language UI strings. --- src/assets/i18n/fi.json5 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/assets/i18n/fi.json5 b/src/assets/i18n/fi.json5 index ca41138eda..62e7e6bffe 100644 --- a/src/assets/i18n/fi.json5 +++ b/src/assets/i18n/fi.json5 @@ -1922,6 +1922,9 @@ // "home.breadcrumbs": "Home", "home.breadcrumbs": "Etusivu", + // "home.search-form.placeholder": "Search the repository ...", + "home.search-form.placeholder": "Hae julkaisuarkistosta ...", + // "home.title": "DSpace Angular :: Home", "home.title": "DSpace Angular :: Etusivu", @@ -4020,6 +4023,8 @@ // "search.breadcrumbs": "Search", "search.breadcrumbs": "Hae", + // "search.search-form.placeholder": "Search the repository ...", + "search.search-form.placeholder": "Hae julkaisuarkistosta ...", // "search.filters.applied.f.author": "Author", "search.filters.applied.f.author": "Tekijä",