diff --git a/package.json b/package.json index 5ebde3bdf4..4ab139db36 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@angular/platform-browser-dynamic": "^17.3.11", "@angular/platform-server": "^17.3.11", "@angular/router": "^17.3.11", - "@angular/ssr": "^17.3.16", + "@angular/ssr": "^17.3.17", "@babel/runtime": "7.27.1", "@kolkov/ngx-gallery": "^2.0.1", "@ng-bootstrap/ng-bootstrap": "^11.0.0", @@ -125,14 +125,14 @@ }, "devDependencies": { "@angular-builders/custom-webpack": "~17.0.2", - "@angular-devkit/build-angular": "^17.3.16", + "@angular-devkit/build-angular": "^17.3.17", "@angular-eslint/builder": "17.5.3", "@angular-eslint/bundled-angular-compiler": "17.5.3", "@angular-eslint/eslint-plugin": "17.5.3", "@angular-eslint/eslint-plugin-template": "17.5.3", "@angular-eslint/schematics": "17.5.3", "@angular-eslint/template-parser": "17.5.3", - "@angular/cli": "^17.3.16", + "@angular/cli": "^17.3.17", "@angular/compiler-cli": "^17.3.11", "@angular/language-service": "^17.3.11", "@cypress/schematic": "^1.5.0", diff --git a/src/app/item-page/media-viewer/media-viewer.component.html b/src/app/item-page/media-viewer/media-viewer.component.html index a76ee73963..9f33277983 100644 --- a/src/app/item-page/media-viewer/media-viewer.component.html +++ b/src/app/item-page/media-viewer/media-viewer.component.html @@ -16,12 +16,7 @@ - - + diff --git a/src/app/item-page/media-viewer/media-viewer.component.spec.ts b/src/app/item-page/media-viewer/media-viewer.component.spec.ts index 7649e17b71..b479d30a92 100644 --- a/src/app/item-page/media-viewer/media-viewer.component.spec.ts +++ b/src/app/item-page/media-viewer/media-viewer.component.spec.ts @@ -1,4 +1,7 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + NO_ERRORS_SCHEMA, + PLATFORM_ID, +} from '@angular/core'; import { ComponentFixture, TestBed, @@ -14,7 +17,9 @@ import { of as observableOf } from 'rxjs'; import { AuthService } from '../../core/auth/auth.service'; import { BitstreamDataService } from '../../core/data/bitstream-data.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { Bitstream } from '../../core/shared/bitstream.model'; +import { FileService } from '../../core/shared/file.service'; import { MediaViewerItem } from '../../core/shared/media-viewer-item.model'; import { MetadataFieldWrapperComponent } from '../../shared/metadata-field-wrapper/metadata-field-wrapper.component'; import { AuthServiceMock } from '../../shared/mocks/auth.service.mock'; @@ -31,6 +36,9 @@ import { MediaViewerComponent } from './media-viewer.component'; describe('MediaViewerComponent', () => { let comp: MediaViewerComponent; let fixture: ComponentFixture; + let authService; + let authorizationService; + let fileService; const mockBitstream: Bitstream = Object.assign(new Bitstream(), { sizeBytes: 10201, @@ -55,7 +63,7 @@ describe('MediaViewerComponent', () => { 'dc.title': [ { language: null, - value: 'test_word.docx', + value: 'test_image.jpg', }, ], }, @@ -73,6 +81,15 @@ describe('MediaViewerComponent', () => { ); beforeEach(waitForAsync(() => { + authService = jasmine.createSpyObj('AuthService', { + isAuthenticated: observableOf(true), + }); + authorizationService = jasmine.createSpyObj('AuthorizationService', { + isAuthorized: observableOf(true), + }); + fileService = jasmine.createSpyObj('FileService', { + retrieveFileDownloadLink: null, + }); return TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot({ @@ -88,6 +105,10 @@ describe('MediaViewerComponent', () => { MetadataFieldWrapperComponent, ], providers: [ + { provide: AuthService, useValue: authService }, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: FileService, useValue: fileService }, + { provide: PLATFORM_ID, useValue: 'browser' }, { provide: BitstreamDataService, useValue: bitstreamDataService }, { provide: ThemeService, useValue: getMockThemeService() }, { provide: AuthService, useValue: new AuthServiceMock() }, @@ -150,9 +171,9 @@ describe('MediaViewerComponent', () => { expect(mediaItem.thumbnail).toBe(null); }); - it('should display a default, thumbnail', () => { + it('should display a default thumbnail', () => { const defaultThumbnail = fixture.debugElement.query( - By.css('ds-media-viewer-image'), + By.css('ds-thumbnail'), ); expect(defaultThumbnail.nativeElement).toBeDefined(); }); diff --git a/src/app/shared/upload/uploader/uploader-options.model.ts b/src/app/shared/upload/uploader/uploader-options.model.ts index 559fb0485b..e21628d06e 100644 --- a/src/app/shared/upload/uploader/uploader-options.model.ts +++ b/src/app/shared/upload/uploader/uploader-options.model.ts @@ -22,6 +22,11 @@ export class UploaderOptions { */ maxFileNumber: number; + /** + * Impersonating user uuid + */ + impersonatingID: string; + /** * The request method to use for the file upload request */ diff --git a/src/app/shared/upload/uploader/uploader.component.ts b/src/app/shared/upload/uploader/uploader.component.ts index fbafe811eb..804200d220 100644 --- a/src/app/shared/upload/uploader/uploader.component.ts +++ b/src/app/shared/upload/uploader/uploader.component.ts @@ -47,6 +47,11 @@ import { UploaderProperties } from './uploader-properties.model'; }) export class UploaderComponent implements OnInit, AfterViewInit { + /** + * Header key to impersonate a user + */ + private readonly ON_BEHALF_HEADER = 'X-On-Behalf-Of'; + /** * The message to show when drag files on the drop zone */ @@ -162,7 +167,13 @@ export class UploaderComponent implements OnInit, AfterViewInit { item.url = this.uploader.options.url; } // Ensure the current XSRF token is included in every upload request (token may change between items uploaded) - this.uploader.options.headers = [{ name: XSRF_REQUEST_HEADER, value: this.tokenExtractor.getToken() }]; + // Ensure the behalf header is set if impersonating + this.uploader.options.headers = [ + { name: XSRF_REQUEST_HEADER, value: this.tokenExtractor.getToken() }, + ]; + if (hasValue(this.uploadFilesOptions.impersonatingID)) { + this.uploader.options.headers.push({ name: this.ON_BEHALF_HEADER, value: this.uploadFilesOptions.impersonatingID }); + } this.onBeforeUpload(); this.isOverDocumentDropZone = observableOf(false); }; diff --git a/src/app/submission/form/submission-form.component.ts b/src/app/submission/form/submission-form.component.ts index 74c262befc..67e2e9691b 100644 --- a/src/app/submission/form/submission-form.component.ts +++ b/src/app/submission/form/submission-form.component.ts @@ -209,6 +209,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { distinctUntilChanged()) .subscribe((endpointURL) => { this.uploadFilesOptions.authToken = this.authService.buildAuthHeader(); + this.uploadFilesOptions.impersonatingID = this.authService.getImpersonateID(); this.uploadFilesOptions.url = endpointURL.concat(`/${this.submissionId}`); this.definitionId = this.submissionDefinition.name; this.submissionService.dispatchInit( diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index e151684a01..c0f3ad91c0 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,15 +1,15 @@
-
+
- - -
+ + +
{{ placeholder | translate }} diff --git a/src/app/thumbnail/thumbnail.component.spec.ts b/src/app/thumbnail/thumbnail.component.spec.ts index f81089e57c..38eceb8427 100644 --- a/src/app/thumbnail/thumbnail.component.spec.ts +++ b/src/app/thumbnail/thumbnail.component.spec.ts @@ -100,31 +100,31 @@ describe('ThumbnailComponent', () => { describe('loading', () => { it('should start out with isLoading$ true', () => { - expect(comp.isLoading).toBeTrue(); + expect(comp.isLoading$.getValue()).toBeTrue(); }); it('should set isLoading$ to false once an image is successfully loaded', () => { comp.setSrc('http://bit.stream'); fixture.debugElement.query(By.css('img.thumbnail-content')).triggerEventHandler('load', new Event('load')); - expect(comp.isLoading).toBeFalse(); + expect(comp.isLoading$.getValue()).toBeFalse(); }); it('should set isLoading$ to false once the src is set to null', () => { comp.setSrc(null); - expect(comp.isLoading).toBeFalse(); + expect(comp.isLoading$.getValue()).toBeFalse(); }); it('should show a loading animation while isLoading$ is true', () => { expect(de.query(By.css('ds-loading'))).toBeTruthy(); - comp.isLoading = false; + comp.isLoading$.next(false); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('ds-loading'))).toBeFalsy(); }); describe('with a thumbnail image', () => { beforeEach(() => { - comp.src = 'https://bit.stream'; + comp.src$.next('https://bit.stream'); fixture.detectChanges(); }); @@ -133,7 +133,7 @@ describe('ThumbnailComponent', () => { expect(img).toBeTruthy(); expect(img.classes['d-none']).toBeTrue(); - comp.isLoading = false; + comp.isLoading$.next(false); fixture.detectChanges(); img = fixture.debugElement.query(By.css('img.thumbnail-content')); expect(img).toBeTruthy(); @@ -144,14 +144,14 @@ describe('ThumbnailComponent', () => { describe('without a thumbnail image', () => { beforeEach(() => { - comp.src = null; + comp.src$.next(null); fixture.detectChanges(); }); it('should only show the HTML placeholder once done loading', () => { expect(fixture.debugElement.query(By.css('div.thumbnail-placeholder'))).toBeFalsy(); - comp.isLoading = false; + comp.isLoading$.next(false); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('div.thumbnail-placeholder'))).toBeTruthy(); }); @@ -247,14 +247,14 @@ describe('ThumbnailComponent', () => { describe('fallback', () => { describe('if there is a default image', () => { it('should display the default image', () => { - comp.src = 'http://bit.stream'; + comp.src$.next('http://bit.stream'); comp.defaultImage = 'http://default.img'; comp.errorHandler(); - expect(comp.src).toBe(comp.defaultImage); + expect(comp.src$.getValue()).toBe(comp.defaultImage); }); it('should include the alt text', () => { - comp.src = 'http://bit.stream'; + comp.src$.next('http://bit.stream'); comp.defaultImage = 'http://default.img'; comp.errorHandler(); @@ -266,10 +266,10 @@ describe('ThumbnailComponent', () => { describe('if there is no default image', () => { it('should display the HTML placeholder', () => { - comp.src = 'http://default.img'; + comp.src$.next('http://default.img'); comp.defaultImage = null; comp.errorHandler(); - expect(comp.src).toBe(null); + expect(comp.src$.getValue()).toBe(null); fixture.detectChanges(); const placeholder = fixture.debugElement.query(By.css('div.thumbnail-placeholder')).nativeElement; @@ -361,7 +361,7 @@ describe('ThumbnailComponent', () => { it('should show the default image', () => { comp.defaultImage = 'default/image.jpg'; comp.ngOnChanges({}); - expect(comp.src).toBe('default/image.jpg'); + expect(comp.src$.getValue()).toBe('default/image.jpg'); }); }); }); @@ -417,7 +417,7 @@ describe('ThumbnailComponent', () => { }); it('should start out with isLoading$ true', () => { - expect(comp.isLoading).toBeTrue(); + expect(comp.isLoading$.getValue()).toBeTrue(); expect(de.query(By.css('ds-loading'))).toBeTruthy(); }); diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index 7b22dde4cd..179af1dac0 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -11,7 +11,10 @@ import { SimpleChanges, } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; -import { of as observableOf } from 'rxjs'; +import { + BehaviorSubject, + of as observableOf, +} from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { AuthService } from '../core/auth/auth.service'; @@ -55,7 +58,7 @@ export class ThumbnailComponent implements OnChanges { /** * The src attribute used in the template to render the image. */ - src: string = undefined; + src$: BehaviorSubject = new BehaviorSubject(undefined); retriedWithToken = false; @@ -78,7 +81,7 @@ export class ThumbnailComponent implements OnChanges { * Whether the thumbnail is currently loading * Start out as true to avoid flashing the alt text while a thumbnail is being loaded. */ - isLoading = true; + isLoading$: BehaviorSubject = new BehaviorSubject(true); constructor( @Inject(PLATFORM_ID) private platformID: any, @@ -134,7 +137,7 @@ export class ThumbnailComponent implements OnChanges { * Otherwise, fall back to the default image or a HTML placeholder */ errorHandler() { - const src = this.src; + const src = this.src$.getValue(); const thumbnail = this.bitstream; const thumbnailSrc = thumbnail?._links?.content?.href; @@ -186,9 +189,22 @@ export class ThumbnailComponent implements OnChanges { * @param src */ setSrc(src: string): void { - this.src = src; - if (src === null) { - this.isLoading = false; + // only update the src if it has changed (the parent component may fire the same one multiple times + if (this.src$.getValue() !== src) { + // every time the src changes we need to start the loading animation again, as it's possible + // that it is first set to null when the parent component initializes and then set to + // the actual value + // + // isLoading$ will be set to false by the error or success handler afterwards, except in the + // case where src is null, then we have to set it manually here (because those handlers won't + // trigger) + if (src !== null && this.isLoading$.getValue() === false) { + this.isLoading$.next(true); + } + this.src$.next(src); + if (src === null && this.isLoading$.getValue() === true) { + this.isLoading$.next(false); + } } } @@ -196,6 +212,6 @@ export class ThumbnailComponent implements OnChanges { * Stop the loading animation once the thumbnail is successfully loaded */ successHandler() { - this.isLoading = false; + this.isLoading$.next(false); } } diff --git a/src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.scss b/src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.scss index 444d3a9722..5293fdfb57 100644 --- a/src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.scss +++ b/src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.scss @@ -28,7 +28,6 @@ ::ng-deep { .ds-menu-item, .ds-menu-toggler-wrapper { - white-space: nowrap; text-decoration: none; } diff --git a/src/themes/dspace/app/header/header.component.html b/src/themes/dspace/app/header/header.component.html index e0a55b3dee..e66adf8c05 100644 --- a/src/themes/dspace/app/header/header.component.html +++ b/src/themes/dspace/app/header/header.component.html @@ -1,5 +1,5 @@
-
+
-
-
+