From be6dbdec66e6caf000c966a2e1e30f3acf199458 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 2 Oct 2023 13:31:25 +0200 Subject: [PATCH 001/123] 107155: Allow caching null objects --- src/app/core/cache/object-cache.reducer.ts | 31 +++++++++++-------- src/app/core/cache/object-cache.service.ts | 16 +++++++--- .../dspace-rest-response-parsing.service.ts | 7 +++-- src/app/core/index/index.effects.ts | 4 +-- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index dc3f50db68..7f389344fb 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -166,20 +166,25 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi * the new state, with the object added, or overwritten. */ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState { - const existing = state[action.payload.objectToCache._links.self.href] || {} as any; + const cacheLink = hasValue(action.payload.objectToCache) ? action.payload.objectToCache._links.self.href : action.payload.alternativeLink; + const existing = state[cacheLink] || {} as any; const newAltLinks = hasValue(action.payload.alternativeLink) ? [action.payload.alternativeLink] : []; - return Object.assign({}, state, { - [action.payload.objectToCache._links.self.href]: { - data: action.payload.objectToCache, - timeCompleted: action.payload.timeCompleted, - msToLive: action.payload.msToLive, - requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])], - dependentRequestUUIDs: existing.dependentRequestUUIDs || [], - isDirty: isNotEmpty(existing.patches), - patches: existing.patches || [], - alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks] - } as ObjectCacheEntry - }); + if (hasValue(cacheLink)) { + return Object.assign({}, state, { + [cacheLink]: { + data: action.payload.objectToCache, + timeCompleted: action.payload.timeCompleted, + msToLive: action.payload.msToLive, + requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])], + dependentRequestUUIDs: existing.dependentRequestUUIDs || [], + isDirty: isNotEmpty(existing.patches), + patches: existing.patches || [], + alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks] + } as ObjectCacheEntry + }); + } else { + return state; + } } /** diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 9ca0216210..0330a03f02 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -63,7 +63,9 @@ export class ObjectCacheService { * An optional alternative link to this object */ add(object: CacheableObject, msToLive: number, requestUUID: string, alternativeLink?: string): void { - object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links + if (hasValue(object)) { + object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links + } this.store.dispatch(new AddToObjectCacheAction(object, new Date().getTime(), msToLive, requestUUID, alternativeLink)); } @@ -139,11 +141,15 @@ export class ObjectCacheService { } ), map((entry: ObjectCacheEntry) => { - const type: GenericConstructor = getClassForType((entry.data as any).type); - if (typeof type !== 'function') { - throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`); + if (hasValue(entry.data)) { + const type: GenericConstructor = getClassForType((entry.data as any).type); + if (typeof type !== 'function') { + throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`); + } + return Object.assign(new type(), entry.data) as T; + } else { + return null; } - return Object.assign(new type(), entry.data) as T; }) ); } diff --git a/src/app/core/data/dspace-rest-response-parsing.service.ts b/src/app/core/data/dspace-rest-response-parsing.service.ts index 500afc4aff..2f79edd129 100644 --- a/src/app/core/data/dspace-rest-response-parsing.service.ts +++ b/src/app/core/data/dspace-rest-response-parsing.service.ts @@ -109,6 +109,9 @@ export class DspaceRestResponseParsingService implements ResponseParsingService if (hasValue(match)) { embedAltUrl = new URLCombiner(embedAltUrl, `?size=${match.size}`).toString(); } + if (data._embedded[property] == null) { + this.addToObjectCache(null, request, data, embedAltUrl); + } this.process(data._embedded[property], request, embedAltUrl); }); } @@ -226,7 +229,7 @@ export class DspaceRestResponseParsingService implements ResponseParsingService * @param alternativeURL an alternative url that can be used to retrieve the object */ addToObjectCache(co: CacheableObject, request: RestRequest, data: any, alternativeURL?: string): void { - if (!isCacheableObject(co)) { + if (hasValue(co) && !isCacheableObject(co)) { const type = hasValue(data) && hasValue(data.type) ? data.type : 'object'; let dataJSON: string; if (hasValue(data._embedded)) { @@ -240,7 +243,7 @@ export class DspaceRestResponseParsingService implements ResponseParsingService return; } - if (alternativeURL === co._links.self.href) { + if (hasValue(co) && alternativeURL === co._links.self.href) { alternativeURL = undefined; } diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index 18d639023f..9ec013813d 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -27,7 +27,7 @@ export class UUIDIndexEffects { addObject$ = createEffect(() => this.actions$ .pipe( ofType(ObjectCacheActionTypes.ADD), - filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid)), + filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache) && hasValue(action.payload.objectToCache.uuid)), map((action: AddToObjectCacheAction) => { return new AddToIndexAction( IndexName.OBJECT, @@ -46,7 +46,7 @@ export class UUIDIndexEffects { ofType(ObjectCacheActionTypes.ADD), map((action: AddToObjectCacheAction) => { const alternativeLink = action.payload.alternativeLink; - const selfLink = action.payload.objectToCache._links.self.href; + const selfLink = hasValue(action.payload.objectToCache) ? action.payload.objectToCache._links.self.href : alternativeLink; if (hasValue(alternativeLink) && alternativeLink !== selfLink) { return new AddToIndexAction( IndexName.ALTERNATIVE_OBJECT_LINK, From 984c9bfc2a2271e65190e02902ea0589dcfe6c4f Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 26 Oct 2023 16:33:14 +0200 Subject: [PATCH 002/123] 107155: Allow caching of embedded objects without selflink --- src/app/core/cache/object-cache.reducer.ts | 2 +- src/app/core/data/dspace-rest-response-parsing.service.ts | 4 ++++ src/app/core/index/index.effects.ts | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 7f389344fb..21dc729f1b 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -166,7 +166,7 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi * the new state, with the object added, or overwritten. */ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState { - const cacheLink = hasValue(action.payload.objectToCache) ? action.payload.objectToCache._links.self.href : action.payload.alternativeLink; + const cacheLink = hasValue(action.payload.objectToCache?._links?.self) ? action.payload.objectToCache._links.self.href : action.payload.alternativeLink; const existing = state[cacheLink] || {} as any; const newAltLinks = hasValue(action.payload.alternativeLink) ? [action.payload.alternativeLink] : []; if (hasValue(cacheLink)) { diff --git a/src/app/core/data/dspace-rest-response-parsing.service.ts b/src/app/core/data/dspace-rest-response-parsing.service.ts index 2f79edd129..c0e1c70cae 100644 --- a/src/app/core/data/dspace-rest-response-parsing.service.ts +++ b/src/app/core/data/dspace-rest-response-parsing.service.ts @@ -110,7 +110,11 @@ export class DspaceRestResponseParsingService implements ResponseParsingService embedAltUrl = new URLCombiner(embedAltUrl, `?size=${match.size}`).toString(); } if (data._embedded[property] == null) { + // Embedded object is null, meaning it exists (not undefined), but had an empty response (204) -> cache it as null this.addToObjectCache(null, request, data, embedAltUrl); + } else if (!isCacheableObject(data._embedded[property])) { + // Embedded object exists, but doesn't contain a self link -> cache it using the alternative link instead + this.objectCache.add(data._embedded[property], hasValue(request.responseMsToLive) ? request.responseMsToLive : environment.cache.msToLive.default, request.uuid, embedAltUrl); } this.process(data._embedded[property], request, embedAltUrl); }); diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index 9ec013813d..65aa45e571 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -46,7 +46,7 @@ export class UUIDIndexEffects { ofType(ObjectCacheActionTypes.ADD), map((action: AddToObjectCacheAction) => { const alternativeLink = action.payload.alternativeLink; - const selfLink = hasValue(action.payload.objectToCache) ? action.payload.objectToCache._links.self.href : alternativeLink; + const selfLink = hasValue(action.payload.objectToCache?._links?.self) ? action.payload.objectToCache._links.self.href : alternativeLink; if (hasValue(alternativeLink) && alternativeLink !== selfLink) { return new AddToIndexAction( IndexName.ALTERNATIVE_OBJECT_LINK, From a23cdfbc2b1dd6dd4a494432c992fdac3a38fc70 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 26 Jun 2024 08:32:43 +0200 Subject: [PATCH 003/123] 115284: Add repeatable based on relationship max cardinality --- .../edit-relationship-list.component.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index 6cca52ba96..835ee4ad7a 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -232,6 +232,22 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { return update && update.field ? update.field.uuid : undefined; } + /** + * Check whether the current entity can have multiple relationships of this type + * This is based on the max cardinality of the relationship + * @private + */ + private isRepeatable(): boolean { + const isLeft = this.currentItemIsLeftItem$.getValue(); + if (isLeft) { + const leftMaxCardinality = this.relationshipType.leftMaxCardinality; + return hasNoValue(leftMaxCardinality) || leftMaxCardinality > 1; + } else { + const rightMaxCardinality = this.relationshipType.rightMaxCardinality; + return hasNoValue(rightMaxCardinality) || rightMaxCardinality > 1; + } + } + /** * Open the dynamic lookup modal to search for items to add as relationships */ @@ -249,6 +265,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { modalComp.toAdd = []; modalComp.toRemove = []; modalComp.isPending = false; + modalComp.repeatable = this.isRepeatable(); modalComp.hiddenQuery = '-search.resourceid:' + this.item.uuid; this.item.owningCollection.pipe( From bb770ba65b7941f936d8e657771a88cb11994aeb Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Wed, 10 Jul 2024 11:34:18 +0200 Subject: [PATCH 004/123] [CST-14903] Orcid Synchronization improvements feat: - Introduces reactive states derived from item inside orcid-sync page - Removes unnecessary navigation ref: - Introduces catchError operator and handles failures with error messages --- .../orcid-auth/orcid-auth.component.ts | 15 +- .../orcid-page/orcid-page.component.ts | 18 +- .../orcid-sync-settings.component.spec.ts | 10 +- .../orcid-sync-settings.component.ts | 165 ++++++++++++------ 4 files changed, 144 insertions(+), 64 deletions(-) diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts index ea970e7d31..73b4a7b4e1 100644 --- a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts @@ -1,8 +1,8 @@ import { Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service'; import { Item } from '../../../core/shared/item.model'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; @@ -10,6 +10,8 @@ import { RemoteData } from '../../../core/data/remote-data'; import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service'; +import { createFailedRemoteDataObject } from '../../../shared/remote-data.utils'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'ds-orcid-auth', @@ -170,14 +172,15 @@ export class OrcidAuthComponent implements OnInit, OnChanges { unlinkOrcid(): void { this.unlinkProcessing.next(true); this.orcidAuthService.unlinkOrcidByItem(this.item).pipe( - getFirstCompletedRemoteData() + getFirstCompletedRemoteData(), + catchError((err: HttpErrorResponse) => of(createFailedRemoteDataObject(err.message, err.status))) ).subscribe((remoteData: RemoteData) => { this.unlinkProcessing.next(false); - if (remoteData.isSuccess) { + if (remoteData.hasFailed) { + this.notificationsService.error(this.translateService.get('person.page.orcid.unlink.error')); + } else { this.notificationsService.success(this.translateService.get('person.page.orcid.unlink.success')); this.unlink.emit(); - } else { - this.notificationsService.error(this.translateService.get('person.page.orcid.unlink.error')); } }); } diff --git a/src/app/item-page/orcid-page/orcid-page.component.ts b/src/app/item-page/orcid-page/orcid-page.component.ts index f3dbb569d9..1d62c9691c 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.ts +++ b/src/app/item-page/orcid-page/orcid-page.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; import { BehaviorSubject, combineLatest } from 'rxjs'; -import { map, take } from 'rxjs/operators'; +import { filter, map, take } from 'rxjs/operators'; import { OrcidAuthService } from '../../core/orcid/orcid-auth.service'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; @@ -147,7 +147,19 @@ export class OrcidPageComponent implements OnInit { */ private clearRouteParams(): void { // update route removing the code from query params - const redirectUrl = this.router.url.split('?')[0]; - this.router.navigate([redirectUrl]); + this.route.queryParamMap + .pipe( + filter((paramMap: ParamMap) => isNotEmpty(paramMap.keys)), + map(_ => Object.assign({})), + take(1), + ).subscribe(queryParams => + this.router.navigate( + [], + { + relativeTo: this.route, + queryParams + } + ) + ); } } diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts index f2fa9d2440..38a6df909e 100644 --- a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { RouterTestingModule } from '@angular/router/testing'; import { By } from '@angular/platform-browser'; @@ -24,8 +24,8 @@ describe('OrcidSyncSettingsComponent test suite', () => { let comp: OrcidSyncSettingsComponent; let fixture: ComponentFixture; let scheduler: TestScheduler; - let researcherProfileService: jasmine.SpyObj; let notificationsService; + let researcherProfileService: jasmine.SpyObj; let formGroup: UntypedFormGroup; const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), { @@ -161,6 +161,7 @@ describe('OrcidSyncSettingsComponent test suite', () => { scheduler = getTestScheduler(); fixture = TestBed.createComponent(OrcidSyncSettingsComponent); comp = fixture.componentInstance; + researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); comp.item = mockItemLinkedToOrcid; fixture.detectChanges(); })); @@ -197,7 +198,6 @@ describe('OrcidSyncSettingsComponent test suite', () => { }); it('should call updateByOrcidOperations properly', () => { - researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); researcherProfileService.patch.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); const expectedOps: Operation[] = [ { @@ -226,7 +226,6 @@ describe('OrcidSyncSettingsComponent test suite', () => { }); it('should show notification on success', () => { - researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); researcherProfileService.patch.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); scheduler.schedule(() => comp.onSubmit(formGroup)); @@ -238,6 +237,8 @@ describe('OrcidSyncSettingsComponent test suite', () => { it('should show notification on error', () => { researcherProfileService.findByRelatedItem.and.returnValue(createFailedRemoteDataObject$()); + comp.item = mockItemLinkedToOrcid; + fixture.detectChanges(); scheduler.schedule(() => comp.onSubmit(formGroup)); scheduler.flush(); @@ -247,7 +248,6 @@ describe('OrcidSyncSettingsComponent test suite', () => { }); it('should show notification on error', () => { - researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); researcherProfileService.patch.and.returnValue(createFailedRemoteDataObject$()); scheduler.schedule(() => comp.onSubmit(formGroup)); 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 0bcbc295ac..422041d340 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 @@ -1,80 +1,97 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { Operation } from 'fast-json-patch'; -import { of } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { catchError, filter, map, switchMap, take, takeUntil } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { ResearcherProfileDataService } from '../../../core/profile/researcher-profile-data.service'; import { Item } from '../../../core/shared/item.model'; -import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; +import { hasValue } from '../../../shared/empty.util'; +import { HttpErrorResponse } from '@angular/common/http'; +import { createFailedRemoteDataObject } from '../../../shared/remote-data.utils'; @Component({ selector: 'ds-orcid-sync-setting', templateUrl: './orcid-sync-settings.component.html', styleUrls: ['./orcid-sync-settings.component.scss'] }) -export class OrcidSyncSettingsComponent implements OnInit { - - /** - * The item for which showing the orcid settings - */ - @Input() item: Item; +export class OrcidSyncSettingsComponent implements OnInit, OnDestroy { /** * The prefix used for i18n keys */ messagePrefix = 'person.page.orcid'; - /** * The current synchronization mode */ currentSyncMode: string; - /** * The current synchronization mode for publications */ currentSyncPublications: string; - /** * The current synchronization mode for funding */ currentSyncFunding: string; - /** * The synchronization options */ syncModes: { value: string, label: string }[]; - /** * The synchronization options for publications */ syncPublicationOptions: { value: string, label: string }[]; - /** * The synchronization options for funding */ syncFundingOptions: { value: string, label: string }[]; - /** * The profile synchronization options */ syncProfileOptions: { value: string, label: string, checked: boolean }[]; - /** * An event emitted when settings are updated */ @Output() settingsUpdated: EventEmitter = new EventEmitter(); + /** + * Emitter that triggers onDestroy lifecycle + * @private + */ + readonly #destroy$ = new EventEmitter(); + /** + * {@link BehaviorSubject} that reflects {@link item} input changes + * @private + */ + readonly #item$ = new BehaviorSubject(null); + /** + * {@link Observable} that contains {@link ResearcherProfile} linked to the {@link #item$} + * @private + */ + #researcherProfile$: Observable; constructor(private researcherProfileService: ResearcherProfileDataService, private notificationsService: NotificationsService, private translateService: TranslateService) { } + /** + * The item for which showing the orcid settings + */ + @Input() + set item(item: Item) { + this.#item$.next(item); + } + + ngOnDestroy(): void { + this.#destroy$.next(); + } + /** * Init orcid settings form */ @@ -106,20 +123,21 @@ export class OrcidSyncSettingsComponent implements OnInit { }; }); - const syncProfilePreferences = this.item.allMetadataValues('dspace.orcid.sync-profile'); + this.updateSyncProfileOptions(this.#item$.asObservable()); + this.updateSyncPreferences(this.#item$.asObservable()); - this.syncProfileOptions = ['BIOGRAPHICAL', 'IDENTIFIERS'] - .map((value) => { - return { - label: this.messagePrefix + '.sync-profile.' + value.toLowerCase(), - value: value, - checked: syncProfilePreferences.includes(value) - }; - }); - - this.currentSyncMode = this.getCurrentPreference('dspace.orcid.sync-mode', ['BATCH', 'MANUAL'], 'MANUAL'); - this.currentSyncPublications = this.getCurrentPreference('dspace.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED'); - this.currentSyncFunding = this.getCurrentPreference('dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED'); + this.#researcherProfile$ = + this.#item$.pipe( + switchMap(item => + this.researcherProfileService.findByRelatedItem(item) + .pipe( + getFirstCompletedRemoteData(), + catchError((err: HttpErrorResponse) => of(createFailedRemoteDataObject(err.message, err.status))), + getRemoteDataPayload(), + ) + ), + takeUntil(this.#destroy$) + ); } /** @@ -144,37 +162,84 @@ export class OrcidSyncSettingsComponent implements OnInit { return; } - this.researcherProfileService.findByRelatedItem(this.item).pipe( - getFirstCompletedRemoteData(), - switchMap((profileRD: RemoteData) => { - if (profileRD.hasSucceeded) { - return this.researcherProfileService.patch(profileRD.payload, operations).pipe( - getFirstCompletedRemoteData(), - ); + this.#researcherProfile$ + .pipe( + switchMap(researcherProfile => this.researcherProfileService.patch(researcherProfile, operations)), + getFirstCompletedRemoteData(), + catchError((err: HttpErrorResponse) => of(createFailedRemoteDataObject(err.message, err.status))), + take(1) + ) + .subscribe((remoteData: RemoteData) => { + if (remoteData.hasFailed) { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.error')); } else { - return of(profileRD); + this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success')); + this.settingsUpdated.emit(); } - }), - ).subscribe((remoteData: RemoteData) => { - if (remoteData.isSuccess) { - this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success')); - this.settingsUpdated.emit(); - } else { - this.notificationsService.error(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.error')); - } - }); + }); + } + + /** + * + * Handles subscriptions to populate sync preferences + * + * @param item observable that emits update on item changes + * @private + */ + private updateSyncPreferences(item: Observable) { + item.pipe( + filter(hasValue), + map(i => this.getCurrentPreference(i, 'dspace.orcid.sync-mode', ['BATCH', 'MANUAL'], 'MANUAL')), + takeUntil(this.#destroy$) + ).subscribe(val => this.currentSyncMode = val); + item.pipe( + filter(hasValue), + map(i => this.getCurrentPreference(i, 'dspace.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED')), + takeUntil(this.#destroy$) + ).subscribe(val => this.currentSyncPublications = val); + item.pipe( + filter(hasValue), + map(i => this.getCurrentPreference(i, 'dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED')), + takeUntil(this.#destroy$) + ).subscribe(val => this.currentSyncFunding = val); + } + + /** + * Handles subscription to populate the {@link syncProfileOptions} field + * + * @param item observable that emits update on item changes + * @private + */ + private updateSyncProfileOptions(item: Observable) { + item.pipe( + filter(hasValue), + map(i => i.allMetadataValues('dspace.orcid.sync-profile')), + map(metadata => + ['BIOGRAPHICAL', 'IDENTIFIERS'] + .map((value) => { + return { + label: this.messagePrefix + '.sync-profile.' + value.toLowerCase(), + value: value, + checked: metadata.includes(value) + }; + }) + ), + takeUntil(this.#destroy$) + ) + .subscribe(value => this.syncProfileOptions = value); } /** * Retrieve setting saved in the item's metadata * + * @param item The item from which retrieve settings * @param metadataField The metadata name that contains setting * @param allowedValues The allowed values * @param defaultValue The default value * @private */ - private getCurrentPreference(metadataField: string, allowedValues: string[], defaultValue: string): string { - const currentPreference = this.item.firstMetadataValue(metadataField); + private getCurrentPreference(item: Item, metadataField: string, allowedValues: string[], defaultValue: string): string { + const currentPreference = item.firstMetadataValue(metadataField); return (currentPreference && allowedValues.includes(currentPreference)) ? currentPreference : defaultValue; } From 70f0af66117722e4d12d2b828ee85537946e69de Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Tue, 3 Sep 2024 11:36:04 +0200 Subject: [PATCH 005/123] 117287: Removed method calls returning observables from the EPerson form --- .../epeople-registry.component.html | 2 +- .../epeople-registry.component.ts | 22 +- .../eperson-form/eperson-form.component.html | 4 +- .../eperson-form.component.spec.ts | 69 ++--- .../eperson-form/eperson-form.component.ts | 244 +++++++++--------- 5 files changed, 147 insertions(+), 194 deletions(-) diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.html b/src/app/access-control/epeople-registry/epeople-registry.component.html index e3a8e2c590..bd47124856 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -66,7 +66,7 @@ + [ngClass]="{'table-primary' : (activeEPerson$ | async) === epersonDto.eperson}"> {{epersonDto.eperson.id}} {{ dsoNameService.getName(epersonDto.eperson) }} {{epersonDto.eperson.email}} 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 fb045ebb88..f180534587 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.ts @@ -45,6 +45,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { */ ePeopleDto$: BehaviorSubject> = new BehaviorSubject>({} as any); + activeEPerson$: Observable; + /** * An observable for the pageInfo, needed to pass to the pagination component */ @@ -121,6 +123,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { this.isEPersonFormShown = true; } })); + this.activeEPerson$ = this.epersonService.getActiveEPerson(); this.subs.push(this.ePeople$.pipe( switchMap((epeople: PaginatedList) => { if (epeople.pageInfo.totalElements > 0) { @@ -188,29 +191,12 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { ); } - /** - * Checks whether the given EPerson is active (being edited) - * @param eperson - */ - isActive(eperson: EPerson): Observable { - return this.getActiveEPerson().pipe( - map((activeEPerson) => eperson === activeEPerson) - ); - } - - /** - * Gets the active eperson (being edited) - */ - getActiveEPerson(): Observable { - return this.epersonService.getActiveEPerson(); - } - /** * Start editing the selected EPerson * @param ePerson */ toggleEditEPerson(ePerson: EPerson) { - this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => { + this.activeEPerson$.pipe(take(1)).subscribe((activeEPerson: EPerson) => { if (ePerson === activeEPerson) { this.epersonService.cancelEditEPerson(); this.isEPersonFormShown = false; diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html index 228449a8a5..3c5b2d13b4 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html @@ -1,4 +1,4 @@ -
+

{{messagePrefix + '.create' | translate}}

@@ -39,7 +39,7 @@ -
+
{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}
diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts index fb911e709c..dc2e380c33 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts @@ -1,11 +1,11 @@ import { Observable, of as observableOf } from 'rxjs'; import { CommonModule } from '@angular/common'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateModule } from '@ngx-translate/core'; import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; @@ -19,7 +19,6 @@ import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock' import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; import { AuthService } from '../../../core/auth/auth.service'; import { AuthServiceStub } from '../../../shared/testing/auth-service.stub'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; @@ -31,6 +30,7 @@ import { PaginationServiceStub } from '../../../shared/testing/pagination-servic import { FindListOptions } from '../../../core/data/find-list-options.model'; import { ValidateEmailNotTaken } from './validators/email-taken.validator'; import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service'; +import { RouterTestingModule } from '@angular/router/testing'; describe('EPersonFormComponent', () => { let component: EPersonFormComponent; @@ -53,9 +53,6 @@ describe('EPersonFormComponent', () => { ePersonDataServiceStub = { activeEPerson: null, allEpeople: mockEPeople, - getEPeople(): Observable>> { - return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople)); - }, getActiveEPerson(): Observable { return observableOf(this.activeEPerson); }, @@ -184,12 +181,8 @@ describe('EPersonFormComponent', () => { paginationService = new PaginationServiceStub(); TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), + RouterTestingModule, + TranslateModule.forRoot(), ], declarations: [EPersonFormComponent], providers: [ @@ -204,7 +197,7 @@ describe('EPersonFormComponent', () => { { provide: EpersonRegistrationService, useValue: epersonRegistrationService }, EPeopleRegistryComponent ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); })); @@ -223,37 +216,13 @@ describe('EPersonFormComponent', () => { }); describe('check form validation', () => { - let firstName; - let lastName; - let email; - let canLogIn; - let requireCertificate; + let canLogIn: boolean; + let requireCertificate: boolean; - let expected; beforeEach(() => { - firstName = 'testName'; - lastName = 'testLastName'; - email = 'testEmail@test.com'; canLogIn = false; requireCertificate = false; - expected = Object.assign(new EPerson(), { - metadata: { - 'eperson.firstname': [ - { - value: firstName - } - ], - 'eperson.lastname': [ - { - value: lastName - }, - ], - }, - email: email, - canLogIn: canLogIn, - requireCertificate: requireCertificate, - }); spyOn(component.submitForm, 'emit'); component.canLogIn.value = canLogIn; component.requireCertificate.value = requireCertificate; @@ -343,15 +312,13 @@ describe('EPersonFormComponent', () => { }); })); }); - - - }); + describe('when submitting the form', () => { let firstName; let lastName; let email; - let canLogIn; + let canLogIn: boolean; let requireCertificate; let expected; @@ -380,6 +347,7 @@ describe('EPersonFormComponent', () => { requireCertificate: requireCertificate, }); spyOn(component.submitForm, 'emit'); + component.ngOnInit(); component.firstName.value = firstName; component.lastName.value = lastName; component.email.value = email; @@ -421,9 +389,17 @@ describe('EPersonFormComponent', () => { email: email, canLogIn: canLogIn, requireCertificate: requireCertificate, - _links: undefined + _links: { + groups: { + href: '', + }, + self: { + href: '', + }, + }, }); spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId)); + component.ngOnInit(); component.onSubmit(); fixture.detectChanges(); }); @@ -473,22 +449,19 @@ describe('EPersonFormComponent', () => { }); describe('delete', () => { - - let ePersonId; let eperson: EPerson; let modalService; beforeEach(() => { spyOn(authService, 'impersonate').and.callThrough(); - ePersonId = 'testEPersonId'; eperson = EPersonMock; component.epersonInitial = eperson; component.canDelete$ = observableOf(true); spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson)); modalService = (component as any).modalService; spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) })); + component.ngOnInit(); fixture.detectChanges(); - }); it('the delete button should be active if the eperson can be deleted', () => { 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 d009d56058..296038ca2b 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 @@ -137,6 +137,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy { */ canImpersonate$: Observable; + /** + * The current {@link EPerson} + */ + activeEPerson$: Observable; + /** * List of subscriptions */ @@ -195,7 +200,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy { private epersonRegistrationService: EpersonRegistrationService, public dsoNameService: DSONameService, ) { - this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { + } + + ngOnInit() { + this.activeEPerson$ = this.epersonService.getActiveEPerson(); + this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => { this.epersonInitial = eperson; if (hasValue(eperson)) { this.isImpersonated = this.authService.isImpersonatingUser(eperson.id); @@ -203,9 +212,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy { this.submitLabel = 'form.submit'; } })); - } - - ngOnInit() { this.initialisePage(); } @@ -213,124 +219,112 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * This method will initialise the page */ initialisePage() { - - observableCombineLatest([ - this.translateService.get(`${this.messagePrefix}.firstName`), - this.translateService.get(`${this.messagePrefix}.lastName`), - this.translateService.get(`${this.messagePrefix}.email`), - this.translateService.get(`${this.messagePrefix}.canLogIn`), - this.translateService.get(`${this.messagePrefix}.requireCertificate`), - this.translateService.get(`${this.messagePrefix}.emailHint`), - ]).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => { - this.firstName = new DynamicInputModel({ - id: 'firstName', - label: firstName, - name: 'firstName', - validators: { - required: null, - }, - required: true, - }); - this.lastName = new DynamicInputModel({ - id: 'lastName', - label: lastName, - name: 'lastName', - validators: { - required: null, - }, - required: true, - }); - this.email = new DynamicInputModel({ - id: 'email', - label: email, - name: 'email', - validators: { - required: null, - pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$', - }, - required: true, - errorMessages: { - emailTaken: 'error.validation.emailTaken', - pattern: 'error.validation.NotValidEmail' - }, - hint: emailHint - }); - this.canLogIn = new DynamicCheckboxModel( - { - id: 'canLogIn', - label: canLogIn, - name: 'canLogIn', - value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true) - }); - this.requireCertificate = new DynamicCheckboxModel( - { - id: 'requireCertificate', - label: requireCertificate, - name: 'requireCertificate', - value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false) - }); - this.formModel = [ - this.firstName, - this.lastName, - this.email, - this.canLogIn, - this.requireCertificate, - ]; - this.formGroup = this.formBuilderService.createFormGroup(this.formModel); - this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { - if (eperson != null) { - this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, { - currentPage: 1, - elementsPerPage: this.config.pageSize - }); - } - this.formGroup.patchValue({ - firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '', - lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '', - email: eperson != null ? eperson.email : '', - canLogIn: eperson != null ? eperson.canLogIn : true, - requireCertificate: eperson != null ? eperson.requireCertificate : false - }); - - if (eperson === null && !!this.formGroup.controls.email) { - this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService)); - this.emailValueChangeSubscribe = this.email.valueChanges.pipe(debounceTime(300)).subscribe(() => { - this.changeDetectorRef.detectChanges(); - }); - } - })); - - const activeEPerson$ = this.epersonService.getActiveEPerson(); - - this.groups = activeEPerson$.pipe( - switchMap((eperson) => { - return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, { - currentPage: 1, - elementsPerPage: this.config.pageSize - })]); - }), - switchMap(([eperson, findListOptions]) => { - if (eperson != null) { - return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object')); - } - return observableOf(undefined); - }) - ); - - this.canImpersonate$ = activeEPerson$.pipe( - switchMap((eperson) => { - if (hasValue(eperson)) { - return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self); - } else { - return observableOf(false); - } - }) - ); - this.canDelete$ = activeEPerson$.pipe( - switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)) - ); - this.canReset$ = observableOf(true); + this.firstName = new DynamicInputModel({ + id: 'firstName', + label: this.translateService.instant(`${this.messagePrefix}.firstName`), + name: 'firstName', + validators: { + required: null, + }, + required: true, }); + this.lastName = new DynamicInputModel({ + id: 'lastName', + label: this.translateService.instant(`${this.messagePrefix}.lastName`), + name: 'lastName', + validators: { + required: null, + }, + required: true, + }); + this.email = new DynamicInputModel({ + id: 'email', + label: this.translateService.instant(`${this.messagePrefix}.email`), + name: 'email', + validators: { + required: null, + pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$', + }, + required: true, + errorMessages: { + emailTaken: 'error.validation.emailTaken', + pattern: 'error.validation.NotValidEmail' + }, + hint: this.translateService.instant(`${this.messagePrefix}.emailHint`), + }); + this.canLogIn = new DynamicCheckboxModel( + { + id: 'canLogIn', + label: this.translateService.instant(`${this.messagePrefix}.canLogIn`), + name: 'canLogIn', + value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true) + }); + this.requireCertificate = new DynamicCheckboxModel( + { + id: 'requireCertificate', + label: this.translateService.instant(`${this.messagePrefix}.requireCertificate`), + name: 'requireCertificate', + value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false) + }); + this.formModel = [ + this.firstName, + this.lastName, + this.email, + this.canLogIn, + this.requireCertificate, + ]; + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => { + if (eperson != null) { + this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, { + currentPage: 1, + elementsPerPage: this.config.pageSize + }); + } + this.formGroup.patchValue({ + firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '', + lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '', + email: eperson != null ? eperson.email : '', + canLogIn: eperson != null ? eperson.canLogIn : true, + requireCertificate: eperson != null ? eperson.requireCertificate : false + }); + + if (eperson === null && !!this.formGroup.controls.email) { + this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService)); + this.emailValueChangeSubscribe = this.email.valueChanges.pipe(debounceTime(300)).subscribe(() => { + this.changeDetectorRef.detectChanges(); + }); + } + })); + + this.groups = this.activeEPerson$.pipe( + switchMap((eperson) => { + return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, { + currentPage: 1, + elementsPerPage: this.config.pageSize + })]); + }), + switchMap(([eperson, findListOptions]) => { + if (eperson != null) { + return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object')); + } + return observableOf(undefined); + }) + ); + + this.canImpersonate$ = this.activeEPerson$.pipe( + switchMap((eperson) => { + if (hasValue(eperson)) { + return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self); + } else { + return observableOf(false); + } + }) + ); + this.canDelete$ = this.activeEPerson$.pipe( + switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)) + ); + this.canReset$ = observableOf(true); } /** @@ -348,7 +342,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Emit the updated/created eperson using the EventEmitter submitForm */ onSubmit() { - this.epersonService.getActiveEPerson().pipe(take(1)).subscribe( + this.activeEPerson$.pipe(take(1)).subscribe( (ePerson: EPerson) => { const values = { metadata: { @@ -464,7 +458,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * It'll either show a success or error message depending on whether the delete was successful or not. */ delete(): void { - this.epersonService.getActiveEPerson().pipe( + this.activeEPerson$.pipe( take(1), switchMap((eperson: EPerson) => { const modalRef = this.modalService.open(ConfirmationModalComponent); @@ -545,7 +539,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * This method will ensure that the page gets reset and that the cache is cleared */ reset() { - this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => { + this.activeEPerson$.pipe(take(1)).subscribe((eperson: EPerson) => { this.requestService.removeByHrefSubstring(eperson.self); }); this.initialisePage(); @@ -577,7 +571,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Update the list of groups by fetching it from the rest api or cache */ private updateGroups(options) { - this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { + this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => { this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, options); })); } From d3019e4006aaa72f1dd8561a8d83050e48a9cebb Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Tue, 3 Sep 2024 18:44:49 +0200 Subject: [PATCH 006/123] 117287: Removed method calls returning observables from the Registry Formats page --- .../bitstream-formats.component.html | 16 +++---- .../bitstream-formats.component.spec.ts | 22 ++++------ .../bitstream-formats.component.ts | 42 +++++++++---------- 3 files changed, 34 insertions(+), 46 deletions(-) diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html index 0a2e9f0f92..97740d5ea1 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html @@ -9,10 +9,10 @@
@@ -27,11 +27,11 @@ - + @@ -45,13 +45,13 @@
-
diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts index 8a44240b7e..ec59f57c80 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts @@ -15,8 +15,7 @@ import { NotificationsService } from '../../../shared/notifications/notification import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../../core/shared/bitstream-format-support-level'; -import { cold, getTestScheduler, hot } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/testing'; +import { hot } from 'jasmine-marbles'; import { createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject, @@ -31,7 +30,6 @@ describe('BitstreamFormatsComponent', () => { let comp: BitstreamFormatsComponent; let fixture: ComponentFixture; let bitstreamFormatService; - let scheduler: TestScheduler; let notificationsServiceStub; let paginationService; @@ -86,8 +84,6 @@ describe('BitstreamFormatsComponent', () => { const initAsync = () => { notificationsServiceStub = new NotificationsServiceStub(); - scheduler = getTestScheduler(); - bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), @@ -178,17 +174,17 @@ describe('BitstreamFormatsComponent', () => { beforeEach(waitForAsync(initAsync)); beforeEach(initBeforeEach); it('should return an observable of true if the provided bistream is in the list returned by the service', () => { - const result = comp.isSelected(bitstreamFormat1); - - expect(result).toBeObservable(cold('b', { b: true })); + comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => { + expect(selectedBitstreamFormatIDs).toContain(bitstreamFormat1.id); + }); }); it('should return an observable of false if the provided bistream is not in the list returned by the service', () => { const format = new BitstreamFormat(); format.uuid = 'new'; - const result = comp.isSelected(format); - - expect(result).toBeObservable(cold('b', { b: false })); + comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => { + expect(selectedBitstreamFormatIDs).not.toContain(format.id); + }); }); }); @@ -214,8 +210,6 @@ describe('BitstreamFormatsComponent', () => { beforeEach(waitForAsync(() => { notificationsServiceStub = new NotificationsServiceStub(); - scheduler = getTestScheduler(); - bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), @@ -263,8 +257,6 @@ describe('BitstreamFormatsComponent', () => { beforeEach(waitForAsync(() => { notificationsServiceStub = new NotificationsServiceStub(); - scheduler = getTestScheduler(); - bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts index 162bf2bdb2..263c1aede2 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { combineLatest as observableCombineLatest, Observable} from 'rxjs'; +import { Observable} from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; @@ -7,7 +7,6 @@ import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service'; import { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { NoContent } from '../../../core/shared/NoContent.model'; import { PaginationService } from '../../../core/pagination/pagination.service'; @@ -26,7 +25,12 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { /** * A paginated list of bitstream formats to be shown on the page */ - bitstreamFormats: Observable>>; + bitstreamFormats$: Observable>>; + + /** + * The currently selected {@link BitstreamFormat} IDs + */ + selectedBitstreamFormatIDs$: Observable; /** * The current pagination configuration for the page @@ -39,7 +43,6 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { }); constructor(private notificationsService: NotificationsService, - private router: Router, private translateService: TranslateService, private bitstreamFormatService: BitstreamFormatDataService, private paginationService: PaginationService, @@ -94,14 +97,11 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { } /** - * Checks whether a given bitstream format is selected in the list (checkbox) - * @param bitstreamFormat + * Returns the list of all the bitstream formats that are selected in the list (checkbox) */ - isSelected(bitstreamFormat: BitstreamFormat): Observable { + selectedBitstreamFormatIDs(): Observable { return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe( - map((bitstreamFormats: BitstreamFormat[]) => { - return bitstreamFormats.find((selectedFormat) => selectedFormat.id === bitstreamFormat.id) != null; - }) + map((bitstreamFormats: BitstreamFormat[]) => bitstreamFormats.map((selectedFormat) => selectedFormat.id)), ); } @@ -125,27 +125,23 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { const prefix = 'admin.registries.bitstream-formats.delete'; const suffix = success ? 'success' : 'failure'; - const messages = observableCombineLatest( - this.translateService.get(`${prefix}.${suffix}.head`), - this.translateService.get(`${prefix}.${suffix}.amount`, {amount: amount}) - ); - messages.subscribe(([head, content]) => { + const head: string = this.translateService.instant(`${prefix}.${suffix}.head`); + const content: string = this.translateService.instant(`${prefix}.${suffix}.amount`, { amount: amount }); - if (success) { - this.notificationsService.success(head, content); - } else { - this.notificationsService.error(head, content); - } - }); + if (success) { + this.notificationsService.success(head, content); + } else { + this.notificationsService.error(head, content); + } } ngOnInit(): void { - - this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe( + this.bitstreamFormats$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe( switchMap((findListOptions: FindListOptions) => { return this.bitstreamFormatService.findAll(findListOptions); }) ); + this.selectedBitstreamFormatIDs$ = this.selectedBitstreamFormatIDs(); } From 680ed3bccf38732ada602a7f01487c60035067ac Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Tue, 3 Sep 2024 22:08:38 +0200 Subject: [PATCH 007/123] 117287: Removed method calls returning observables from the Group form --- .../group-form/group-form.component.html | 35 +- .../group-form/group-form.component.spec.ts | 140 +++++--- .../group-form/group-form.component.ts | 337 ++++++++---------- 3 files changed, 259 insertions(+), 253 deletions(-) diff --git a/src/app/access-control/group-registry/group-form/group-form.component.html b/src/app/access-control/group-registry/group-form/group-form.component.html index 77a81a8daa..546d4598df 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.html +++ b/src/app/access-control/group-registry/group-form/group-form.component.html @@ -2,7 +2,7 @@
-
+

{{messagePrefix + '.head.create' | translate}}

@@ -23,11 +23,15 @@
- - - + + + + + + + {{messagePrefix + '.return' | translate}}
-
+
-
- -
- - - - + +
+ +
+ +
diff --git a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts index f8c5f3cd87..8d148b66fb 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts @@ -1,13 +1,13 @@ import { CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { Store } from '@ngrx/store'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable, of as observableOf } from 'rxjs'; import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; @@ -29,8 +29,6 @@ import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock'; import { GroupFormComponent } from './group-form.component'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; -import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; -import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock'; import { RouterMock } from '../../../shared/mocks/router.mock'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { Operation } from 'fast-json-patch'; @@ -38,23 +36,24 @@ import { ValidateGroupExists } from './validators/group-exists.validator'; import { NoContent } from '../../../core/shared/NoContent.model'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; describe('GroupFormComponent', () => { let component: GroupFormComponent; let fixture: ComponentFixture; - let translateService: TranslateService; let builderService: FormBuilderService; let ePersonDataServiceStub: any; let groupsDataServiceStub: any; let dsoDataServiceStub: any; let authorizationService: AuthorizationDataService; let notificationService: NotificationsServiceStub; - let router; + let router: RouterMock; + let route: ActivatedRouteStub; - let groups; - let groupName; - let groupDescription; - let expected; + let groups: Group[]; + let groupName: string; + let groupDescription: string; + let expected: Group; beforeEach(waitForAsync(() => { groups = [GroupMock, GroupMock2]; @@ -69,6 +68,15 @@ describe('GroupFormComponent', () => { } ], }, + object: createSuccessfulRemoteDataObject$(undefined), + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + } + }, }); ePersonDataServiceStub = {}; groupsDataServiceStub = { @@ -105,7 +113,14 @@ describe('GroupFormComponent', () => { create(group: Group): Observable> { this.allGroups = [...this.allGroups, group]; this.createdGroup = Object.assign({}, group, { - _links: { self: { href: 'group-selflink' } } + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); return createSuccessfulRemoteDataObject$(this.createdGroup); }, @@ -187,17 +202,13 @@ describe('GroupFormComponent', () => { return typeof value === 'object' && value !== null; } }); - translateService = getMockTranslateService(); router = new RouterMock(); + route = new ActivatedRouteStub(); notificationService = new NotificationsServiceStub(); + return TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), + TranslateModule.forRoot(), ], declarations: [GroupFormComponent], providers: [ @@ -214,14 +225,11 @@ describe('GroupFormComponent', () => { { provide: Store, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, - { - provide: ActivatedRoute, - useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) } - }, + { provide: ActivatedRoute, useValue: route }, { provide: Router, useValue: router }, { provide: AuthorizationDataService, useValue: authorizationService }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); })); @@ -234,8 +242,8 @@ describe('GroupFormComponent', () => { describe('when submitting the form', () => { beforeEach(() => { spyOn(component.submitForm, 'emit'); - component.groupName.value = groupName; - component.groupDescription.value = groupDescription; + component.groupName.setValue(groupName); + component.groupDescription.setValue(groupDescription); }); describe('without active Group', () => { beforeEach(() => { @@ -243,14 +251,22 @@ describe('GroupFormComponent', () => { fixture.detectChanges(); }); - it('should emit a new group using the correct values', (async () => { - await fixture.whenStable().then(() => { - expect(component.submitForm.emit).toHaveBeenCalledWith(expected); - }); + it('should emit a new group using the correct values', (() => { + expect(component.submitForm.emit).toHaveBeenCalledWith(jasmine.objectContaining({ + name: groupName, + metadata: { + 'dc.description': [ + { + value: groupDescription, + }, + ], + }, + })); })); }); + describe('with active Group', () => { - let expected2; + let expected2: Group; beforeEach(() => { expected2 = Object.assign(new Group(), { name: 'newGroupName', @@ -261,15 +277,24 @@ describe('GroupFormComponent', () => { } ], }, + object: createSuccessfulRemoteDataObject$(undefined), + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected)); spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2)); - component.groupName.value = 'newGroupName'; - component.onSubmit(); - fixture.detectChanges(); + component.ngOnInit(); }); it('should edit with name and description operations', () => { + component.groupName.setValue('newGroupName'); + component.onSubmit(); const operations = [{ op: 'add', path: '/metadata/dc.description', @@ -283,9 +308,8 @@ describe('GroupFormComponent', () => { }); it('should edit with description operations', () => { - component.groupName.value = null; + component.groupName.setValue(null); component.onSubmit(); - fixture.detectChanges(); const operations = [{ op: 'add', path: '/metadata/dc.description', @@ -295,9 +319,9 @@ describe('GroupFormComponent', () => { }); it('should edit with name operations', () => { - component.groupDescription.value = null; + component.groupName.setValue('newGroupName'); + component.groupDescription.setValue(null); component.onSubmit(); - fixture.detectChanges(); const operations = [{ op: 'replace', path: '/name', @@ -306,12 +330,13 @@ describe('GroupFormComponent', () => { expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); }); - it('should emit the existing group using the correct new values', (async () => { - await fixture.whenStable().then(() => { - expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); - }); - })); + it('should emit the existing group using the correct new values', () => { + component.onSubmit(); + expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); + }); + it('should emit success notification', () => { + component.onSubmit(); expect(notificationService.success).toHaveBeenCalled(); }); }); @@ -326,11 +351,8 @@ describe('GroupFormComponent', () => { describe('check form validation', () => { - let groupCommunity; - beforeEach(() => { groupName = 'testName'; - groupCommunity = 'testgroupCommunity'; groupDescription = 'testgroupDescription'; expected = Object.assign(new Group(), { @@ -342,8 +364,17 @@ describe('GroupFormComponent', () => { } ], }, + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); spyOn(component.submitForm, 'emit'); + spyOn(dsoDataServiceStub, 'findByHref').and.returnValue(observableOf(expected)); fixture.detectChanges(); component.initialisePage(); @@ -393,21 +424,20 @@ describe('GroupFormComponent', () => { }); describe('delete', () => { - let deleteButton; - - beforeEach(() => { - component.initialisePage(); + let deleteButton: HTMLButtonElement; + beforeEach(async () => { + spyOn(groupsDataServiceStub, 'delete').and.callThrough(); + component.activeGroup$ = observableOf({ + id: 'active-group', + permanent: false, + } as Group); component.canEdit$ = observableOf(true); - component.groupBeingEdited = { - permanent: false - } as Group; + + component.initialisePage(); fixture.detectChanges(); deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement; - - spyOn(groupsDataServiceStub, 'delete').and.callThrough(); - spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf({ id: 'active-group' })); }); describe('if confirmed via modal', () => { diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts index 3c0547cca5..37ce30473f 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core'; -import { UntypedFormGroup } from '@angular/forms'; +import { UntypedFormGroup, AbstractControl } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { @@ -10,13 +10,10 @@ import { } from '@ng-dynamic-forms/core'; import { TranslateService } from '@ngx-translate/core'; import { - ObservedValueOf, - combineLatest as observableCombineLatest, Observable, - of as observableOf, - Subscription, + Subscription, combineLatest, } from 'rxjs'; -import { catchError, map, switchMap, take, filter, debounceTime } from 'rxjs/operators'; +import { map, switchMap, take, debounceTime, startWith, filter } from 'rxjs/operators'; import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths'; import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths'; import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; @@ -25,21 +22,20 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { RequestService } from '../../../core/data/request.service'; -import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service'; import { Group } from '../../../core/eperson/models/group.model'; import { Collection } from '../../../core/shared/collection.model'; import { Community } from '../../../core/shared/community.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { + getAllCompletedRemoteData, getRemoteDataPayload, getFirstSucceededRemoteData, getFirstCompletedRemoteData, - getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { AlertType } from '../../../shared/alert/aletr-type'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; -import { hasValue, isNotEmpty, hasValueOperator } from '../../../shared/empty.util'; +import { hasValue, isNotEmpty, hasValueOperator, hasNoValue } from '../../../shared/empty.util'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { followLink } from '../../../shared/utils/follow-link-config.model'; @@ -68,9 +64,9 @@ export class GroupFormComponent implements OnInit, OnDestroy { /** * Dynamic models for the inputs of form */ - groupName: DynamicInputModel; - groupCommunity: DynamicInputModel; - groupDescription: DynamicTextAreaModel; + groupName: AbstractControl; + groupCommunity: AbstractControl; + groupDescription: AbstractControl; /** * A list of all dynamic input models @@ -113,21 +109,30 @@ export class GroupFormComponent implements OnInit, OnDestroy { */ subs: Subscription[] = []; - /** - * Group currently being edited - */ - groupBeingEdited: Group; - /** * Observable whether or not the logged in user is allowed to delete the Group & doesn't have a linked object (community / collection linked to workspace group */ canEdit$: Observable; /** - * The AlertType enumeration - * @type {AlertType} + * The current {@link Group} */ - public AlertTypeEnum = AlertType; + activeGroup$: Observable; + + /** + * The current {@link Group}'s linked {@link Community}/{@link Collection} + */ + activeGroupLinkedDSO$: Observable; + + /** + * Link to the current {@link Group}'s {@link Community}/{@link Collection} edit role tab + */ + linkedEditRolesRoute$: Observable; + + /** + * The AlertType enumeration + */ + public readonly AlertType = AlertType; /** * Subscription to email field value change @@ -137,124 +142,110 @@ export class GroupFormComponent implements OnInit, OnDestroy { constructor( public groupDataService: GroupDataService, - private ePersonDataService: EPersonDataService, - private dSpaceObjectDataService: DSpaceObjectDataService, - private formBuilderService: FormBuilderService, - private translateService: TranslateService, - private notificationsService: NotificationsService, - private route: ActivatedRoute, + protected dSpaceObjectDataService: DSpaceObjectDataService, + protected formBuilderService: FormBuilderService, + protected translateService: TranslateService, + protected notificationsService: NotificationsService, + protected route: ActivatedRoute, protected router: Router, - private authorizationService: AuthorizationDataService, - private modalService: NgbModal, + protected authorizationService: AuthorizationDataService, + protected modalService: NgbModal, public requestService: RequestService, protected changeDetectorRef: ChangeDetectorRef, public dsoNameService: DSONameService, ) { } - ngOnInit() { + ngOnInit(): void { + if (this.route.snapshot.params.groupId !== 'newGroup') { + this.setActiveGroup(this.route.snapshot.params.groupId); + } + this.activeGroup$ = this.groupDataService.getActiveGroup(); + this.activeGroupLinkedDSO$ = this.getActiveGroupLinkedDSO(); + this.linkedEditRolesRoute$ = this.getLinkedEditRolesRoute(); + this.canEdit$ = this.activeGroupLinkedDSO$.pipe( + filter((dso: DSpaceObject) => hasNoValue(dso)), + switchMap(() => this.activeGroup$), + hasValueOperator(), + switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self)), + startWith(false), + ); this.initialisePage(); } initialisePage() { - this.subs.push(this.route.params.subscribe((params) => { - if (params.groupId !== 'newGroup') { - this.setActiveGroup(params.groupId); - } - })); - this.canEdit$ = this.groupDataService.getActiveGroup().pipe( - hasValueOperator(), - switchMap((group: Group) => { - return observableCombineLatest( - this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined), - this.hasLinkedDSO(group), - (isAuthorized: ObservedValueOf>, hasLinkedDSO: ObservedValueOf>) => { - return isAuthorized && !hasLinkedDSO; - }); + const groupNameModel = new DynamicInputModel({ + id: 'groupName', + label: this.translateService.instant(`${this.messagePrefix}.groupName`), + name: 'groupName', + validators: { + required: null, + }, + required: true, + }); + const groupCommunityModel = new DynamicInputModel({ + id: 'groupCommunity', + label: this.translateService.instant(`${this.messagePrefix}.groupCommunity`), + name: 'groupCommunity', + required: false, + readOnly: true, + }); + const groupDescriptionModel = new DynamicTextAreaModel({ + id: 'groupDescription', + label: this.translateService.instant(`${this.messagePrefix}.groupDescription`), + name: 'groupDescription', + required: false, + spellCheck: environment.form.spellCheck, + }); + this.formModel = [ + groupNameModel, + groupDescriptionModel, + ]; + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + this.groupName = this.formGroup.get('groupName'); + this.groupDescription = this.formGroup.get('groupDescription'); + + if (hasValue(this.groupName)) { + this.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService)); + this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => { + this.changeDetectorRef.detectChanges(); + }); + } + + this.subs.push( + combineLatest([ + this.activeGroup$, + this.canEdit$, + this.activeGroupLinkedDSO$.pipe(take(1)), + ]).subscribe(([activeGroup, canEdit, linkedObject]) => { + + if (activeGroup != null) { + + // Disable group name exists validator + this.formGroup.controls.groupName.clearAsyncValidators(); + + if (linkedObject?.name) { + this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel); + this.groupDescription = this.formGroup.get('groupCommunity'); + this.formGroup.patchValue({ + groupName: activeGroup.name, + groupCommunity: linkedObject?.name ?? '', + groupDescription: activeGroup.firstMetadataValue('dc.description'), + }); + } else { + this.formGroup.patchValue({ + groupName: activeGroup.name, + groupDescription: activeGroup.firstMetadataValue('dc.description'), + }); + } + setTimeout(() => { + if (!canEdit || activeGroup.permanent) { + this.formGroup.disable(); + } + }, 200); + } }) ); - observableCombineLatest( - this.translateService.get(`${this.messagePrefix}.groupName`), - this.translateService.get(`${this.messagePrefix}.groupCommunity`), - this.translateService.get(`${this.messagePrefix}.groupDescription`) - ).subscribe(([groupName, groupCommunity, groupDescription]) => { - this.groupName = new DynamicInputModel({ - id: 'groupName', - label: groupName, - name: 'groupName', - validators: { - required: null, - }, - required: true, - }); - this.groupCommunity = new DynamicInputModel({ - id: 'groupCommunity', - label: groupCommunity, - name: 'groupCommunity', - required: false, - readOnly: true, - }); - this.groupDescription = new DynamicTextAreaModel({ - id: 'groupDescription', - label: groupDescription, - name: 'groupDescription', - required: false, - spellCheck: environment.form.spellCheck, - }); - this.formModel = [ - this.groupName, - this.groupDescription, - ]; - this.formGroup = this.formBuilderService.createFormGroup(this.formModel); - - if (!!this.formGroup.controls.groupName) { - this.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService)); - this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => { - this.changeDetectorRef.detectChanges(); - }); - } - - this.subs.push( - observableCombineLatest( - this.groupDataService.getActiveGroup(), - this.canEdit$, - this.groupDataService.getActiveGroup() - .pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload()))) - ).subscribe(([activeGroup, canEdit, linkedObject]) => { - - if (activeGroup != null) { - - // Disable group name exists validator - this.formGroup.controls.groupName.clearAsyncValidators(); - - this.groupBeingEdited = activeGroup; - - if (linkedObject?.name) { - this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity); - this.formGroup.patchValue({ - groupName: activeGroup.name, - groupCommunity: linkedObject?.name ?? '', - groupDescription: activeGroup.firstMetadataValue('dc.description'), - }); - } else { - this.formModel = [ - this.groupName, - this.groupDescription, - ]; - this.formGroup.patchValue({ - groupName: activeGroup.name, - groupDescription: activeGroup.firstMetadataValue('dc.description'), - }); - } - setTimeout(() => { - if (!canEdit || activeGroup.permanent) { - this.formGroup.disable(); - } - }, 200); - } - }) - ); - }); } /** @@ -273,25 +264,22 @@ export class GroupFormComponent implements OnInit, OnDestroy { * Emit the updated/created eperson using the EventEmitter submitForm */ onSubmit() { - this.groupDataService.getActiveGroup().pipe(take(1)).subscribe( - (group: Group) => { - const values = { + this.activeGroup$.pipe(take(1)).subscribe((group: Group) => { + if (group === null) { + this.createNewGroup({ name: this.groupName.value, metadata: { 'dc.description': [ { - value: this.groupDescription.value - } - ] + value: this.groupDescription.value, + }, + ], }, - }; - if (group === null) { - this.createNewGroup(values); - } else { - this.editGroup(group); - } + }); + } else { + this.editGroup(group); } - ); + }); } /** @@ -397,7 +385,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { * @param groupSelfLink SelfLink of group to set as active */ setActiveGroupWithLink(groupSelfLink: string) { - this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + this.activeGroup$.pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup === null) { this.groupDataService.cancelEditGroup(); this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object')) @@ -416,7 +404,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { * It'll either show a success or error message depending on whether the delete was successful or not. */ delete() { - this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((group: Group) => { + this.activeGroup$.pipe(take(1)).subscribe((group: Group) => { const modalRef = this.modalService.open(ConfirmationModalComponent); modalRef.componentInstance.dso = group; modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header'; @@ -460,52 +448,37 @@ export class GroupFormComponent implements OnInit, OnDestroy { } /** - * Check if group has a linked object (community or collection linked to a workflow group) - * @param group + * Get the active {@link Group}'s linked object if it has one ({@link Community} or {@link Collection} linked to a + * workflow group) */ - hasLinkedDSO(group: Group): Observable { - if (hasValue(group) && hasValue(group._links.object.href)) { - return this.getLinkedDSO(group).pipe( - map((rd: RemoteData) => { - return hasValue(rd) && hasValue(rd.payload); - }), - catchError(() => observableOf(false)), - ); - } + getActiveGroupLinkedDSO(): Observable { + return this.activeGroup$.pipe( + hasValueOperator(), + switchMap((group: Group) => { + if (group.object === undefined) { + return this.dSpaceObjectDataService.findByHref(group._links.object.href); + } + return group.object; + }), + getAllCompletedRemoteData(), + getRemoteDataPayload(), + ); } /** - * Get group's linked object if it has one (community or collection linked to a workflow group) - * @param group + * Get the route to the edit roles tab of the active {@link Group}'s linked object (community or collection linked + * to a workflow group) if it has one */ - getLinkedDSO(group: Group): Observable> { - if (hasValue(group) && hasValue(group._links.object.href)) { - if (group.object === undefined) { - return this.dSpaceObjectDataService.findByHref(group._links.object.href); - } - return group.object; - } - } - - /** - * Get the route to the edit roles tab of the group's linked object (community or collection linked to a workflow group) if it has one - * @param group - */ - getLinkedEditRolesRoute(group: Group): Observable { - if (hasValue(group) && hasValue(group._links.object.href)) { - return this.getLinkedDSO(group).pipe( - map((rd: RemoteData) => { - if (hasValue(rd) && hasValue(rd.payload)) { - const dso = rd.payload; - switch ((dso as any).type) { - case Community.type.value: - return getCommunityEditRolesRoute(rd.payload.id); - case Collection.type.value: - return getCollectionEditRolesRoute(rd.payload.id); - } - } - }) - ); - } + getLinkedEditRolesRoute(): Observable { + return this.activeGroupLinkedDSO$.pipe( + map((dso: DSpaceObject) => { + switch ((dso as any).type) { + case Community.type.value: + return getCommunityEditRolesRoute(dso.id); + case Collection.type.value: + return getCollectionEditRolesRoute(dso.id); + } + }) + ); } } From b55686e1870cfb5083832239b5b2dbe18160c419 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Tue, 3 Sep 2024 22:32:32 +0200 Subject: [PATCH 008/123] 117287: Removed method calls returning observables from the pagination component --- .../pagination/pagination.component.html | 2 +- .../shared/pagination/pagination.component.ts | 104 +++++++++--------- 2 files changed, 51 insertions(+), 55 deletions(-) diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index 7d90c6a439..d1a820b1da 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -3,7 +3,7 @@
{{ 'pagination.showing.label' | translate }} - {{ 'pagination.showing.detail' | translate:(getShowingDetails(collectionSize)|async)}} + {{ 'pagination.showing.detail' | translate:(showingDetails$ | async)}}
diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts index 6da813cbc7..0e39345595 100644 --- a/src/app/shared/pagination/pagination.component.ts +++ b/src/app/shared/pagination/pagination.component.ts @@ -4,27 +4,33 @@ import { Component, EventEmitter, Input, + OnChanges, OnDestroy, OnInit, Output, - ViewEncapsulation + SimpleChanges, + ViewEncapsulation, } from '@angular/core'; -import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { Observable, of as observableOf, Subscription, switchMap } from 'rxjs'; import { HostWindowService } from '../host-window.service'; -import { HostWindowState } from '../search/host-window.reducer'; import { PaginationComponentOptions } from './pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; -import { hasValue } from '../empty.util'; +import { hasValue, hasValueOperator } from '../empty.util'; import { PageInfo } from '../../core/shared/page-info.model'; import { PaginationService } from '../../core/pagination/pagination.service'; -import { map, take } from 'rxjs/operators'; +import { map, take, startWith } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { ListableObject } from '../object-collection/shared/listable-object.model'; import { ViewMode } from '../../core/shared/view-mode.model'; +interface PaginationDetails { + range: string; + total: number; +} + /** * The default pagination controls component. */ @@ -36,7 +42,7 @@ import { ViewMode } from '../../core/shared/view-mode.model'; changeDetection: ChangeDetectionStrategy.Default, encapsulation: ViewEncapsulation.Emulated }) -export class PaginationComponent implements OnDestroy, OnInit { +export class PaginationComponent implements OnChanges, OnDestroy, OnInit { /** * ViewMode that should be passed to {@link ListableObjectComponentLoaderComponent}. */ @@ -143,11 +149,6 @@ export class PaginationComponent implements OnDestroy, OnInit { */ public currentPageState: number = undefined; - /** - * An observable of HostWindowState type - */ - public hostWindow: Observable; - /** * ID for the pagination instance. This ID is used in the routing to retrieve the pagination options. * This ID needs to be unique between different pagination components when more than one will be displayed on the same page. @@ -186,6 +187,9 @@ export class PaginationComponent implements OnDestroy, OnInit { public sortField$; public defaultSortField = 'name'; + + public showingDetails$: Observable; + /** * Array to track all subscriptions and unsubscribe them onDestroy * @type {Array} @@ -214,6 +218,12 @@ export class PaginationComponent implements OnDestroy, OnInit { this.initializeConfig(); } + ngOnChanges(changes: SimpleChanges): void { + if (changes.collectionSize.currentValue !== changes.collectionSize.previousValue) { + this.showingDetails$ = this.getShowingDetails(this.collectionSize); + } + } + /** * Method provided by Angular. Invoked when the instance is destroyed. */ @@ -251,19 +261,11 @@ export class PaginationComponent implements OnDestroy, OnInit { ); } - /** - * @param cdRef - * ChangeDetectorRef is a singleton service provided by Angular. - * @param route - * Route is a singleton service provided by Angular. - * @param router - * Router is a singleton service provided by Angular. - * @param hostWindowService - * the HostWindowService singleton. - */ - constructor(private cdRef: ChangeDetectorRef, - private paginationService: PaginationService, - public hostWindowService: HostWindowService) { + constructor( + protected cdRef: ChangeDetectorRef, + protected paginationService: PaginationService, + public hostWindowService: HostWindowService, + ) { } /** @@ -299,17 +301,6 @@ export class PaginationComponent implements OnDestroy, OnInit { this.emitPaginationChange(); } - /** - * Method to change the route to the given sort field - * - * @param sortField - * The sort field being navigated to. - */ - public doSortFieldChange(field: string) { - this.updateParams({ pageId: this.id, page: 1, sortField: field }); - this.emitPaginationChange(); - } - /** * Method to emit a general pagination change event */ @@ -328,26 +319,31 @@ export class PaginationComponent implements OnDestroy, OnInit { /** * Method to get pagination details of the current viewed page. */ - public getShowingDetails(collectionSize: number): Observable { - let showingDetails = observableOf({ range: null + ' - ' + null, total: null }); - if (collectionSize) { - showingDetails = this.paginationService.getCurrentPagination(this.id, this.paginationOptions).pipe( - map((currentPaginationOptions) => { - let firstItem; - let lastItem; - const pageMax = currentPaginationOptions.pageSize * currentPaginationOptions.currentPage; + public getShowingDetails(collectionSize: number): Observable { + return observableOf(collectionSize).pipe( + hasValueOperator(), + switchMap(() => this.paginationService.getCurrentPagination(this.id, this.paginationOptions)), + map((currentPaginationOptions) => { + let firstItem: number; + let lastItem: number; + const pageMax = currentPaginationOptions.pageSize * currentPaginationOptions.currentPage; - firstItem = currentPaginationOptions.pageSize * (currentPaginationOptions.currentPage - 1) + 1; - if (collectionSize > pageMax) { - lastItem = pageMax; - } else { - lastItem = collectionSize; - } - return {range: firstItem + ' - ' + lastItem, total: collectionSize}; - }) - ); - } - return showingDetails; + firstItem = currentPaginationOptions.pageSize * (currentPaginationOptions.currentPage - 1) + 1; + if (collectionSize > pageMax) { + lastItem = pageMax; + } else { + lastItem = collectionSize; + } + return { + range: `${firstItem} - ${lastItem}`, + total: collectionSize, + }; + }), + startWith({ + range: `${null} - ${null}`, + total: null, + }), + ); } /** From c74c178533d8ffd2c4cca926896b703e10af00b8 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Thu, 5 Sep 2024 10:50:13 +0200 Subject: [PATCH 009/123] 117287: Removed method calls returning observables from the metadata registry --- .../metadata-registry.component.html | 4 +- .../metadata-registry.component.spec.ts | 42 ++--- .../metadata-registry.component.ts | 128 +++++++--------- .../metadata-schema-form.component.html | 2 +- .../metadata-schema-form.component.spec.ts | 37 ++--- .../metadata-schema-form.component.ts | 145 +++++++++--------- .../shared/testing/registry.service.stub.ts | 107 +++++++++++++ 7 files changed, 268 insertions(+), 197 deletions(-) create mode 100644 src/app/shared/testing/registry.service.stub.ts diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html index 35bffad185..0f9df119c7 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html @@ -27,11 +27,11 @@ + [ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}"> diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts index 944288a7a5..d69d14aacf 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts @@ -1,7 +1,6 @@ import { MetadataRegistryComponent } from './metadata-registry.component'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { of as observableOf } from 'rxjs'; -import { buildPaginatedList } from '../../../core/data/paginated-list.model'; import { TranslateModule } from '@ngx-translate/core'; import { By } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; @@ -15,18 +14,21 @@ import { HostWindowService } from '../../../shared/host-window.service'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { RestResponse } from '../../../core/cache/response.models'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; +import { RegistryServiceStub } from '../../../shared/testing/registry.service.stub'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; describe('MetadataRegistryComponent', () => { let comp: MetadataRegistryComponent; let fixture: ComponentFixture; - let registryService: RegistryService; - let paginationService; - const mockSchemasList = [ + + let paginationService: PaginationServiceStub; + let registryService: RegistryServiceStub; + + const mockSchemasList: MetadataSchema[] = [ { id: 1, _links: { @@ -47,32 +49,18 @@ describe('MetadataRegistryComponent', () => { prefix: 'mock', namespace: 'http://dspace.org/mockschema' } - ]; - const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList)); - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getMetadataSchemas: () => mockSchemas, - getActiveMetadataSchema: () => observableOf(undefined), - getSelectedMetadataSchemas: () => observableOf([]), - editMetadataSchema: (schema) => { - }, - cancelEditMetadataSchema: () => { - }, - deleteMetadataSchema: () => observableOf(new RestResponse(true, 200, 'OK')), - deselectAllMetadataSchema: () => { - }, - clearMetadataSchemaRequests: () => observableOf(undefined) - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ - - paginationService = new PaginationServiceStub(); + ] as MetadataSchema[]; beforeEach(waitForAsync(() => { + paginationService = new PaginationServiceStub(); + registryService = new RegistryServiceStub(); + spyOn(registryService, 'getMetadataSchemas').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList))); + TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], declarations: [MetadataRegistryComponent, PaginationComponent, EnumKeysPipe], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, + { provide: RegistryService, useValue: registryService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: PaginationService, useValue: paginationService }, { provide: NotificationsService, useValue: new NotificationsServiceStub() } @@ -123,7 +111,7 @@ describe('MetadataRegistryComponent', () => { })); it('should cancel editing the selected schema when clicked again', waitForAsync(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0] as MetadataSchema)); + comp.activeMetadataSchema$ = observableOf(mockSchemasList[0] as MetadataSchema); spyOn(registryService, 'cancelEditMetadataSchema'); row.click(); fixture.detectChanges(); @@ -138,7 +126,7 @@ describe('MetadataRegistryComponent', () => { beforeEach(() => { spyOn(registryService, 'deleteMetadataSchema').and.callThrough(); - spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas as MetadataSchema[])); + comp.selectedMetadataSchemaIDs$ = observableOf(selectedSchemas.map((selectedSchema: MetadataSchema) => selectedSchema.id)); comp.deleteSchemas(); fixture.detectChanges(); }); diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts index 857034604e..dbeb2b4f52 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts @@ -1,13 +1,11 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { RegistryService } from '../../../core/registry/registry.service'; -import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, zip } from 'rxjs'; +import { BehaviorSubject, Observable, zip, Subscription } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { filter, map, switchMap, take } from 'rxjs/operators'; -import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; @@ -24,13 +22,23 @@ import { PaginationService } from '../../../core/pagination/pagination.service'; * A component used for managing all existing metadata schemas within the repository. * The admin can create, edit or delete metadata schemas here. */ -export class MetadataRegistryComponent { +export class MetadataRegistryComponent implements OnDestroy, OnInit { /** * A list of all the current metadata schemas within the repository */ metadataSchemas: Observable>>; + /** + * The {@link MetadataSchema}that is being edited + */ + activeMetadataSchema$: Observable; + + /** + * The selected {@link MetadataSchema} IDs + */ + selectedMetadataSchemaIDs$: Observable; + /** * Pagination config used to display the list of metadata schemas */ @@ -40,15 +48,25 @@ export class MetadataRegistryComponent { }); /** - * Whether or not the list of MetadataSchemas needs an update + * Whether the list of MetadataSchemas needs an update */ needsUpdate$: BehaviorSubject = new BehaviorSubject(true); - constructor(private registryService: RegistryService, - private notificationsService: NotificationsService, - private router: Router, - private paginationService: PaginationService, - private translateService: TranslateService) { + subscriptions: Subscription[] = []; + + constructor( + protected registryService: RegistryService, + protected notificationsService: NotificationsService, + protected paginationService: PaginationService, + protected translateService: TranslateService, + ) { + } + + ngOnInit(): void { + this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema(); + this.selectedMetadataSchemaIDs$ = this.registryService.getSelectedMetadataSchemas().pipe( + map((schemas: MetadataSchema[]) => schemas.map((schema: MetadataSchema) => schema.id)), + ); this.updateSchemas(); } @@ -77,30 +95,13 @@ export class MetadataRegistryComponent { * @param schema */ editSchema(schema: MetadataSchema) { - this.getActiveSchema().pipe(take(1)).subscribe((activeSchema) => { + this.subscriptions.push(this.activeMetadataSchema$.pipe(take(1)).subscribe((activeSchema: MetadataSchema) => { if (schema === activeSchema) { this.registryService.cancelEditMetadataSchema(); } else { this.registryService.editMetadataSchema(schema); } - }); - } - - /** - * Checks whether the given metadata schema is active (being edited) - * @param schema - */ - isActive(schema: MetadataSchema): Observable { - return this.getActiveSchema().pipe( - map((activeSchema) => schema === activeSchema) - ); - } - - /** - * Gets the active metadata schema (being edited) - */ - getActiveSchema(): Observable { - return this.registryService.getActiveMetadataSchema(); + })); } /** @@ -114,42 +115,25 @@ export class MetadataRegistryComponent { this.registryService.deselectMetadataSchema(schema); } - /** - * Checks whether a given metadata schema is selected in the list (checkbox) - * @param schema - */ - isSelected(schema: MetadataSchema): Observable { - return this.registryService.getSelectedMetadataSchemas().pipe( - map((schemas) => schemas.find((selectedSchema) => selectedSchema === schema) != null) - ); - } - /** * Delete all the selected metadata schemas */ deleteSchemas() { - this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe( - (schemas) => { - const tasks$ = []; - for (const schema of schemas) { - if (hasValue(schema.id)) { - tasks$.push(this.registryService.deleteMetadataSchema(schema.id).pipe(getFirstCompletedRemoteData())); - } - } - zip(...tasks$).subscribe((responses: RemoteData[]) => { - const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); - const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); - if (successResponses.length > 0) { - this.showNotification(true, successResponses.length); - } - if (failedResponses.length > 0) { - this.showNotification(false, failedResponses.length); - } - this.registryService.deselectAllMetadataSchema(); - this.registryService.cancelEditMetadataSchema(); - }); + this.subscriptions.push(this.selectedMetadataSchemaIDs$.pipe( + take(1), + switchMap((schemaIDs: number[]) => zip(schemaIDs.map((schemaID: number) => this.registryService.deleteMetadataSchema(schemaID).pipe(getFirstCompletedRemoteData())))), + ).subscribe((responses: RemoteData[]) => { + const successResponses: RemoteData[] = responses.filter((response: RemoteData) => response.hasSucceeded); + const failedResponses: RemoteData[] = responses.filter((response: RemoteData) => response.hasFailed); + if (successResponses.length > 0) { + this.showNotification(true, successResponses.length); } - ); + if (failedResponses.length > 0) { + this.showNotification(false, failedResponses.length); + } + this.registryService.deselectAllMetadataSchema(); + this.registryService.cancelEditMetadataSchema(); + })); } /** @@ -160,20 +144,20 @@ export class MetadataRegistryComponent { showNotification(success: boolean, amount: number) { const prefix = 'admin.registries.schema.notification'; const suffix = success ? 'success' : 'failure'; - const messages = observableCombineLatest( - this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`), - this.translateService.get(`${prefix}.deleted.${suffix}`, {amount: amount}) - ); - messages.subscribe(([head, content]) => { - if (success) { - this.notificationsService.success(head, content); - } else { - this.notificationsService.error(head, content); - } - }); + + const head: string = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`); + const content: string = this.translateService.instant(`${prefix}.deleted.${suffix}`, {amount: amount}); + + if (success) { + this.notificationsService.success(head, content); + } else { + this.notificationsService.error(head, content); + } } + ngOnDestroy(): void { this.paginationService.clearPagination(this.config.id); + this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe()); } } diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html index a4a4613565..521fb06a6e 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html @@ -1,4 +1,4 @@ -
+

{{messagePrefix + '.create' | translate}}

diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts index b758767ddb..442a06dc67 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { MetadataSchemaFormComponent } from './metadata-schema-form.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; @@ -10,41 +10,26 @@ import { RegistryService } from '../../../../core/registry/registry.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { of as observableOf } from 'rxjs'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; +import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub'; +import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; describe('MetadataSchemaFormComponent', () => { let component: MetadataSchemaFormComponent; let fixture: ComponentFixture; - let registryService: RegistryService; - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getActiveMetadataSchema: () => observableOf(undefined), - createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema), - cancelEditMetadataSchema: () => { - }, - clearMetadataSchemaRequests: () => observableOf(undefined) - }; - const formBuilderServiceStub = { - createFormGroup: () => { - return { - patchValue: () => { - }, - reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { - }, - }; - } - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ + let registryService: RegistryServiceStub; beforeEach(waitForAsync(() => { + registryService = new RegistryServiceStub(); + return TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], declarations: [MetadataSchemaFormComponent, EnumKeysPipe], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, - { provide: FormBuilderService, useValue: formBuilderServiceStub } + { provide: RegistryService, useValue: registryService }, + { provide: FormBuilderService, useValue: getMockFormBuilderService() } ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); })); @@ -75,7 +60,7 @@ describe('MetadataSchemaFormComponent', () => { describe('without an active schema', () => { beforeEach(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(undefined)); + component.activeMetadataSchema$ = observableOf(undefined); component.onSubmit(); fixture.detectChanges(); }); @@ -94,7 +79,7 @@ describe('MetadataSchemaFormComponent', () => { } as MetadataSchema); beforeEach(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId)); + component.activeMetadataSchema$ = observableOf(expectedWithId); component.onSubmit(); fixture.detectChanges(); }); diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts index 1992289ff7..78680f6d9a 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts @@ -8,10 +8,10 @@ import { import { UntypedFormGroup } from '@angular/forms'; import { RegistryService } from '../../../../core/registry/registry.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; -import { take } from 'rxjs/operators'; +import { take, switchMap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; -import { combineLatest } from 'rxjs'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; +import { Subscription, Observable } from 'rxjs'; @Component({ selector: 'ds-metadata-schema-form', @@ -73,64 +73,71 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy { */ @Output() submitForm: EventEmitter = new EventEmitter(); - constructor(public registryService: RegistryService, private formBuilderService: FormBuilderService, private translateService: TranslateService) { + /** + * The {@link MetadataSchema} that is currently being edited + */ + activeMetadataSchema$: Observable; + + subscriptions: Subscription[] = []; + + constructor( + protected registryService: RegistryService, + protected formBuilderService: FormBuilderService, + protected translateService: TranslateService, + ) { } ngOnInit() { - combineLatest([ - this.translateService.get(`${this.messagePrefix}.name`), - this.translateService.get(`${this.messagePrefix}.namespace`) - ]).subscribe(([name, namespace]) => { - this.name = new DynamicInputModel({ - id: 'name', - label: name, - name: 'name', - validators: { - required: null, - pattern: '^[^. ,]*$', - maxLength: 32, - }, - required: true, - errorMessages: { - pattern: 'error.validation.metadata.name.invalid-pattern', - maxLength: 'error.validation.metadata.name.max-length', - }, - }); - this.namespace = new DynamicInputModel({ - id: 'namespace', - label: namespace, - name: 'namespace', - validators: { - required: null, - maxLength: 256, - }, - required: true, - errorMessages: { - maxLength: 'error.validation.metadata.namespace.max-length', - }, - }); - this.formModel = [ - new DynamicFormGroupModel( - { - id: 'metadatadataschemagroup', - group:[this.namespace, this.name] - }) - ]; - this.formGroup = this.formBuilderService.createFormGroup(this.formModel); - this.registryService.getActiveMetadataSchema().subscribe((schema: MetadataSchema) => { - if (schema == null) { - this.clearFields(); - } else { - this.formGroup.patchValue({ - metadatadataschemagroup: { - name: schema.prefix, - namespace: schema.namespace, - }, - }); - this.name.disabled = true; - } + this.name = new DynamicInputModel({ + id: 'name', + label: this.translateService.instant(`${this.messagePrefix}.name`), + name: 'name', + validators: { + required: null, + pattern: '^[^. ,]*$', + maxLength: 32, + }, + required: true, + errorMessages: { + pattern: 'error.validation.metadata.name.invalid-pattern', + maxLength: 'error.validation.metadata.name.max-length', + }, }); - }); + this.namespace = new DynamicInputModel({ + id: 'namespace', + label: this.translateService.instant(`${this.messagePrefix}.namespace`), + name: 'namespace', + validators: { + required: null, + maxLength: 256, + }, + required: true, + errorMessages: { + maxLength: 'error.validation.metadata.namespace.max-length', + }, + }); + this.formModel = [ + new DynamicFormGroupModel( + { + id: 'metadatadataschemagroup', + group:[this.namespace, this.name] + }) + ]; + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema(); + this.subscriptions.push(this.activeMetadataSchema$.subscribe((schema: MetadataSchema) => { + if (schema == null) { + this.clearFields(); + } else { + this.formGroup.patchValue({ + metadatadataschemagroup: { + name: schema.prefix, + namespace: schema.namespace, + }, + }); + this.name.disabled = true; + } + })); } /** @@ -148,29 +155,28 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy { */ onSubmit(): void { this.registryService.clearMetadataSchemaRequests().subscribe(); - this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe( - (schema: MetadataSchema) => { + this.subscriptions.push(this.activeMetadataSchema$.pipe( + take(1), + switchMap((schema: MetadataSchema) => { const values = { prefix: this.name.value, namespace: this.namespace.value }; if (schema == null) { - this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), values)).subscribe((newSchema) => { - this.submitForm.emit(newSchema); - }); + return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), values)); } else { - this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, { + return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, { id: schema.id, prefix: schema.prefix, namespace: values.namespace, - })).subscribe((updatedSchema: MetadataSchema) => { - this.submitForm.emit(updatedSchema); - }); + })); } - this.clearFields(); - this.registryService.cancelEditMetadataSchema(); - } - ); + }), + ).subscribe((schema: MetadataSchema) => { + this.clearFields(); + this.registryService.cancelEditMetadataSchema(); + this.submitForm.emit(schema); + })); } /** @@ -186,5 +192,6 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.onCancel(); + this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); } } diff --git a/src/app/shared/testing/registry.service.stub.ts b/src/app/shared/testing/registry.service.stub.ts new file mode 100644 index 0000000000..de5e4f933f --- /dev/null +++ b/src/app/shared/testing/registry.service.stub.ts @@ -0,0 +1,107 @@ +/* eslint-disable no-empty,@typescript-eslint/no-empty-function */ +import { FindListOptions } from '../../core/data/find-list-options.model'; +import { FollowLinkConfig } from '../utils/follow-link-config.model'; +import { MetadataSchema } from '../../core/metadata/metadata-schema.model'; +import { Observable, of as observableOf } from 'rxjs'; +import { RemoteData } from '../../core/data/remote-data'; +import { PaginatedList } from '../../core/data/paginated-list.model'; +import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; +import { createPaginatedList } from './utils.test'; +import { MetadataField } from '../../core/metadata/metadata-field.model'; +import { NoContent } from '../../core/shared/NoContent.model'; + +/** + * Stub class of {@link RegistryService} + */ +export class RegistryServiceStub { + + getMetadataSchemas(_options: FindListOptions = {}, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig[]): Observable>> { + return createSuccessfulRemoteDataObject$(createPaginatedList()); + } + + getMetadataSchemaByPrefix(_prefix: string, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig[]): Observable> { + return createSuccessfulRemoteDataObject$(undefined); + } + + getMetadataFieldsBySchema(_schema: MetadataSchema, _options: FindListOptions = {}, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig[]): Observable>> { + return createSuccessfulRemoteDataObject$(createPaginatedList()); + } + + editMetadataSchema(_schema: MetadataSchema): void { + } + + cancelEditMetadataSchema(): void { + } + + getActiveMetadataSchema(): Observable { + return observableOf(undefined); + } + + selectMetadataSchema(_schema: MetadataSchema): void { + } + + deselectMetadataSchema(_schema: MetadataSchema): void { + } + + deselectAllMetadataSchema(): void { + } + + getSelectedMetadataSchemas(): Observable { + return observableOf([]); + } + + editMetadataField(_field: MetadataField): void { + } + + cancelEditMetadataField(): void { + } + + getActiveMetadataField(): Observable { + return observableOf(undefined); + } + + selectMetadataField(_field: MetadataField): void { + } + + deselectMetadataField(_field: MetadataField): void { + } + + deselectAllMetadataField(): void { + } + + getSelectedMetadataFields(): Observable { + return observableOf([]); + } + + createOrUpdateMetadataSchema(schema: MetadataSchema): Observable { + return observableOf(schema); + } + + deleteMetadataSchema(_id: number): Observable> { + return createSuccessfulRemoteDataObject$(undefined); + } + + clearMetadataSchemaRequests(): Observable { + return observableOf(''); + } + + createMetadataField(_field: MetadataField, _schema: MetadataSchema): Observable { + return observableOf(undefined); + } + + updateMetadataField(_field: MetadataField): Observable { + return observableOf(undefined); + } + + deleteMetadataField(_id: number): Observable> { + return createSuccessfulRemoteDataObject$(undefined); + } + + clearMetadataFieldRequests(): void { + } + + queryMetadataFields(_query: string, _options: FindListOptions = {}, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig[]): Observable>> { + return createSuccessfulRemoteDataObject$(createPaginatedList()); + } + +} From f03ed89687e12fa849e73b336e95fb2ab2996033 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Thu, 5 Sep 2024 11:49:18 +0200 Subject: [PATCH 010/123] 117287: Removed method calls returning observables from the metadata schema registry --- .../metadata-field-form.component.spec.ts | 38 ++--- .../metadata-schema.component.html | 4 +- .../metadata-schema.component.spec.ts | 65 ++++---- .../metadata-schema.component.ts | 139 +++++++----------- .../shared/testing/registry.service.stub.ts | 8 +- 5 files changed, 95 insertions(+), 159 deletions(-) diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts index ad7b54945d..7ae672f9e1 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts @@ -9,14 +9,17 @@ import { TranslateModule } from '@ngx-translate/core'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { MetadataField } from '../../../../core/metadata/metadata-field.model'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; +import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; +import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub'; describe('MetadataFieldFormComponent', () => { let component: MetadataFieldFormComponent; let fixture: ComponentFixture; - let registryService: RegistryService; + + let registryService: RegistryServiceStub; const metadataSchema = Object.assign(new MetadataSchema(), { id: 1, @@ -24,38 +27,17 @@ describe('MetadataFieldFormComponent', () => { prefix: 'fake' }); - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getActiveMetadataField: () => observableOf(undefined), - createMetadataField: (field: MetadataField) => observableOf(field), - updateMetadataField: (field: MetadataField) => observableOf(field), - cancelEditMetadataField: () => { - }, - cancelEditMetadataSchema: () => { - }, - clearMetadataFieldRequests: () => observableOf(undefined) - }; - const formBuilderServiceStub = { - createFormGroup: () => { - return { - patchValue: () => { - }, - reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { - }, - }; - } - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ - beforeEach(waitForAsync(() => { + registryService = new RegistryServiceStub(); + return TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], declarations: [MetadataFieldFormComponent, EnumKeysPipe], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, - { provide: FormBuilderService, useValue: formBuilderServiceStub } + { provide: RegistryService, useValue: registryService }, + { provide: FormBuilderService, useValue: getMockFormBuilderService() } ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); })); diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html index 557741df80..6d895d187b 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html +++ b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html @@ -32,11 +32,11 @@ + [ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}"> diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts index 2b660a6363..ce4987a02c 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts @@ -4,7 +4,7 @@ import { of as observableOf } from 'rxjs'; import { buildPaginatedList } from '../../../core/data/paginated-list.model'; import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { By } from '@angular/platform-browser'; import { RegistryService } from '../../../core/registry/registry.service'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; @@ -12,25 +12,28 @@ import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub'; import { HostWindowService } from '../../../shared/host-window.service'; -import { RouterStub } from '../../../shared/testing/router.stub'; import { RouterTestingModule } from '@angular/router/testing'; import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { RestResponse } from '../../../core/cache/response.models'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { VarDirective } from '../../../shared/utils/var.directive'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; +import { RegistryServiceStub } from '../../../shared/testing/registry.service.stub'; describe('MetadataSchemaComponent', () => { let comp: MetadataSchemaComponent; let fixture: ComponentFixture; - let registryService: RegistryService; - const mockSchemasList = [ + + let registryService: RegistryServiceStub; + let activatedRoute: ActivatedRouteStub; + let paginationService: PaginationServiceStub; + + const mockSchemasList: MetadataSchema[] = [ { id: 1, _links: { @@ -51,8 +54,8 @@ describe('MetadataSchemaComponent', () => { prefix: 'mock', namespace: 'http://dspace.org/mockschema' } - ]; - const mockFieldsList = [ + ] as MetadataSchema[]; + const mockFieldsList: MetadataField[] = [ { id: 1, _links: { @@ -101,47 +104,29 @@ describe('MetadataSchemaComponent', () => { scopeNote: null, schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]) } - ]; - const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList)); - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getMetadataSchemas: () => mockSchemas, - getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))), - getMetadataSchemaByPrefix: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]), - getActiveMetadataField: () => observableOf(undefined), - getSelectedMetadataFields: () => observableOf([]), - editMetadataField: (schema) => { - }, - cancelEditMetadataField: () => { - }, - deleteMetadataField: () => observableOf(new RestResponse(true, 200, 'OK')), - deselectAllMetadataField: () => { - }, - clearMetadataFieldRequests: () => observableOf(undefined) - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ + ] as MetadataField[]; const schemaNameParam = 'mock'; - const activatedRouteStub = Object.assign(new ActivatedRouteStub(), { - params: observableOf({ - schemaName: schemaNameParam - }) - }); - - const paginationService = new PaginationServiceStub(); beforeEach(waitForAsync(() => { + activatedRoute = new ActivatedRouteStub({ + schemaName: schemaNameParam, + }); + paginationService = new PaginationServiceStub(); + registryService = new RegistryServiceStub(); + spyOn(registryService, 'getMetadataFieldsBySchema').and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4)))); + spyOn(registryService, 'getMetadataSchemaByPrefix').and.callFake((schemaName) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0])); + TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe, VarDirective], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, - { provide: ActivatedRoute, useValue: activatedRouteStub }, + { provide: RegistryService, useValue: registryService }, + { provide: ActivatedRoute, useValue: activatedRoute }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, - { provide: Router, useValue: new RouterStub() }, { provide: PaginationService, useValue: paginationService }, { provide: NotificationsService, useValue: new NotificationsServiceStub() } ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); })); @@ -190,7 +175,7 @@ describe('MetadataSchemaComponent', () => { })); it('should cancel editing the selected field when clicked again', waitForAsync(() => { - spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2] as MetadataField)); + comp.activeField$ = observableOf(mockFieldsList[2] as MetadataField); spyOn(registryService, 'cancelEditMetadataField'); row.click(); fixture.detectChanges(); @@ -205,7 +190,7 @@ describe('MetadataSchemaComponent', () => { beforeEach(() => { spyOn(registryService, 'deleteMetadataField').and.callThrough(); - spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields as MetadataField[])); + comp.selectedMetadataFieldIDs$ = observableOf(selectedFields.map((metadataField: MetadataField) => metadataField.id)); comp.deleteFields(); fixture.detectChanges(); }); diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts index d0827e6e4d..1a5fd15ccd 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts @@ -1,19 +1,18 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { RegistryService } from '../../../core/registry/registry.service'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject, - combineLatest as observableCombineLatest, combineLatest, Observable, of as observableOf, - zip + zip, + Subscription, } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { map, switchMap, take } from 'rxjs/operators'; -import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { MetadataField } from '../../../core/metadata/metadata-field.model'; @@ -32,7 +31,7 @@ import { PaginationService } from '../../../core/pagination/pagination.service'; * A component used for managing all existing metadata fields within the current metadata schema. * The admin can create, edit or delete metadata fields here. */ -export class MetadataSchemaComponent implements OnInit { +export class MetadataSchemaComponent implements OnDestroy, OnInit { /** * The metadata schema */ @@ -57,27 +56,33 @@ export class MetadataSchemaComponent implements OnInit { */ needsUpdate$: BehaviorSubject = new BehaviorSubject(true); - constructor(private registryService: RegistryService, - private route: ActivatedRoute, - private notificationsService: NotificationsService, - private router: Router, - private paginationService: PaginationService, - private translateService: TranslateService) { + /** + * The current {@link MetadataField} that is being edited + */ + activeField$: Observable; + /** + * The selected {@link MetadataField} IDs + */ + selectedMetadataFieldIDs$: Observable; + + subscriptions: Subscription[] = []; + + constructor( + protected registryService: RegistryService, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected paginationService: PaginationService, + protected translateService: TranslateService, + ) { } ngOnInit(): void { - this.route.params.subscribe((params) => { - this.initialize(params); - }); - } - - /** - * Initialize the component using the params within the url (schemaName) - * @param params - */ - initialize(params) { - this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(params.schemaName).pipe(getFirstSucceededRemoteDataPayload()); + this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(this.route.snapshot.params.schemaName).pipe(getFirstSucceededRemoteDataPayload()); + this.activeField$ = this.registryService.getActiveMetadataField(); + this.selectedMetadataFieldIDs$ = this.registryService.getSelectedMetadataFields().pipe( + map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => metadataField.id)), + ); this.updateFields(); } @@ -86,7 +91,7 @@ export class MetadataSchemaComponent implements OnInit { */ private updateFields() { this.metadataFields$ = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( - switchMap((currentPagination) => combineLatest(this.metadataSchema$, this.needsUpdate$, observableOf(currentPagination))), + switchMap((currentPagination) => combineLatest([this.metadataSchema$, this.needsUpdate$, observableOf(currentPagination)])), switchMap(([schema, update, currentPagination]: [MetadataSchema, boolean, PaginationComponentOptions]) => { if (update) { this.needsUpdate$.next(false); @@ -110,30 +115,13 @@ export class MetadataSchemaComponent implements OnInit { * @param field */ editField(field: MetadataField) { - this.getActiveField().pipe(take(1)).subscribe((activeField) => { + this.subscriptions.push(this.activeField$.pipe(take(1)).subscribe((activeField) => { if (field === activeField) { this.registryService.cancelEditMetadataField(); } else { this.registryService.editMetadataField(field); } - }); - } - - /** - * Checks whether the given metadata field is active (being edited) - * @param field - */ - isActive(field: MetadataField): Observable { - return this.getActiveField().pipe( - map((activeField) => field === activeField) - ); - } - - /** - * Gets the active metadata field (being edited) - */ - getActiveField(): Observable { - return this.registryService.getActiveMetadataField(); + })); } /** @@ -147,42 +135,25 @@ export class MetadataSchemaComponent implements OnInit { this.registryService.deselectMetadataField(field); } - /** - * Checks whether a given metadata field is selected in the list (checkbox) - * @param field - */ - isSelected(field: MetadataField): Observable { - return this.registryService.getSelectedMetadataFields().pipe( - map((fields) => fields.find((selectedField) => selectedField === field) != null) - ); - } - /** * Delete all the selected metadata fields */ deleteFields() { - this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe( - (fields) => { - const tasks$ = []; - for (const field of fields) { - if (hasValue(field.id)) { - tasks$.push(this.registryService.deleteMetadataField(field.id).pipe(getFirstCompletedRemoteData())); - } - } - zip(...tasks$).subscribe((responses: RemoteData[]) => { - const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); - const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); - if (successResponses.length > 0) { - this.showNotification(true, successResponses.length); - } - if (failedResponses.length > 0) { - this.showNotification(false, failedResponses.length); - } - this.registryService.deselectAllMetadataField(); - this.registryService.cancelEditMetadataField(); - }); + this.subscriptions.push(this.selectedMetadataFieldIDs$.pipe( + take(1), + switchMap((fieldIDs) => zip(fieldIDs.map((fieldID) => this.registryService.deleteMetadataField(fieldID).pipe(getFirstCompletedRemoteData())))), + ).subscribe((responses: RemoteData[]) => { + const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); + const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); + if (successResponses.length > 0) { + this.showNotification(true, successResponses.length); } - ); + if (failedResponses.length > 0) { + this.showNotification(false, failedResponses.length); + } + this.registryService.deselectAllMetadataField(); + this.registryService.cancelEditMetadataField(); + })); } /** @@ -193,20 +164,18 @@ export class MetadataSchemaComponent implements OnInit { showNotification(success: boolean, amount: number) { const prefix = 'admin.registries.schema.notification'; const suffix = success ? 'success' : 'failure'; - const messages = observableCombineLatest( - this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`), - this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount }) - ); - messages.subscribe(([head, content]) => { - if (success) { - this.notificationsService.success(head, content); - } else { - this.notificationsService.error(head, content); - } - }); + const head = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`); + const content = this.translateService.instant(`${prefix}.field.deleted.${suffix}`, { amount: amount }); + if (success) { + this.notificationsService.success(head, content); + } else { + this.notificationsService.error(head, content); + } } + ngOnDestroy(): void { this.paginationService.clearPagination(this.config.id); + this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); } } diff --git a/src/app/shared/testing/registry.service.stub.ts b/src/app/shared/testing/registry.service.stub.ts index de5e4f933f..b52f32af4e 100644 --- a/src/app/shared/testing/registry.service.stub.ts +++ b/src/app/shared/testing/registry.service.stub.ts @@ -85,12 +85,12 @@ export class RegistryServiceStub { return observableOf(''); } - createMetadataField(_field: MetadataField, _schema: MetadataSchema): Observable { - return observableOf(undefined); + createMetadataField(field: MetadataField, _schema: MetadataSchema): Observable { + return observableOf(field); } - updateMetadataField(_field: MetadataField): Observable { - return observableOf(undefined); + updateMetadataField(field: MetadataField): Observable { + return observableOf(field); } deleteMetadataField(_id: number): Observable> { From cf54af2c22a8344d67573d7185401ade04cf5588 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 3 Sep 2024 13:43:02 +0200 Subject: [PATCH 011/123] 117803: Refactor Item Edit Bitstreams page to use HTML Table elements --- .../item-bitstreams.component.html | 11 +- .../item-bitstreams.component.scss | 20 +- .../item-edit-bitstream-bundle.component.html | 109 +++++++-- .../item-edit-bitstream-bundle.component.scss | 20 ++ .../item-edit-bitstream-bundle.component.ts | 216 +++++++++++++++++- ...-drag-and-drop-bitstream-list.component.ts | 4 + src/assets/i18n/en.json5 | 2 + 7 files changed, 336 insertions(+), 46 deletions(-) create mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index 4cb9577fcb..70f6ca55e8 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -23,16 +23,7 @@
-
-
-
- - {{'item.edit.bitstreams.headers.name' | translate}} -
-
{{'item.edit.bitstreams.headers.description' | translate}}
-
{{'item.edit.bitstreams.headers.format' | translate}}
-
{{'item.edit.bitstreams.headers.actions' | translate}}
-
+
-
-
- -
- {{'item.edit.bitstreams.bundle.name' | translate:{ name: dsoNameService.getName(bundle) } }} -
-
-
-
- -
-
-
- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{'item.edit.bitstreams.headers.name' | translate}} + + {{'item.edit.bitstreams.headers.description' | translate}} + + {{'item.edit.bitstreams.headers.format' | translate}} + + {{'item.edit.bitstreams.headers.actions' | translate}} +
+ {{'item.edit.bitstreams.bundle.name' | translate:{ name: bundleName } }} + + +
+ {{ entry.name }} + + {{ entry.description }} + + {{ (entry.format | async)?.shortDescription }} + +
+
+ + + + + + +
+
+
+ +
+
+ diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss new file mode 100644 index 0000000000..d344b1657a --- /dev/null +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss @@ -0,0 +1,20 @@ +.header-row { + color: var(--bs-table-dark-color); + background-color: var(--bs-table-dark-bg); + border-color: var(--bs-table-dark-border-color); +} + +.bundle-row { + color: var(--bs-table-head-color); + background-color: var(--bs-table-head-bg); + border-color: var(--bs-table-border-color); +} + +.row-element { + padding: 0.75em; + border-bottom: var(--bs-table-border-width) solid var(--bs-table-border-color); +} + +.bitstream-name { + font-weight: normal; +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 70f4b63217..ad009e7cb7 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -5,10 +5,65 @@ import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; import { getItemPageRoute } from '../../../item-page-routing-paths'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { RemoteData } from 'src/app/core/data/remote-data'; +import { PaginatedList } from 'src/app/core/data/paginated-list.model'; +import { Bitstream } from 'src/app/core/shared/bitstream.model'; +import { Observable, BehaviorSubject, switchMap } from 'rxjs'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { FieldUpdates } from '../../../../core/data/object-updates/field-updates.model'; +import { PaginatedSearchOptions } from '../../../../shared/search/models/paginated-search-options.model'; +import { BundleDataService } from '../../../../core/data/bundle-data.service'; +import { followLink } from '../../../../shared/utils/follow-link-config.model'; +import { + getAllSucceededRemoteData, + paginatedListToArray, + getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteData +} from '../../../../core/shared/operators'; +import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; +import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; +import { map } from 'rxjs/operators'; +import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; +import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; +import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; + +/** + * Interface storing all the information necessary to create a row in the bitstream edit table + */ +export interface BitstreamTableEntry { + /** + * The bitstream + */ + bitstream: Bitstream, + /** + * The uuid of the Bitstream + */ + id: string, + /** + * The name of the Bitstream + */ + name: string, + /** + * The name of the Bitstream with all whitespace removed + */ + nameStripped: string, + /** + * The description of the Bitstream + */ + description: string, + /** + * Observable emitting the Format of the Bitstream + */ + format: Observable, + /** + * The download url of the Bitstream + */ + downloadUrl: string, +} @Component({ selector: 'ds-item-edit-bitstream-bundle', - styleUrls: ['../item-bitstreams.component.scss'], + styleUrls: ['../item-bitstreams.component.scss', './item-edit-bitstream-bundle.component.scss'], templateUrl: './item-edit-bitstream-bundle.component.html', }) /** @@ -17,6 +72,7 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-bundle element) */ export class ItemEditBitstreamBundleComponent implements OnInit { + protected readonly FieldChangeType = FieldChangeType; /** * The view on the bundle information and bitstreams @@ -56,9 +112,48 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ itemPageRoute: string; + /** + * The name of the bundle + */ + bundleName: string; + + /** + * The bitstreams to show in the table + */ + bitstreamsRD$: Observable>>; + + /** + * The data to show in the table + */ + tableEntries$: Observable; + + /** + * The initial page options to use for fetching the bitstreams + */ + paginationOptions: PaginationComponentOptions; + + /** + * The current page options + */ + currentPaginationOptions$: BehaviorSubject; + + /** + * The self url of the bundle, also used when retrieving fieldUpdates + */ + bundleUrl: string; + + /** + * The updates to the current bitstreams + */ + updates$: Observable; + + constructor( protected viewContainerRef: ViewContainerRef, public dsoNameService: DSONameService, + protected bundleService: BundleDataService, + protected objectUpdatesService: ObjectUpdatesService, + protected paginationService: PaginationService, ) { } @@ -66,5 +161,124 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.bundleNameColumn = this.columnSizes.combineColumns(0, 2); this.viewContainerRef.createEmbeddedView(this.bundleView); this.itemPageRoute = getItemPageRoute(this.item); + this.bundleName = this.dsoNameService.getName(this.bundle); + this.bundleUrl = this.bundle.self; + + this.initializePagination(); + this.initializeBitstreams(); + + // this.bitstreamsRD = this. + } + + protected initializePagination() { + this.paginationOptions = Object.assign(new PaginationComponentOptions(),{ + id: this.bundleName, // This might behave unexpectedly if the item contains two bundles with the same name + currentPage: 1, + pageSize: 10 + }); + + this.currentPaginationOptions$ = new BehaviorSubject(this.paginationOptions); + + this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions) + .subscribe((pagination) => { + this.currentPaginationOptions$.next(pagination); + }); + } + + protected initializeBitstreams() { + this.bitstreamsRD$ = this.currentPaginationOptions$.pipe( + switchMap((page: PaginationComponentOptions) => { + const paginatedOptions = new PaginatedSearchOptions({ pagination: Object.assign({}, page) }); + return this.bundleService.getBitstreams(this.bundle.id, paginatedOptions, followLink('format')); + }), + ); + + this.bitstreamsRD$.pipe( + getFirstSucceededRemoteData(), + paginatedListToArray(), + ).subscribe((bitstreams) => { + this.objectUpdatesService.initialize(this.bundleUrl, bitstreams, new Date()); + }); + + this.updates$ = this.bitstreamsRD$.pipe( + getAllSucceededRemoteData(), + paginatedListToArray(), + switchMap((bitstreams) => this.objectUpdatesService.getFieldUpdatesExclusive(this.bundleUrl, bitstreams)) + ); + + this.tableEntries$ = this.bitstreamsRD$.pipe( + getAllSucceededRemoteData(), + paginatedListToArray(), + map((bitstreams) => { + return bitstreams.map((bitstream) => { + const name = this.dsoNameService.getName(bitstream); + + return { + bitstream: bitstream, + id: bitstream.uuid, + name: name, + nameStripped: this.stripWhiteSpace(name), + description: bitstream.firstMetadataValue('dc.description'), + format: bitstream.format.pipe(getFirstSucceededRemoteDataPayload()), + downloadUrl: getBitstreamDownloadRoute(bitstream), + }; + }); + }), + ); + } + + /** + * Check if a user should be allowed to remove this field + */ + canRemove(fieldUpdate: FieldUpdate): boolean { + return fieldUpdate.changeType !== FieldChangeType.REMOVE; + } + + /** + * Check if a user should be allowed to cancel the update to this field + */ + canUndo(fieldUpdate: FieldUpdate): boolean { + return fieldUpdate.changeType >= 0; + } + + /** + * Sends a new remove update for this field to the object updates service + */ + remove(bitstream: Bitstream): void { + this.objectUpdatesService.saveRemoveFieldUpdate(this.bundleUrl, bitstream); + } + + /** + * Cancels the current update for this field in the object updates service + */ + undo(bitstream: Bitstream): void { + this.objectUpdatesService.removeSingleFieldUpdate(this.bundleUrl, bitstream.uuid); + } + + getRowClass(update: FieldUpdate): string { + switch (update.changeType) { + case FieldChangeType.UPDATE: + return 'table-warning'; + case FieldChangeType.ADD: + return 'table-success'; + case FieldChangeType.REMOVE: + return 'table-danger'; + default: + return 'bg-white'; + } + } + + /** + * Returns a string equal to the input string with all whitespace removed. + * @param str + */ + // Whitespace is stripped from the Bitstream names for accessibility reasons. + // To make it clear which headers are relevant for a specific field in the table, the 'headers' attribute is used to + // refer to specific headers. The Bitstream's name is used as header ID for the row containing information regarding + // that bitstream. As the 'headers' attribute contains a space-separated string of header IDs, the Bitstream's header + // ID can not contain strings itself. + stripWhiteSpace(str: string): string { + // '/\s+/g' matches all occurrences of any amount of whitespace characters + return str.replace(/\s+/g, ''); } } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts index 2c81a4e2cb..d5bb9eceea 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts @@ -24,6 +24,10 @@ import { PaginationComponentOptions } from '../../../../../shared/pagination/pag * bitstreams within the paginated list. To drag and drop a bitstream between two pages, drag the row on top of the * page number you want the bitstream to end up at. Doing so will add the bitstream to the top of that page. */ +// NOTE: +// This component was used by the item-edit-bitstream-bundle.component, but this is no longer the case. It is left here +// as a reference for the drag-and-drop functionality. This component (and the abstract version it extends) should be +// removed once this reference is no longer useful. export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginatedDragAndDropListComponent implements OnInit { /** * The bundle to display bitstreams for diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 6c91bae4c1..4d5ef9ee1b 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1934,6 +1934,8 @@ "item.edit.bitstreams.bundle.name": "BUNDLE: {{ name }}", + "item.edit.bitstreams.bundle.table.aria-label": "Bitstreams in the {{ bundle }} Bundle", + "item.edit.bitstreams.discard-button": "Discard", "item.edit.bitstreams.edit.buttons.download": "Download", From a11bfc80ad5b087aab9c6e283a08f0db2f87d5ed Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 3 Sep 2024 14:24:31 +0200 Subject: [PATCH 012/123] 117803: Hide table headers for subsequent bundle tables --- .../item-bitstreams/item-bitstreams.component.html | 3 ++- .../item-edit-bitstream-bundle.component.html | 14 +++++++++----- .../item-edit-bitstream-bundle.component.ts | 5 +++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index 70f6ca55e8..f22dbe6a0e 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -24,10 +24,11 @@
-
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index c11035e305..a95a7921a4 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -10,7 +10,7 @@ - + - - - - diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss index d344b1657a..725d329936 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss @@ -18,3 +18,12 @@ .bitstream-name { font-weight: normal; } + +.pagination-control-container { + display: flex; +} + +.pagination-control { + padding: 0 0.1rem; + vertical-align: center; +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index ae6bc0876c..2a84c00c02 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -26,6 +26,8 @@ import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { SortDirection } from '../../../../core/cache/models/sort-options.model'; +import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; /** * Interface storing all the information necessary to create a row in the bitstream edit table @@ -79,6 +81,8 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ @ViewChild('bundleView', {static: true}) bundleView; + @ViewChild(PaginationComponent) paginationComponent: PaginationComponent; + /** * The bundle to display bitstreams for */ @@ -142,6 +146,16 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ currentPaginationOptions$: BehaviorSubject; + /** + * The available page size options + */ + pageSizeOptions: number[]; + + /** + * The currently selected page size + */ + pageSize$: BehaviorSubject; + /** * The self url of the bundle, also used when retrieving fieldUpdates */ @@ -182,11 +196,15 @@ export class ItemEditBitstreamBundleComponent implements OnInit { pageSize: 10 }); + this.pageSizeOptions = this.paginationOptions.pageSizeOptions; + this.currentPaginationOptions$ = new BehaviorSubject(this.paginationOptions); + this.pageSize$ = new BehaviorSubject(this.paginationOptions.pageSize); this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions) .subscribe((pagination) => { this.currentPaginationOptions$.next(pagination); + this.pageSize$.next(pagination.pageSize); }); } @@ -286,4 +304,9 @@ export class ItemEditBitstreamBundleComponent implements OnInit { // '/\s+/g' matches all occurrences of any amount of whitespace characters return str.replace(/\s+/g, ''); } + + public doPageSizeChange(pageSize: number) { + this.paginationComponent.doPageSizeChange(pageSize); + } + } From d85124c121db1ef88f3015cfdc333bfe1b8c9ce3 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 4 Sep 2024 14:32:29 +0200 Subject: [PATCH 014/123] 117803: Fix deleted bitstreams not being removed from list --- .../item-edit-bitstream-bundle.component.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 2a84c00c02..6ef15397b0 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -26,8 +26,8 @@ import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; -import { SortDirection } from '../../../../core/cache/models/sort-options.model'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; +import { RequestService } from '../../../../core/data/request.service'; /** * Interface storing all the information necessary to create a row in the bitstream edit table @@ -173,6 +173,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { protected bundleService: BundleDataService, protected objectUpdatesService: ObjectUpdatesService, protected paginationService: PaginationService, + protected requestService: RequestService, ) { } @@ -212,7 +213,14 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.bitstreamsRD$ = this.currentPaginationOptions$.pipe( switchMap((page: PaginationComponentOptions) => { const paginatedOptions = new PaginatedSearchOptions({ pagination: Object.assign({}, page) }); - return this.bundleService.getBitstreams(this.bundle.id, paginatedOptions, followLink('format')); + return this.bundleService.getBitstreamsEndpoint(this.bundle.id, paginatedOptions).pipe( + switchMap((href) => this.requestService.hasByHref$(href)), + switchMap(() => this.bundleService.getBitstreams( + this.bundle.id, + paginatedOptions, + followLink('format') + )) + ); }), ); From 374a9ae14eb036c1bf91eec2891aa722722545ed Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 4 Sep 2024 15:46:04 +0200 Subject: [PATCH 015/123] 117803: Add item-bitstream service --- .../item-bitstreams.component.ts | 82 ++------- .../item-bitstreams.service.ts | 158 ++++++++++++++++++ .../item-edit-bitstream-bundle.component.ts | 41 +---- 3 files changed, 176 insertions(+), 105 deletions(-) create mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index ee53bd919c..9f27cf11b3 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -8,23 +8,19 @@ import { ActivatedRoute, Router } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; -import { hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { hasValue } from '../../../shared/empty.util'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { RequestService } from '../../../core/data/request.service'; import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { Bundle } from '../../../core/shared/bundle.model'; -import { Bitstream } from '../../../core/shared/bitstream.model'; import { BundleDataService } from '../../../core/data/bundle-data.service'; import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; -import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; import { NoContent } from '../../../core/shared/NoContent.model'; import { Operation } from 'fast-json-patch'; -import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; -import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model'; -import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; +import { ItemBitstreamsService } from './item-bitstreams.service'; @Component({ selector: 'ds-item-bitstreams', @@ -41,28 +37,10 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ bundles$: Observable; - /** - * The page options to use for fetching the bundles - */ - bundlesOptions = { - id: 'bundles-pagination-options', - currentPage: 1, - pageSize: 9999 - } as any; - /** * The bootstrap sizes used for the columns within this table */ - columnSizes = new ResponsiveTableSizes([ - // Name column - new ResponsiveColumnSizes(2, 2, 3, 4, 4), - // Description column - new ResponsiveColumnSizes(2, 3, 3, 3, 3), - // Format column - new ResponsiveColumnSizes(2, 2, 2, 2, 2), - // Actions column - new ResponsiveColumnSizes(6, 5, 4, 3, 3) - ]); + columnSizes: ResponsiveTableSizes; /** * Are we currently submitting the changes? @@ -88,16 +66,21 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme public requestService: RequestService, public cdRef: ChangeDetectorRef, public bundleService: BundleDataService, - public zone: NgZone + public zone: NgZone, + public itemBitstreamsService: ItemBitstreamsService, ) { super(itemService, objectUpdatesService, router, notificationsService, translateService, route); + + this.columnSizes = this.itemBitstreamsService.getColumnSizes(); } /** * Actions to perform after the item has been initialized */ postItemInit(): void { - this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: this.bundlesOptions})).pipe( + const bundlesOptions = this.itemBitstreamsService.getInitialBundlesPaginationOptions(); + + this. bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: bundlesOptions})).pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), map((bundlePage: PaginatedList) => bundlePage.page) @@ -119,30 +102,12 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ submit() { this.submitting = true; - const bundlesOnce$ = this.bundles$.pipe(take(1)); - // Fetch all removed bitstreams from the object update service - const removedBitstreams$ = bundlesOnce$.pipe( - switchMap((bundles: Bundle[]) => observableZip( - ...bundles.map((bundle: Bundle) => this.objectUpdatesService.getFieldUpdates(bundle.self, [], true)) - )), - map((fieldUpdates: FieldUpdates[]) => ([] as FieldUpdate[]).concat( - ...fieldUpdates.map((updates: FieldUpdates) => Object.values(updates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)) - )), - map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field)) - ); - - // Send out delete requests for all deleted bitstreams - const removedResponses$: Observable> = removedBitstreams$.pipe( - take(1), - switchMap((removedBitstreams: Bitstream[]) => { - return this.bitstreamService.removeMultiple(removedBitstreams); - }) - ); + const removedResponses$ = this.itemBitstreamsService.removeMarkedBitstreams(this.bundles$); // Perform the setup actions from above in order and display notifications removedResponses$.subscribe((responses: RemoteData) => { - this.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]); + this.itemBitstreamsService.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]); this.submitting = false; }); } @@ -164,7 +129,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme } as Operation; this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RemoteData) => { this.zone.run(() => { - this.displayNotifications('item.edit.bitstreams.notifications.move', [response]); + this.itemBitstreamsService.displayNotifications('item.edit.bitstreams.notifications.move', [response]); // Remove all cached requests from this bundle and call the event's callback when the requests are cleared this.requestService.removeByHrefSubstring(bundle.self).pipe( filter((isCached) => isCached), @@ -176,27 +141,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme }); } - /** - * Display notifications - * - Error notification for each failed response with their message - * - Success notification in case there's at least one successful response - * @param key The i18n key for the notification messages - * @param responses The returned responses to display notifications for - */ - displayNotifications(key: string, responses: RemoteData[]) { - if (isNotEmpty(responses)) { - const failedResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasFailed); - const successfulResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasSucceeded); - - failedResponses.forEach((response: RemoteData) => { - this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage); - }); - if (successfulResponses.length > 0) { - this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`)); - } - } - } - /** * Request the object updates service to discard all current changes to this item * Shows a notification to remind the user that they can undo this diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts new file mode 100644 index 0000000000..487df77b28 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -0,0 +1,158 @@ +import { Injectable } from '@angular/core'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; +import { RemoteData } from '../../../core/data/remote-data'; +import { isNotEmpty, hasValue } from '../../../shared/empty.util'; +import { Bundle } from '../../../core/shared/bundle.model'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable, zip as observableZip } from 'rxjs'; +import { NoContent } from '../../../core/shared/NoContent.model'; +import { take, switchMap, map } from 'rxjs/operators'; +import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model'; +import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; +import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; +import { BitstreamTableEntry } from './item-edit-bitstream-bundle/item-edit-bitstream-bundle.component'; +import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { getBitstreamDownloadRoute } from '../../../app-routing-paths'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; + +@Injectable( + { providedIn: 'root' }, +) +export class ItemBitstreamsService { + + constructor( + protected notificationsService: NotificationsService, + protected translateService: TranslateService, + protected objectUpdatesService: ObjectUpdatesService, + protected bitstreamService: BitstreamDataService, + protected dsoNameService: DSONameService, + ) { + } + + /** + * Returns the pagination options to use when fetching the bundles + */ + getInitialBundlesPaginationOptions(): PaginationComponentOptions { + return Object.assign(new PaginationComponentOptions(), { + id: 'bundles-pagination-options', + currentPage: 1, + pageSize: 9999 + }); + } + + getInitialBitstreamsPaginationOptions(bundleName: string): PaginationComponentOptions { + return Object.assign(new PaginationComponentOptions(),{ + id: bundleName, // This might behave unexpectedly if the item contains two bundles with the same name + currentPage: 1, + pageSize: 10 + }); + } + + /** + * Returns the {@link ResponsiveTableSizes} for use in the columns of the edit bitstreams table + */ + getColumnSizes(): ResponsiveTableSizes { + return new ResponsiveTableSizes([ + // Name column + new ResponsiveColumnSizes(2, 2, 3, 4, 4), + // Description column + new ResponsiveColumnSizes(2, 3, 3, 3, 3), + // Format column + new ResponsiveColumnSizes(2, 2, 2, 2, 2), + // Actions column + new ResponsiveColumnSizes(6, 5, 4, 3, 3) + ]); + } + + /** + * Display notifications + * - Error notification for each failed response with their message + * - Success notification in case there's at least one successful response + * @param key The i18n key for the notification messages + * @param responses The returned responses to display notifications for + */ + displayNotifications(key: string, responses: RemoteData[]) { + if (isNotEmpty(responses)) { + const failedResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasFailed); + const successfulResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasSucceeded); + + failedResponses.forEach((response: RemoteData) => { + this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage); + }); + if (successfulResponses.length > 0) { + this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`)); + } + } + } + + /** + * Removes the bitstreams marked for deletion from the Bundles emitted by the provided observable. + * @param bundles$ + */ + removeMarkedBitstreams(bundles$: Observable): Observable> { + const bundlesOnce$ = bundles$.pipe(take(1)); + + // Fetch all removed bitstreams from the object update service + const removedBitstreams$ = bundlesOnce$.pipe( + switchMap((bundles: Bundle[]) => observableZip( + ...bundles.map((bundle: Bundle) => this.objectUpdatesService.getFieldUpdates(bundle.self, [], true)) + )), + map((fieldUpdates: FieldUpdates[]) => ([] as FieldUpdate[]).concat( + ...fieldUpdates.map((updates: FieldUpdates) => Object.values(updates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)) + )), + map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field)) + ); + + // Send out delete requests for all deleted bitstreams + return removedBitstreams$.pipe( + take(1), + switchMap((removedBitstreams: Bitstream[]) => { + return this.bitstreamService.removeMultiple(removedBitstreams); + }) + ); + } + + mapBitstreamsToTableEntries(bitstreams: Bitstream[]): BitstreamTableEntry[] { + return bitstreams.map((bitstream) => { + const name = this.dsoNameService.getName(bitstream); + + return { + bitstream: bitstream, + id: bitstream.uuid, + name: name, + nameStripped: this.nameToHeader(name), + description: bitstream.firstMetadataValue('dc.description'), + format: bitstream.format.pipe(getFirstSucceededRemoteDataPayload()), + downloadUrl: getBitstreamDownloadRoute(bitstream), + }; + }); + } + + /** + * Returns a string appropriate to be used as header ID + * @param name + */ + nameToHeader(name: string): string { + // Whitespace is stripped from the Bitstream names for accessibility reasons. + // To make it clear which headers are relevant for a specific field in the table, the 'headers' attribute is used to + // refer to specific headers. The Bitstream's name is used as header ID for the row containing information regarding + // that bitstream. As the 'headers' attribute contains a space-separated string of header IDs, the Bitstream's header + // ID can not contain strings itself. + return this.stripWhiteSpace(name); + } + + /** + * Returns a string equal to the input string with all whitespace removed. + * @param str + */ + stripWhiteSpace(str: string): string { + // '/\s+/g' matches all occurrences of any amount of whitespace characters + return str.replace(/\s+/g, ''); + } +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 6ef15397b0..9c62fe06e7 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -17,17 +17,17 @@ import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { getAllSucceededRemoteData, paginatedListToArray, - getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteData + getFirstSucceededRemoteData } from '../../../../core/shared/operators'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; import { map } from 'rxjs/operators'; -import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { RequestService } from '../../../../core/data/request.service'; +import { ItemBitstreamsService } from '../item-bitstreams.service'; /** * Interface storing all the information necessary to create a row in the bitstream edit table @@ -174,6 +174,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { protected objectUpdatesService: ObjectUpdatesService, protected paginationService: PaginationService, protected requestService: RequestService, + protected itemBitstreamsService: ItemBitstreamsService, ) { } @@ -191,11 +192,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { } protected initializePagination() { - this.paginationOptions = Object.assign(new PaginationComponentOptions(),{ - id: this.bundleName, // This might behave unexpectedly if the item contains two bundles with the same name - currentPage: 1, - pageSize: 10 - }); + this.paginationOptions = this.itemBitstreamsService.getInitialBitstreamsPaginationOptions(this.bundleName); this.pageSizeOptions = this.paginationOptions.pageSizeOptions; @@ -240,21 +237,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.tableEntries$ = this.bitstreamsRD$.pipe( getAllSucceededRemoteData(), paginatedListToArray(), - map((bitstreams) => { - return bitstreams.map((bitstream) => { - const name = this.dsoNameService.getName(bitstream); - - return { - bitstream: bitstream, - id: bitstream.uuid, - name: name, - nameStripped: this.stripWhiteSpace(name), - description: bitstream.firstMetadataValue('dc.description'), - format: bitstream.format.pipe(getFirstSucceededRemoteDataPayload()), - downloadUrl: getBitstreamDownloadRoute(bitstream), - }; - }); - }), + map((bitstreams) => this.itemBitstreamsService.mapBitstreamsToTableEntries(bitstreams)), ); } @@ -299,20 +282,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit { } } - /** - * Returns a string equal to the input string with all whitespace removed. - * @param str - */ - // Whitespace is stripped from the Bitstream names for accessibility reasons. - // To make it clear which headers are relevant for a specific field in the table, the 'headers' attribute is used to - // refer to specific headers. The Bitstream's name is used as header ID for the row containing information regarding - // that bitstream. As the 'headers' attribute contains a space-separated string of header IDs, the Bitstream's header - // ID can not contain strings itself. - stripWhiteSpace(str: string): string { - // '/\s+/g' matches all occurrences of any amount of whitespace characters - return str.replace(/\s+/g, ''); - } - public doPageSizeChange(pageSize: number) { this.paginationComponent.doPageSizeChange(pageSize); } From 3f4bf7ce0fdf73494bcb28045ef6d7cb8a4634dd Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 4 Sep 2024 16:32:30 +0200 Subject: [PATCH 016/123] 117803: Fix existing tests --- ...em-edit-bitstream-bundle.component.spec.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts index c26f99eb8f..1502ad2311 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts @@ -6,6 +6,15 @@ import { Item } from '../../../../core/shared/item.model'; import { Bundle } from '../../../../core/shared/bundle.model'; import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; +import { BundleDataService } from '../../../../core/data/bundle-data.service'; +import { of as observableOf } from 'rxjs'; +import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; +import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; +import { RequestService } from '../../../../core/data/request.service'; +import { getMockRequestService } from '../../../../shared/mocks/request.service.mock'; +import { ItemBitstreamsService } from '../item-bitstreams.service'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; describe('ItemEditBitstreamBundleComponent', () => { let comp: ItemEditBitstreamBundleComponent; @@ -31,10 +40,37 @@ describe('ItemEditBitstreamBundleComponent', () => { } }); + const restEndpoint = 'fake-rest-endpoint'; + const bundleService = jasmine.createSpyObj('bundleService', { + getBitstreamsEndpoint: observableOf(restEndpoint), + getBitstreams: null, + }); + + const objectUpdatesService = { + initialize: () => { + // do nothing + }, + }; + + const itemBitstreamsService = jasmine.createSpyObj('itemBitstreamsService', { + getInitialBitstreamsPaginationOptions: Object.assign(new PaginationComponentOptions(), { + id: 'bundles-pagination-options', + currentPage: 1, + pageSize: 9999 + }), + }); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], declarations: [ItemEditBitstreamBundleComponent], + providers: [ + { provide: BundleDataService, useValue: bundleService }, + { provide: ObjectUpdatesService, useValue: objectUpdatesService }, + { provide: PaginationService, useValue: new PaginationServiceStub() }, + { provide: RequestService, useValue: getMockRequestService() }, + { provide: ItemBitstreamsService, useValue: itemBitstreamsService }, + ], schemas: [ NO_ERRORS_SCHEMA ] From d8b426d7458da47320447a85e3c2fb4c7028f20c Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Thu, 5 Sep 2024 10:52:02 +0200 Subject: [PATCH 017/123] 117803: Add ItemBitsreamsService tests --- .../object-updates.service.stub.ts | 28 ++++ .../item-bitstreams.service.spec.ts | 129 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/app/core/data/object-updates/object-updates.service.stub.ts create mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts diff --git a/src/app/core/data/object-updates/object-updates.service.stub.ts b/src/app/core/data/object-updates/object-updates.service.stub.ts new file mode 100644 index 0000000000..c41728a338 --- /dev/null +++ b/src/app/core/data/object-updates/object-updates.service.stub.ts @@ -0,0 +1,28 @@ +export class ObjectUpdatesServiceStub { + + initialize = jasmine.createSpy('initialize'); + saveFieldUpdate = jasmine.createSpy('saveFieldUpdate'); + getObjectEntry = jasmine.createSpy('getObjectEntry'); + getFieldState = jasmine.createSpy('getFieldState'); + getFieldUpdates = jasmine.createSpy('getFieldUpdates'); + getFieldUpdatesExclusive = jasmine.createSpy('getFieldUpdatesExclusive'); + isValid = jasmine.createSpy('isValid'); + isValidPage = jasmine.createSpy('isValidPage'); + saveAddFieldUpdate = jasmine.createSpy('saveAddFieldUpdate'); + saveRemoveFieldUpdate = jasmine.createSpy('saveRemoveFieldUpdate'); + saveChangeFieldUpdate = jasmine.createSpy('saveChangeFieldUpdate'); + isSelectedVirtualMetadata = jasmine.createSpy('isSelectedVirtualMetadata'); + setSelectedVirtualMetadata = jasmine.createSpy('setSelectedVirtualMetadata'); + setEditableFieldUpdate = jasmine.createSpy('setEditableFieldUpdate'); + setValidFieldUpdate = jasmine.createSpy('setValidFieldUpdate'); + discardFieldUpdates = jasmine.createSpy('discardFieldUpdates'); + discardAllFieldUpdates = jasmine.createSpy('discardAllFieldUpdates'); + reinstateFieldUpdates = jasmine.createSpy('reinstateFieldUpdates'); + removeSingleFieldUpdate = jasmine.createSpy('removeSingleFieldUpdate'); + getUpdateFields = jasmine.createSpy('getUpdateFields'); + hasUpdates = jasmine.createSpy('hasUpdates'); + isReinstatable = jasmine.createSpy('isReinstatable'); + getLastModified = jasmine.createSpy('getLastModified'); + createPatch = jasmine.createSpy('getPatch'); + +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts new file mode 100644 index 0000000000..89ecfb518f --- /dev/null +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts @@ -0,0 +1,129 @@ +import { ItemBitstreamsService } from './item-bitstreams.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; +import { ObjectUpdatesServiceStub } from '../../../core/data/object-updates/object-updates.service.stub'; +import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub'; +import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { TranslateService } from '@ngx-translate/core'; +import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; +import { + createSuccessfulRemoteDataObject$, + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject +} from '../../../shared/remote-data.utils'; + +describe('ItemBitstreamsService', () => { + let service: ItemBitstreamsService; + let notificationsService: NotificationsService; + let translateService: TranslateService; + let objectUpdatesService: ObjectUpdatesService; + let bitstreamDataService: BitstreamDataService; + let dsoNameService: DSONameService; + + beforeEach(() => { + notificationsService = new NotificationsServiceStub() as any; + translateService = getMockTranslateService(); + objectUpdatesService = new ObjectUpdatesServiceStub() as any; + bitstreamDataService = new BitstreamDataServiceStub() as any; + dsoNameService = new DSONameServiceMock() as any; + + service = new ItemBitstreamsService( + notificationsService, + translateService, + objectUpdatesService, + bitstreamDataService, + dsoNameService, + ); + }); + + describe('displayNotifications', () => { + it('should display an error notification if a response failed', () => { + const responses = [ + createFailedRemoteDataObject(), + ]; + + const key = 'some.key'; + + service.displayNotifications(key, responses); + + expect(notificationsService.success).not.toHaveBeenCalled(); + expect(notificationsService.error).toHaveBeenCalled(); + expect(translateService.instant).toHaveBeenCalledWith('some.key.failed.title'); + }); + + it('should display a success notification if a response succeeded', () => { + const responses = [ + createSuccessfulRemoteDataObject(undefined), + ]; + + const key = 'some.key'; + + service.displayNotifications(key, responses); + + expect(notificationsService.success).toHaveBeenCalled(); + expect(notificationsService.error).not.toHaveBeenCalled(); + expect(translateService.instant).toHaveBeenCalledWith('some.key.saved.title'); + }); + + it('should display both notifications if some failed and some succeeded', () => { + const responses = [ + createFailedRemoteDataObject(), + createSuccessfulRemoteDataObject(undefined), + ]; + + const key = 'some.key'; + + service.displayNotifications(key, responses); + + expect(notificationsService.success).toHaveBeenCalled(); + expect(notificationsService.error).toHaveBeenCalled(); + expect(translateService.instant).toHaveBeenCalledWith('some.key.saved.title'); + expect(translateService.instant).toHaveBeenCalledWith('some.key.saved.title'); + }); + }); + + describe('mapBitstreamsToTableEntries', () => { + it('should correctly map a Bitstream to a BitstreamTableEntry', () => { + const format: BitstreamFormat = new BitstreamFormat(); + + const bitstream: Bitstream = Object.assign(new Bitstream(), { + uuid: 'testUUID', + format: createSuccessfulRemoteDataObject$(format), + }); + + spyOn(dsoNameService, 'getName').and.returnValue('Test Name'); + spyOn(bitstream, 'firstMetadataValue').and.returnValue('description'); + + const tableEntry = service.mapBitstreamsToTableEntries([bitstream])[0]; + + expect(tableEntry.name).toEqual('Test Name'); + expect(tableEntry.nameStripped).toEqual('TestName'); + expect(tableEntry.bitstream).toBe(bitstream); + expect(tableEntry.id).toEqual('testUUID'); + expect(tableEntry.description).toEqual('description'); + expect(tableEntry.downloadUrl).toEqual('/bitstreams/testUUID/download'); + }); + }); + + describe('nameToHeader', () => { + it('should correctly transform a string to an appropriate header ID', () => { + const stringA = 'Test String'; + const stringAResult = 'TestString'; + expect(service.nameToHeader(stringA)).toEqual(stringAResult); + + const stringB = 'Test String Two'; + const stringBResult = 'TestStringTwo'; + expect(service.nameToHeader(stringB)).toEqual(stringBResult); + + const stringC = 'Test String Three'; + const stringCResult = 'TestStringThree'; + expect(service.nameToHeader(stringC)).toEqual(stringCResult); + }); + }); + +}); From cc5b841a65ee396a2622ca3eceb1f2d9bbce07c7 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 10 Sep 2024 13:36:27 +0200 Subject: [PATCH 018/123] 117803: Only visually hide header rows --- .../item-edit-bitstream-bundle.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index 6a505b9274..2eb1a151b7 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -10,7 +10,7 @@
{{'item.edit.bitstreams.headers.name' | translate}} @@ -45,16 +45,20 @@
+ {{ entry.name }} + {{ entry.description }} + {{ (entry.format | async)?.shortDescription }} + - + +
+ +
+ + + +
+
+
- +
{{'item.edit.bitstreams.headers.name' | translate}} From 8481604b1eb63acdc152fc9b7061bb9cd464294e Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 10 Sep 2024 14:14:58 +0200 Subject: [PATCH 019/123] 117803: Fix table & pagination margin --- .../item-bitstreams/item-bitstreams.component.scss | 4 ++++ .../item-edit-bitstream-bundle.component.scss | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss index 480bf56cc7..662c999461 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss @@ -42,3 +42,7 @@ .table-border { border: 1px solid #dee2e6; } + +:host ::ng-deep .pagination { + padding-top: 0.5rem; +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss index 725d329936..ae4eac8d52 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss @@ -27,3 +27,7 @@ padding: 0 0.1rem; vertical-align: center; } + +.table { + margin-bottom: 0; +} From 79f3a3116e2e358d5d7b778324d15c4977c7d55f Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 10 Sep 2024 14:35:05 +0200 Subject: [PATCH 020/123] 117803: Set header border to background color --- .../item-edit-bitstream-bundle.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss index ae4eac8d52..7088c3978e 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss @@ -1,7 +1,7 @@ .header-row { color: var(--bs-table-dark-color); background-color: var(--bs-table-dark-bg); - border-color: var(--bs-table-dark-border-color); + border-color: var(--bs-table-dark-bg); } .bundle-row { From 6a8095d456167a139d320c4ad5c3a3d8a20a365e Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 10 Sep 2024 15:00:18 +0200 Subject: [PATCH 021/123] 117803: Change pagination settings styling --- .../item-edit-bitstream-bundle.component.html | 4 ++-- .../item-edit-bitstream-bundle.component.scss | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index 2eb1a151b7..f434bf0f8f 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -41,8 +41,8 @@ title="{{'item.edit.bitstreams.bundle.edit.buttons.upload' | translate}}"> -
- diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss index 7088c3978e..bbd4e1e75c 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss @@ -19,15 +19,6 @@ font-weight: normal; } -.pagination-control-container { - display: flex; -} - -.pagination-control { - padding: 0 0.1rem; - vertical-align: center; -} - .table { margin-bottom: 0; } From a230eee76d931a5b55046587c0fe06fcdd383a90 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 10 Sep 2024 16:06:44 +0200 Subject: [PATCH 022/123] 117803: Add negative top-margin to subsequent tables --- .../item-bitstreams/item-bitstreams.component.html | 2 +- .../item-edit-bitstream-bundle.component.html | 4 ++-- .../item-edit-bitstream-bundle.component.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index f22dbe6a0e..3527f2f5b8 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -28,7 +28,7 @@ [bundle]="bundle" [item]="item" [columnSizes]="columnSizes" - [hideHeader]="!isFirst" + [isFirstTable]="isFirst" (dropObject)="dropBitstream(bundle, $event)">
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index f434bf0f8f..3d6ee4fa47 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -8,9 +8,9 @@ [collectionSize]="bitstreamsList.totalElements"> - - + - + - + - + - + + (cdkDragStarted)="dragStart(entry.name)" (cdkDragEnded)="dragEnd(entry.name)">
{{'item.edit.bitstreams.headers.name' | translate}} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 9c62fe06e7..0974da3f8c 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -99,9 +99,9 @@ export class ItemEditBitstreamBundleComponent implements OnInit { @Input() columnSizes: ResponsiveTableSizes; /** - * Whether to hide the table headers + * Whether this is the first in a series of bundle tables */ - @Input() hideHeader = false; + @Input() isFirstTable = false; /** * Send an event when the user drops an object on the pagination From be99cc5c23763240869697798b95608e4ff1e319 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 11 Sep 2024 10:09:03 +0200 Subject: [PATCH 023/123] 118219: Allow dragging of table rows --- .../item-bitstreams/item-bitstreams.component.scss | 7 ------- .../item-edit-bitstream-bundle.component.html | 7 +++++-- .../item-edit-bitstream-bundle.component.ts | 5 +++++ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss index 662c999461..0ee56fe67e 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss @@ -1,7 +1,4 @@ - - .drag-handle { - visibility: hidden; &:hover { cursor: move; } @@ -11,10 +8,6 @@ cursor: move; } -:host ::ng-deep .bitstream-row:hover .drag-handle, :host ::ng-deep .bitstream-row-drag-handle:focus .drag-handle { - visibility: visible !important; -} - .cdk-drag-preview { margin-left: 0; box-sizing: border-box; diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index 3d6ee4fa47..b79518a763 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -27,7 +27,7 @@
{{'item.edit.bitstreams.bundle.name' | translate:{ name: bundleName } }} @@ -65,10 +65,13 @@
+
+ +
{{ entry.name }}
) { + console.log('dropEvent:', event); + } + } From eadbcdbe14108b6c47f9e623e8b99c10d03ff5bd Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 11 Sep 2024 16:25:42 +0200 Subject: [PATCH 024/123] 118219: Store result of drag & dropping bitstream --- .../item-bitstreams.component.ts | 15 +++-- .../item-edit-bitstream-bundle.component.ts | 60 ++++++++++++++++--- 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 9f27cf11b3..e7c846b5da 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; -import { filter, map, switchMap, take } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { Observable, Subscription, zip as observableZip } from 'rxjs'; import { ItemDataService } from '../../../core/data/item-data.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; @@ -11,7 +11,11 @@ import { BitstreamDataService } from '../../../core/data/bitstream-data.service' import { hasValue } from '../../../shared/empty.util'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { RequestService } from '../../../core/data/request.service'; -import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; +import { + getFirstSucceededRemoteData, + getRemoteDataPayload, + getFirstCompletedRemoteData +} from '../../../core/shared/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { Bundle } from '../../../core/shared/bundle.model'; @@ -127,12 +131,13 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme from: `/_links/bitstreams/${event.fromIndex}/href`, path: `/_links/bitstreams/${event.toIndex}/href` } as Operation; - this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RemoteData) => { + this.bundleService.patch(bundle, [moveOperation]).pipe( + getFirstCompletedRemoteData(), + ).subscribe((response: RemoteData) => { this.zone.run(() => { this.itemBitstreamsService.displayNotifications('item.edit.bitstreams.notifications.move', [response]); // Remove all cached requests from this bundle and call the event's callback when the requests are cleared - this.requestService.removeByHrefSubstring(bundle.self).pipe( - filter((isCached) => isCached), + this.requestService.setStaleByHrefSubstring(bundle.self).pipe( take(1) ).subscribe(() => event.finish()); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index c0c4e30231..0293a1daea 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -28,7 +28,8 @@ import { PaginationService } from '../../../../core/pagination/pagination.servic import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { RequestService } from '../../../../core/data/request.service'; import { ItemBitstreamsService } from '../item-bitstreams.service'; -import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { hasValue } from '../../../../shared/empty.util'; /** * Interface storing all the information necessary to create a row in the bitstream edit table @@ -135,7 +136,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { /** * The data to show in the table */ - tableEntries$: Observable; + tableEntries$: BehaviorSubject = new BehaviorSubject(null); /** * The initial page options to use for fetching the bitstreams @@ -165,7 +166,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { /** * The updates to the current bitstreams */ - updates$: Observable; + updates$: BehaviorSubject = new BehaviorSubject(null); constructor( @@ -229,17 +230,17 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.objectUpdatesService.initialize(this.bundleUrl, bitstreams, new Date()); }); - this.updates$ = this.bitstreamsRD$.pipe( + this.bitstreamsRD$.pipe( getAllSucceededRemoteData(), paginatedListToArray(), switchMap((bitstreams) => this.objectUpdatesService.getFieldUpdatesExclusive(this.bundleUrl, bitstreams)) - ); + ).subscribe((updates) => this.updates$.next(updates)); - this.tableEntries$ = this.bitstreamsRD$.pipe( + this.bitstreamsRD$.pipe( getAllSucceededRemoteData(), paginatedListToArray(), map((bitstreams) => this.itemBitstreamsService.mapBitstreamsToTableEntries(bitstreams)), - ); + ).subscribe((tableEntries) => this.tableEntries$.next(tableEntries)); } /** @@ -288,7 +289,50 @@ export class ItemEditBitstreamBundleComponent implements OnInit { } drop(event: CdkDragDrop) { - console.log('dropEvent:', event); + const dragIndex = event.previousIndex; + let dropIndex = event.currentIndex; + const dragPage = this.currentPaginationOptions$.value.currentPage - 1; + let dropPage = this.currentPaginationOptions$.value.currentPage - 1; + + // Check if the user is hovering over any of the pagination's pages at the time of dropping the object + const droppedOnElement = document.elementFromPoint(event.dropPoint.x, event.dropPoint.y); + if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent) && droppedOnElement.classList.contains('page-link')) { + // The user is hovering over a page, fetch the page's number from the element + const droppedPage = Number(droppedOnElement.textContent); + if (hasValue(droppedPage) && !Number.isNaN(droppedPage)) { + dropPage = droppedPage - 1; + dropIndex = 0; + } + } + + const isNewPage = dragPage !== dropPage; + // Move the object in the custom order array if the drop happened within the same page + // This allows us to instantly display a change in the order, instead of waiting for the REST API's response first + if (!isNewPage && dragIndex !== dropIndex) { + const currentEntries = [...this.tableEntries$.value]; + moveItemInArray(currentEntries, dragIndex, dropIndex); + this.tableEntries$.next(currentEntries); + } + + const pageSize = this.currentPaginationOptions$.value.pageSize; + const redirectPage = dropPage + 1; + const fromIndex = (dragPage * pageSize) + dragIndex; + const toIndex = (dropPage * pageSize) + dropIndex; + // Send out a drop event (and navigate to the new page) when the "from" and "to" indexes are different from each other + if (fromIndex !== toIndex) { + // if (isNewPage) { + // this.loading$.next(true); + // } + this.dropObject.emit(Object.assign({ + fromIndex, + toIndex, + finish: () => { + if (isNewPage) { + this.paginationComponent.doPageChange(redirectPage); + } + } + })); + } } } From 6a2c7d09d69e6275d5e29d06a2672ce9b79e52fb Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Thu, 12 Sep 2024 10:01:55 +0200 Subject: [PATCH 025/123] 118219: Add dragging tooltip explaining how to drag to other page --- .../item-edit-bitstream-bundle.component.html | 7 +++-- .../item-edit-bitstream-bundle.component.ts | 30 ++++++++++++++++++- src/assets/i18n/en.json5 | 2 ++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index b79518a763..0338055df5 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -27,7 +27,9 @@
{{'item.edit.bitstreams.bundle.name' | translate:{ name: bundleName } }} @@ -65,7 +67,8 @@
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 0293a1daea..bd99fc1a09 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -21,7 +21,7 @@ import { } from '../../../../core/shared/operators'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; -import { map } from 'rxjs/operators'; +import { map, take, filter } from 'rxjs/operators'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; @@ -83,8 +83,16 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ @ViewChild('bundleView', {static: true}) bundleView; + /** + * The view on the pagination component + */ @ViewChild(PaginationComponent) paginationComponent: PaginationComponent; + /** + * The view on the drag tooltip + */ + @ViewChild('dragTooltip') dragTooltip; + /** * The bundle to display bitstreams for */ @@ -158,6 +166,11 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ pageSize$: BehaviorSubject; + /** + * Whether the table has multiple pages + */ + hasMultiplePages = false; + /** * The self url of the bundle, also used when retrieving fieldUpdates */ @@ -288,6 +301,21 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.paginationComponent.doPageSizeChange(pageSize); } + dragStart() { + // Only open the drag tooltip when there are multiple pages + this.paginationComponent.shouldShowBottomPager.pipe( + take(1), + filter((hasMultiplePages) => hasMultiplePages), + ).subscribe(() => { + this.dragTooltip.open(); + }); + } + + dragEnd() { + this.dragTooltip.close(); + } + + drop(event: CdkDragDrop) { const dragIndex = event.previousIndex; let dropIndex = event.currentIndex; diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4d5ef9ee1b..f2dbf9b2d1 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1936,6 +1936,8 @@ "item.edit.bitstreams.bundle.table.aria-label": "Bitstreams in the {{ bundle }} Bundle", + "item.edit.bitstreams.bundle.tooltip": "You can move a bitstream to a different page by dropping it on the page number.", + "item.edit.bitstreams.discard-button": "Discard", "item.edit.bitstreams.edit.buttons.download": "Download", From 8a16597b6958f99f949a413028ee2d5dee2905dc Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Thu, 12 Sep 2024 10:33:21 +0200 Subject: [PATCH 026/123] 118219: Fix tests --- .../item-bitstreams.component.spec.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index 10e1812131..6ce7394473 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -18,7 +18,6 @@ import { ObjectValuesPipe } from '../../../shared/utils/object-values-pipe'; import { VarDirective } from '../../../shared/utils/var.directive'; import { BundleDataService } from '../../../core/data/bundle-data.service'; import { Bundle } from '../../../core/shared/bundle.model'; -import { RestResponse } from '../../../core/cache/response.models'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; import { RouterStub } from '../../../shared/testing/router.stub'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; @@ -145,7 +144,7 @@ describe('ItemBitstreamsComponent', () => { url: url }); bundleService = jasmine.createSpyObj('bundleService', { - patch: observableOf(new RestResponse(true, 200, 'OK')) + patch: createSuccessfulRemoteDataObject$({}), }); TestBed.configureTestingModule({ @@ -191,20 +190,6 @@ describe('ItemBitstreamsComponent', () => { }); }); - describe('when dropBitstream is called', () => { - const event = { - fromIndex: 0, - toIndex: 50, - // eslint-disable-next-line no-empty,@typescript-eslint/no-empty-function - finish: () => { - } - }; - - beforeEach(() => { - comp.dropBitstream(bundle, event); - }); - }); - describe('when dropBitstream is called', () => { beforeEach((done) => { comp.dropBitstream(bundle, { From a207fb51e9159c0076bb8a76cd5a73944a11655b Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Thu, 12 Sep 2024 10:38:38 +0200 Subject: [PATCH 027/123] 118219: Remove unused paginated-drag-and-drop components --- .../edit-item-page/edit-item-page.module.ts | 4 - ...rag-and-drop-bitstream-list.component.html | 33 --- ...-and-drop-bitstream-list.component.spec.ts | 150 ----------- ...-drag-and-drop-bitstream-list.component.ts | 80 ------ ...-edit-bitstream-drag-handle.component.html | 5 - ...em-edit-bitstream-drag-handle.component.ts | 26 -- ...nated-drag-and-drop-list.component.spec.ts | 136 ---------- ...-paginated-drag-and-drop-list.component.ts | 239 ------------------ 8 files changed, 673 deletions(-) delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts delete mode 100644 src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts delete mode 100644 src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts diff --git a/src/app/item-page/edit-item-page/edit-item-page.module.ts b/src/app/item-page/edit-item-page/edit-item-page.module.ts index 0a75394ddd..4ae5ebe666 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.module.ts @@ -26,8 +26,6 @@ import { ItemMoveComponent } from './item-move/item-move.component'; import { ItemEditBitstreamBundleComponent } from './item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component'; import { BundleDataService } from '../../core/data/bundle-data.service'; import { DragDropModule } from '@angular/cdk/drag-drop'; -import { ItemEditBitstreamDragHandleComponent } from './item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component'; -import { PaginatedDragAndDropBitstreamListComponent } from './item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component'; import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component'; import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component'; @@ -82,12 +80,10 @@ import { ItemVersionHistoryComponent, ItemEditBitstreamComponent, ItemEditBitstreamBundleComponent, - PaginatedDragAndDropBitstreamListComponent, EditRelationshipComponent, EditRelationshipListComponent, ItemCollectionMapperComponent, ItemMoveComponent, - ItemEditBitstreamDragHandleComponent, VirtualMetadataComponent, ItemAuthorizationsComponent, IdentifierDataComponent, diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html deleted file mode 100644 index f54aa73597..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html +++ /dev/null @@ -1,33 +0,0 @@ - - -
- -
- -
- -
-
-
-
-
-
- -
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts deleted file mode 100644 index 7317eb93be..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { Bundle } from '../../../../../core/shared/bundle.model'; -import { TranslateModule } from '@ngx-translate/core'; -import { PaginatedDragAndDropBitstreamListComponent } from './paginated-drag-and-drop-bitstream-list.component'; -import { VarDirective } from '../../../../../shared/utils/var.directive'; -import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe'; -import { ObjectUpdatesService } from '../../../../../core/data/object-updates/object-updates.service'; -import { BundleDataService } from '../../../../../core/data/bundle-data.service'; -import { Bitstream } from '../../../../../core/shared/bitstream.model'; -import { BitstreamFormat } from '../../../../../core/shared/bitstream-format.model'; -import { of as observableOf } from 'rxjs'; -import { take } from 'rxjs/operators'; -import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { ResponsiveColumnSizes } from '../../../../../shared/responsive-table-sizes/responsive-column-sizes'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; -import { createPaginatedList } from '../../../../../shared/testing/utils.test'; -import { RequestService } from '../../../../../core/data/request.service'; -import { PaginationService } from '../../../../../core/pagination/pagination.service'; -import { PaginationServiceStub } from '../../../../../shared/testing/pagination-service.stub'; - -describe('PaginatedDragAndDropBitstreamListComponent', () => { - let comp: PaginatedDragAndDropBitstreamListComponent; - let fixture: ComponentFixture; - let objectUpdatesService: ObjectUpdatesService; - let bundleService: BundleDataService; - let objectValuesPipe: ObjectValuesPipe; - let requestService: RequestService; - let paginationService; - - const columnSizes = new ResponsiveTableSizes([ - new ResponsiveColumnSizes(2, 2, 3, 4, 4), - new ResponsiveColumnSizes(2, 3, 3, 3, 3), - new ResponsiveColumnSizes(2, 2, 2, 2, 2), - new ResponsiveColumnSizes(6, 5, 4, 3, 3) - ]); - - const bundle = Object.assign(new Bundle(), { - id: 'bundle-1', - uuid: 'bundle-1', - _links: { - self: { href: 'bundle-1-selflink' } - } - }); - const date = new Date(); - const format = Object.assign(new BitstreamFormat(), { - shortDescription: 'PDF' - }); - const bitstream1 = Object.assign(new Bitstream(), { - uuid: 'bitstreamUUID1', - name: 'Fake Bitstream 1', - bundleName: 'ORIGINAL', - description: 'Description', - format: createSuccessfulRemoteDataObject$(format) - }); - const fieldUpdate1 = { - field: bitstream1, - changeType: undefined - }; - const bitstream2 = Object.assign(new Bitstream(), { - uuid: 'bitstreamUUID2', - name: 'Fake Bitstream 2', - bundleName: 'ORIGINAL', - description: 'Description', - format: createSuccessfulRemoteDataObject$(format) - }); - const fieldUpdate2 = { - field: bitstream2, - changeType: undefined - }; - - beforeEach(waitForAsync(() => { - objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', - { - getFieldUpdates: observableOf({ - [bitstream1.uuid]: fieldUpdate1, - [bitstream2.uuid]: fieldUpdate2, - }), - getFieldUpdatesExclusive: observableOf({ - [bitstream1.uuid]: fieldUpdate1, - [bitstream2.uuid]: fieldUpdate2, - }), - getFieldUpdatesByCustomOrder: observableOf({ - [bitstream1.uuid]: fieldUpdate1, - [bitstream2.uuid]: fieldUpdate2, - }), - saveMoveFieldUpdate: {}, - saveRemoveFieldUpdate: {}, - removeSingleFieldUpdate: {}, - saveAddFieldUpdate: {}, - discardFieldUpdates: {}, - reinstateFieldUpdates: observableOf(true), - initialize: {}, - getUpdatedFields: observableOf([bitstream1, bitstream2]), - getLastModified: observableOf(date), - hasUpdates: observableOf(true), - isReinstatable: observableOf(false), - isValidPage: observableOf(true), - initializeWithCustomOrder: {}, - addPageToCustomOrder: {} - } - ); - - bundleService = jasmine.createSpyObj('bundleService', { - getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2])), - getBitstreamsEndpoint: observableOf('') - }); - - objectValuesPipe = new ObjectValuesPipe(); - - requestService = jasmine.createSpyObj('requestService', { - hasByHref$: observableOf(true) - }); - - paginationService = new PaginationServiceStub(); - - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective], - providers: [ - { provide: ObjectUpdatesService, useValue: objectUpdatesService }, - { provide: BundleDataService, useValue: bundleService }, - { provide: ObjectValuesPipe, useValue: objectValuesPipe }, - { provide: RequestService, useValue: requestService }, - { provide: PaginationService, useValue: paginationService } - ], schemas: [ - NO_ERRORS_SCHEMA - ] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(PaginatedDragAndDropBitstreamListComponent); - comp = fixture.componentInstance; - comp.bundle = bundle; - comp.columnSizes = columnSizes; - fixture.detectChanges(); - }); - - it('should initialize the objectsRD$', (done) => { - comp.objectsRD$.pipe(take(1)).subscribe((objects) => { - expect(objects.payload.page).toEqual([bitstream1, bitstream2]); - done(); - }); - }); - - it('should initialize the URL', () => { - expect(comp.url).toEqual(bundle.self); - }); -}); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts deleted file mode 100644 index d5bb9eceea..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { AbstractPaginatedDragAndDropListComponent } from '../../../../../shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component'; -import { Component, ElementRef, Input, OnInit } from '@angular/core'; -import { Bundle } from '../../../../../core/shared/bundle.model'; -import { Bitstream } from '../../../../../core/shared/bitstream.model'; -import { ObjectUpdatesService } from '../../../../../core/data/object-updates/object-updates.service'; -import { BundleDataService } from '../../../../../core/data/bundle-data.service'; -import { switchMap } from 'rxjs/operators'; -import { PaginatedSearchOptions } from '../../../../../shared/search/models/paginated-search-options.model'; -import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { followLink } from '../../../../../shared/utils/follow-link-config.model'; -import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe'; -import { RequestService } from '../../../../../core/data/request.service'; -import { PaginationService } from '../../../../../core/pagination/pagination.service'; -import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; - -@Component({ - selector: 'ds-paginated-drag-and-drop-bitstream-list', - styleUrls: ['../../item-bitstreams.component.scss'], - templateUrl: './paginated-drag-and-drop-bitstream-list.component.html', -}) -/** - * A component listing edit-bitstream rows for each bitstream within the given bundle. - * This component makes use of the AbstractPaginatedDragAndDropListComponent, allowing for users to drag and drop - * bitstreams within the paginated list. To drag and drop a bitstream between two pages, drag the row on top of the - * page number you want the bitstream to end up at. Doing so will add the bitstream to the top of that page. - */ -// NOTE: -// This component was used by the item-edit-bitstream-bundle.component, but this is no longer the case. It is left here -// as a reference for the drag-and-drop functionality. This component (and the abstract version it extends) should be -// removed once this reference is no longer useful. -export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginatedDragAndDropListComponent implements OnInit { - /** - * The bundle to display bitstreams for - */ - @Input() bundle: Bundle; - - /** - * The bootstrap sizes used for the columns within this table - */ - @Input() columnSizes: ResponsiveTableSizes; - - constructor(protected objectUpdatesService: ObjectUpdatesService, - protected elRef: ElementRef, - protected objectValuesPipe: ObjectValuesPipe, - protected bundleService: BundleDataService, - protected paginationService: PaginationService, - protected requestService: RequestService) { - super(objectUpdatesService, elRef, objectValuesPipe, paginationService); - } - - ngOnInit() { - super.ngOnInit(); - } - - /** - * Initialize the bitstreams observable depending on currentPage$ - */ - initializeObjectsRD(): void { - this.objectsRD$ = this.currentPage$.pipe( - switchMap((page: PaginationComponentOptions) => { - const paginatedOptions = new PaginatedSearchOptions({pagination: Object.assign({}, page)}); - return this.bundleService.getBitstreamsEndpoint(this.bundle.id, paginatedOptions).pipe( - switchMap((href) => this.requestService.hasByHref$(href)), - switchMap(() => this.bundleService.getBitstreams( - this.bundle.id, - paginatedOptions, - followLink('format') - )) - ); - }) - ); - } - - /** - * Initialize the URL used for the field-update store, in this case the bundle's self-link - */ - initializeURL(): void { - this.url = this.bundle.self; - } -} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html deleted file mode 100644 index 1bce8667ee..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html +++ /dev/null @@ -1,5 +0,0 @@ - -
- -
-
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts deleted file mode 100644 index e5cb9ba403..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; - -@Component({ - selector: 'ds-item-edit-bitstream-drag-handle', - styleUrls: ['../item-bitstreams.component.scss'], - templateUrl: './item-edit-bitstream-drag-handle.component.html', -}) -/** - * Component displaying a drag handle for the item-edit-bitstream page - * Creates an embedded view of the contents - * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-drag-handle element) - */ -export class ItemEditBitstreamDragHandleComponent implements OnInit { - /** - * The view on the drag-handle - */ - @ViewChild('handleView', {static: true}) handleView; - - constructor(private viewContainerRef: ViewContainerRef) { - } - - ngOnInit(): void { - this.viewContainerRef.createEmbeddedView(this.handleView); - } - -} diff --git a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts deleted file mode 100644 index bac6b89583..0000000000 --- a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { AbstractPaginatedDragAndDropListComponent } from './abstract-paginated-drag-and-drop-list.component'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service'; -import { Component, ElementRef } from '@angular/core'; -import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; -import { PaginatedList } from '../../core/data/paginated-list.model'; -import { RemoteData } from '../../core/data/remote-data'; -import { take } from 'rxjs/operators'; -import { PaginationComponent } from '../pagination/pagination.component'; -import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; -import { createPaginatedList } from '../testing/utils.test'; -import { ObjectValuesPipe } from '../utils/object-values-pipe'; -import { PaginationService } from '../../core/pagination/pagination.service'; -import { PaginationServiceStub } from '../testing/pagination-service.stub'; -import { FieldUpdates } from '../../core/data/object-updates/field-updates.model'; - -@Component({ - selector: 'ds-mock-paginated-drag-drop-abstract', - template: '' -}) -class MockAbstractPaginatedDragAndDropListComponent extends AbstractPaginatedDragAndDropListComponent { - - constructor(protected objectUpdatesService: ObjectUpdatesService, - protected elRef: ElementRef, - protected objectValuesPipe: ObjectValuesPipe, - protected mockUrl: string, - protected paginationService: PaginationService, - protected mockObjectsRD$: Observable>>) { - super(objectUpdatesService, elRef, objectValuesPipe, paginationService); - } - - initializeObjectsRD(): void { - this.objectsRD$ = this.mockObjectsRD$; - } - - initializeURL(): void { - this.url = this.mockUrl; - } -} - -describe('AbstractPaginatedDragAndDropListComponent', () => { - let component: MockAbstractPaginatedDragAndDropListComponent; - let objectUpdatesService: ObjectUpdatesService; - let elRef: ElementRef; - let objectValuesPipe: ObjectValuesPipe; - - const url = 'mock-abstract-paginated-drag-and-drop-list-component'; - - - const object1 = Object.assign(new DSpaceObject(), { uuid: 'object-1' }); - const object2 = Object.assign(new DSpaceObject(), { uuid: 'object-2' }); - const objectsRD = createSuccessfulRemoteDataObject(createPaginatedList([object1, object2])); - let objectsRD$: BehaviorSubject>>; - let paginationService; - - const updates = { - [object1.uuid]: { field: object1, changeType: undefined }, - [object2.uuid]: { field: object2, changeType: undefined } - } as FieldUpdates; - - let paginationComponent: PaginationComponent; - - beforeEach(() => { - objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', { - initialize: {}, - getFieldUpdatesExclusive: observableOf(updates) - }); - elRef = { - nativeElement: jasmine.createSpyObj('nativeElement', { - querySelector: {} - }) - }; - objectValuesPipe = new ObjectValuesPipe(); - paginationComponent = jasmine.createSpyObj('paginationComponent', { - doPageChange: {} - }); - paginationService = new PaginationServiceStub(); - objectsRD$ = new BehaviorSubject(objectsRD); - component = new MockAbstractPaginatedDragAndDropListComponent(objectUpdatesService, elRef, objectValuesPipe, url, paginationService, objectsRD$); - component.paginationComponent = paginationComponent; - component.ngOnInit(); - }); - - it('should call initialize to initialize the objects in the store', () => { - expect(objectUpdatesService.initialize).toHaveBeenCalled(); - }); - - it('should initialize the updates correctly', (done) => { - component.updates$.pipe(take(1)).subscribe((fieldUpdates) => { - expect(fieldUpdates).toEqual(updates); - done(); - }); - }); - - describe('drop', () => { - const event = { - previousIndex: 0, - currentIndex: 1, - item: { element: { nativeElement: { id: object1.uuid } } } - } as any; - - describe('when the user is hovering over a new page', () => { - const hoverPage = 3; - const hoverElement = { textContent: '' + hoverPage }; - - beforeEach(() => { - elRef.nativeElement.querySelector.and.returnValue(hoverElement); - spyOn(component.dropObject, 'emit'); - component.drop(event); - }); - - it('should send out a dropObject event with the expected processed paginated indexes', () => { - expect(component.dropObject.emit).toHaveBeenCalledWith(Object.assign({ - fromIndex: ((component.currentPage$.value.currentPage - 1) * component.pageSize) + event.previousIndex, - toIndex: ((hoverPage - 1) * component.pageSize), - finish: jasmine.anything() - })); - }); - }); - - describe('when the user is not hovering over a new page', () => { - beforeEach(() => { - spyOn(component.dropObject, 'emit'); - component.drop(event); - }); - - it('should send out a dropObject event with the expected properties', () => { - expect(component.dropObject.emit).toHaveBeenCalledWith(Object.assign({ - fromIndex: event.previousIndex, - toIndex: event.currentIndex, - finish: jasmine.anything() - })); - }); - }); - }); -}); diff --git a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts deleted file mode 100644 index 8dba47566f..0000000000 --- a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { RemoteData } from '../../core/data/remote-data'; -import { PaginatedList } from '../../core/data/paginated-list.model'; -import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; -import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service'; -import { distinctUntilChanged, map, switchMap } from 'rxjs/operators'; -import { hasValue } from '../empty.util'; -import { - paginatedListToArray, - getFirstSucceededRemoteData, - getAllSucceededRemoteData -} from '../../core/shared/operators'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; -import { Component, ElementRef, EventEmitter, OnDestroy, Output, ViewChild } from '@angular/core'; -import { PaginationComponent } from '../pagination/pagination.component'; -import { ObjectValuesPipe } from '../utils/object-values-pipe'; -import { compareArraysUsing } from '../../item-page/simple/item-types/shared/item-relationships-utils'; -import { PaginationService } from '../../core/pagination/pagination.service'; -import { FieldUpdate } from '../../core/data/object-updates/field-update.model'; -import { FieldUpdates } from '../../core/data/object-updates/field-updates.model'; - -/** - * Operator used for comparing {@link FieldUpdate}s by their field's UUID - */ -export const compareArraysUsingFieldUuids = () => - compareArraysUsing((fieldUpdate: FieldUpdate) => (hasValue(fieldUpdate) && hasValue(fieldUpdate.field)) ? fieldUpdate.field.uuid : undefined); - -/** - * An abstract component containing general methods and logic to be able to drag and drop objects within a paginated - * list. This implementation supports being able to drag and drop objects between pages. - * Dragging an object on top of a page number will automatically detect the page it's being dropped on and send a - * dropObject event to the parent component containing detailed information about the indexes the object was dropped from - * and to. - * - * To extend this component, it is important to make sure to: - * - Initialize objectsRD$ within the initializeObjectsRD() method - * - Initialize a unique URL for this component/page within the initializeURL() method - * - Add (cdkDropListDropped)="drop($event)" to the cdkDropList element in your template - * - Add (pageChange)="switchPage($event)" to the ds-pagination element in your template - * - Use the updates$ observable for building your list of cdkDrag elements in your template - * - * An example component extending from this abstract component: PaginatedDragAndDropBitstreamListComponent - */ -@Component({ - selector: 'ds-paginated-drag-drop-abstract', - template: '' -}) -export abstract class AbstractPaginatedDragAndDropListComponent implements OnDestroy { - /** - * A view on the child pagination component - */ - @ViewChild(PaginationComponent) paginationComponent: PaginationComponent; - - /** - * Send an event when the user drops an object on the pagination - * The event contains details about the index the object came from and is dropped to (across the entirety of the list, - * not just within a single page) - */ - @Output() dropObject: EventEmitter = new EventEmitter(); - - /** - * The URL to use for accessing the object updates from this list - */ - url: string; - - /** - * The objects to retrieve data for and transform into field updates - */ - objectsRD$: Observable>>; - - /** - * The updates to the current list - */ - updates$: Observable; - - /** - * A list of object UUIDs - * This is the order the objects will be displayed in - */ - customOrder: string[]; - - /** - * The amount of objects to display per page - */ - pageSize = 10; - - /** - * The page options to use for fetching the objects - * Start at page 1 and always use the set page size - */ - options = Object.assign(new PaginationComponentOptions(),{ - id: 'dad', - currentPage: 1, - pageSize: this.pageSize - }); - - /** - * The current page being displayed - */ - currentPage$ = new BehaviorSubject(this.options); - - /** - * Whether or not we should display a loading animation - * This is used to display a loading page when the user drops a bitstream onto a new page. The loading animation - * should stop once the bitstream has moved to the new page and the new page's response has loaded and contains the - * dropped object on top (see this.stopLoadingWhenFirstIs below) - */ - loading$: BehaviorSubject = new BehaviorSubject(false); - - /** - * List of subscriptions - */ - subs: Subscription[] = []; - - protected constructor(protected objectUpdatesService: ObjectUpdatesService, - protected elRef: ElementRef, - protected objectValuesPipe: ObjectValuesPipe, - protected paginationService: PaginationService - ) { - } - - /** - * Initialize the observables - */ - ngOnInit() { - this.initializeObjectsRD(); - this.initializeURL(); - this.initializeUpdates(); - this.initializePagination(); - } - - /** - * Overwrite this method to define how the list of objects is initialized and updated - */ - abstract initializeObjectsRD(): void; - - /** - * Overwrite this method to define how the URL is set - */ - abstract initializeURL(): void; - - /** - * Initialize the current pagination retrieval from the paginationService and push to the currentPage$ - */ - initializePagination() { - this.paginationService.getCurrentPagination(this.options.id, this.options).subscribe((currentPagination) => { - this.currentPage$.next(currentPagination); - }); - } - - /** - * Initialize the field-updates in the store - */ - initializeUpdates(): void { - this.objectsRD$.pipe( - getFirstSucceededRemoteData(), - paginatedListToArray(), - ).subscribe((objects: T[]) => { - this.objectUpdatesService.initialize(this.url, objects, new Date()); - }); - this.updates$ = this.objectsRD$.pipe( - getAllSucceededRemoteData(), - paginatedListToArray(), - switchMap((objects: T[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, objects)) - ); - this.subs.push( - this.updates$.pipe( - map((fieldUpdates) => this.objectValuesPipe.transform(fieldUpdates)), - distinctUntilChanged(compareArraysUsingFieldUuids()) - ).subscribe((updateValues) => { - this.customOrder = updateValues.map((fieldUpdate) => fieldUpdate.field.uuid); - // We received new values, stop displaying a loading indicator if it's present - this.loading$.next(false); - }), - // Disable the pagination when objects are loading - this.loading$.subscribe((loading) => this.options.disabled = loading) - ); - } - - /** - * An object was moved, send updates to the dropObject EventEmitter - * When the object is dropped on a page within the pagination of this component, the object moves to the top of that - * page and the pagination automatically loads and switches the view to that page (this is done by calling the event's - * finish() method after sending patch requests to the REST API) - * @param event - */ - drop(event: CdkDragDrop) { - const dragIndex = event.previousIndex; - let dropIndex = event.currentIndex; - const dragPage = this.currentPage$.value.currentPage - 1; - let dropPage = this.currentPage$.value.currentPage - 1; - - // Check if the user is hovering over any of the pagination's pages at the time of dropping the object - const droppedOnElement = this.elRef.nativeElement.querySelector('.page-item:hover'); - if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent)) { - // The user is hovering over a page, fetch the page's number from the element - const droppedPage = Number(droppedOnElement.textContent); - if (hasValue(droppedPage) && !Number.isNaN(droppedPage)) { - dropPage = droppedPage - 1; - dropIndex = 0; - } - } - - const isNewPage = dragPage !== dropPage; - // Move the object in the custom order array if the drop happened within the same page - // This allows us to instantly display a change in the order, instead of waiting for the REST API's response first - if (!isNewPage && dragIndex !== dropIndex) { - moveItemInArray(this.customOrder, dragIndex, dropIndex); - } - - const redirectPage = dropPage + 1; - const fromIndex = (dragPage * this.pageSize) + dragIndex; - const toIndex = (dropPage * this.pageSize) + dropIndex; - // Send out a drop event (and navigate to the new page) when the "from" and "to" indexes are different from each other - if (fromIndex !== toIndex) { - if (isNewPage) { - this.loading$.next(true); - } - this.dropObject.emit(Object.assign({ - fromIndex, - toIndex, - finish: () => { - if (isNewPage) { - this.paginationComponent.doPageChange(redirectPage); - } - } - })); - } - } - - /** - * unsub all subscriptions - */ - ngOnDestroy(): void { - this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); - this.paginationService.clearPagination(this.options.id); - } -} From 876d94e124cf11477a5548f11f095bc129c4b313 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 2 Oct 2024 09:23:26 +0200 Subject: [PATCH 028/123] 115284: Add tests for isRepeatable --- .../edit-relationship-list.component.spec.ts | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index 312f2936ac..69a2340fd5 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -78,7 +78,7 @@ describe('EditRelationshipListComponent', () => { fixture.detectChanges(); }; - function init(leftType: string, rightType: string): void { + function init(leftType: string, rightType: string, leftMaxCardinality?: number, rightMaxCardinality?: number): void { entityTypeLeft = Object.assign(new ItemType(), { id: leftType, uuid: leftType, @@ -98,6 +98,8 @@ describe('EditRelationshipListComponent', () => { rightType: createSuccessfulRemoteDataObject$(entityTypeRight), leftwardType: `is${rightType}Of${leftType}`, rightwardType: `is${leftType}Of${rightType}`, + leftMaxCardinality: leftMaxCardinality, + rightMaxCardinality: rightMaxCardinality, }); paginationOptions = Object.assign(new PaginationComponentOptions(), { @@ -367,4 +369,31 @@ describe('EditRelationshipListComponent', () => { })); }); }); + + describe('Is repeatable relationship', () => { + beforeEach(waitForAsync(() => { + currentItemIsLeftItem$ = new BehaviorSubject(true); + })); + describe('when max cardinality is 1', () => { + beforeEach(waitForAsync(() => init('Publication', 'OrgUnit', 1, undefined))); + it('should return false', () => { + const result = (comp as any).isRepeatable(); + expect(result).toBeFalse(); + }); + }); + describe('when max cardinality is 2', () => { + beforeEach(waitForAsync(() => init('Publication', 'OrgUnit', 2, undefined))); + it('should return true', () => { + const result = (comp as any).isRepeatable(); + expect(result).toBeTrue(); + }); + }); + describe('when max cardinality is undefined', () => { + beforeEach(waitForAsync(() => init('Publication', 'OrgUnit', undefined, undefined))); + it('should return true', () => { + const result = (comp as any).isRepeatable(); + expect(result).toBeTrue(); + }); + }); + }); }); From b709ee03000086cd52f5de9bc67d5619a64df29d Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Fri, 11 Oct 2024 14:59:34 +0200 Subject: [PATCH 029/123] 118944: Embed the accessStatus link on collection, browse, recent item submissions & the item page related items & delete tab when enabled --- .../collection-page.component.ts | 4 +-- .../breadcrumbs/item-breadcrumb.resolver.ts | 4 +-- src/app/core/browse/browse.service.spec.ts | 12 -------- src/app/core/browse/browse.service.ts | 19 ++++++++----- .../data/relationship-data.service.spec.ts | 4 ++- .../core/data/relationship-data.service.ts | 4 ++- .../recent-item-list.component.ts | 3 ++ .../abstract-item-update.component.ts | 4 +-- .../item-delete/item-delete.component.ts | 4 +-- .../edit-relationship-list.component.spec.ts | 5 ++-- .../edit-relationship-list.component.ts | 2 +- src/app/item-page/item.resolver.ts | 27 +++++++++++------- ...ynamic-form-control-container.component.ts | 2 +- src/app/shared/utils/relation-query.utils.ts | 28 +++++++++++-------- 14 files changed, 67 insertions(+), 55 deletions(-) diff --git a/src/app/collection-page/collection-page.component.ts b/src/app/collection-page/collection-page.component.ts index 16704cef52..e74db09f24 100644 --- a/src/app/collection-page/collection-page.component.ts +++ b/src/app/collection-page/collection-page.component.ts @@ -28,7 +28,7 @@ import { AuthorizationDataService } from '../core/data/feature-authorization/aut import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { getCollectionPageRoute } from './collection-page-routing-paths'; import { redirectOn4xx } from '../core/shared/authorized.operators'; -import { BROWSE_LINKS_TO_FOLLOW } from '../core/browse/browse.service'; +import { getBrowseLinksToFollow } from '../core/browse/browse.service'; import { DSONameService } from '../core/breadcrumbs/dso-name.service'; import { APP_CONFIG, AppConfig } from '../../../src/config/app-config.interface'; @@ -115,7 +115,7 @@ export class CollectionPageComponent implements OnInit { pagination: currentPagination, sort: currentSort, dsoTypes: [DSpaceObjectType.ITEM] - }), null, true, true, ...BROWSE_LINKS_TO_FOLLOW) + }), null, true, true, ...getBrowseLinksToFollow()) .pipe(toDSpaceObjectListRD()) as Observable>>; }), startWith(undefined) // Make sure switching pages shows loading component diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index 3005b6f09a..f396fd08dd 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -4,7 +4,7 @@ import { ItemDataService } from '../data/item-data.service'; import { Item } from '../shared/item.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page/item.resolver'; +import { getItemPageLinksToFollow } from '../../item-page/item.resolver'; /** * The class that resolves the BreadcrumbConfig object for an Item @@ -23,6 +23,6 @@ export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver { * Requesting them as embeds will limit the number of requests */ get followLinks(): FollowLinkConfig[] { - return ITEM_PAGE_LINKS_TO_FOLLOW; + return getItemPageLinksToFollow(); } } diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 9f166e3d19..f7027fc91e 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -1,10 +1,8 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestService } from '../data/request.service'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; import { BrowseService } from './browse.service'; @@ -20,7 +18,6 @@ describe('BrowseService', () => { let scheduler: TestScheduler; let service: BrowseService; let requestService: RequestService; - let rdbService: RemoteDataBuildService; const browsesEndpointURL = 'https://rest.api/browses'; const halService: any = new HALEndpointServiceStub(browsesEndpointURL); @@ -118,7 +115,6 @@ describe('BrowseService', () => { halService, browseDefinitionDataService, hrefOnlyDataService, - rdbService ); } @@ -130,11 +126,9 @@ describe('BrowseService', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(halService, 'getEndpoint').and .returnValue(hot('--a-', { a: browsesEndpointURL })); - spyOn(rdbService, 'buildList').and.callThrough(); }); it('should call BrowseDefinitionDataService to create the RemoteData Observable', () => { @@ -151,9 +145,7 @@ describe('BrowseService', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); - spyOn(rdbService, 'buildList').and.callThrough(); }); describe('when getBrowseEntriesFor is called with a valid browse definition id', () => { @@ -204,7 +196,6 @@ describe('BrowseService', () => { describe('if getBrowseDefinitions fires', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('--a-', { @@ -259,7 +250,6 @@ describe('BrowseService', () => { describe('if getBrowseDefinitions doesn\'t fire', () => { it('should return undefined', () => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('----')); @@ -277,9 +267,7 @@ describe('BrowseService', () => { describe('getFirstItemFor', () => { beforeEach(() => { requestService = getMockRequestService(); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); - spyOn(rdbService, 'buildList').and.callThrough(); }); describe('when getFirstItemFor is called with a valid browse definition id', () => { diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index b210b34949..2d8dd4b80f 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../shared/empty.util'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { PaginatedList } from '../data/paginated-list.model'; import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; @@ -24,11 +23,18 @@ import { HrefOnlyDataService } from '../data/href-only-data.service'; import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { BrowseDefinitionDataService } from './browse-definition-data.service'; import { SortDirection } from '../cache/models/sort-options.model'; +import { environment } from '../../../environments/environment'; -export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ - followLink('thumbnail') -]; +export function getBrowseLinksToFollow(): FollowLinkConfig[] { + const followLinks = [ + followLink('thumbnail'), + ]; + if (environment.item.showAccessStatuses) { + followLinks.push(followLink('accessStatus')); + } + return followLinks; +} /** * The service handling all browse requests @@ -55,7 +61,6 @@ export class BrowseService { protected halService: HALEndpointService, private browseDefinitionDataService: BrowseDefinitionDataService, private hrefOnlyDataService: HrefOnlyDataService, - private rdb: RemoteDataBuildService, ) { } @@ -105,7 +110,7 @@ export class BrowseService { }) ); if (options.fetchThumbnail ) { - return this.hrefOnlyDataService.findListByHref(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, null, null, ...getBrowseLinksToFollow()); } return this.hrefOnlyDataService.findListByHref(href$); } @@ -153,7 +158,7 @@ export class BrowseService { }), ); if (options.fetchThumbnail) { - return this.hrefOnlyDataService.findListByHref(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, null, null, ...getBrowseLinksToFollow()); } return this.hrefOnlyDataService.findListByHref(href$); } diff --git a/src/app/core/data/relationship-data.service.spec.ts b/src/app/core/data/relationship-data.service.spec.ts index 4432d5213a..fd24c0665b 100644 --- a/src/app/core/data/relationship-data.service.spec.ts +++ b/src/app/core/data/relationship-data.service.spec.ts @@ -23,6 +23,7 @@ import { FindListOptions } from './find-list-options.model'; import { testSearchDataImplementation } from './base/search-data.spec'; import { MetadataValue } from '../shared/metadata.models'; import { MetadataRepresentationType } from '../shared/metadata-representation/metadata-representation.model'; +import { environment } from '../../../environments/environment.test'; describe('RelationshipDataService', () => { let service: RelationshipDataService; @@ -137,6 +138,7 @@ describe('RelationshipDataService', () => { itemService, null, jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v), + environment, ); } @@ -152,7 +154,7 @@ describe('RelationshipDataService', () => { }); describe('composition', () => { - const initService = () => new RelationshipDataService(null, null, null, null, null, null, null); + const initService = () => new RelationshipDataService(null, null, null, null, null, null, null, environment); testSearchDataImplementation(initService); }); diff --git a/src/app/core/data/relationship-data.service.ts b/src/app/core/data/relationship-data.service.ts index 46a51a2d01..7620f8ab75 100644 --- a/src/app/core/data/relationship-data.service.ts +++ b/src/app/core/data/relationship-data.service.ts @@ -51,6 +51,7 @@ import { MetadataRepresentation } from '../shared/metadata-representation/metada import { MetadatumRepresentation } from '../shared/metadata-representation/metadatum/metadatum-representation.model'; import { ItemMetadataRepresentation } from '../shared/metadata-representation/item/item-metadata-representation.model'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; const relationshipListsStateSelector = (state: AppState) => state.relationshipLists; @@ -92,6 +93,7 @@ export class RelationshipDataService extends IdentifiableDataService, @Inject(PAGINATED_RELATIONS_TO_ITEMS_OPERATOR) private paginatedRelationsToItems: (thisId: string) => (source: Observable>>) => Observable>>, + @Inject(APP_CONFIG) private appConfig: AppConfig, ) { super('relationships', requestService, rdbService, objectCache, halService, 15 * 60 * 1000); @@ -264,7 +266,7 @@ export class RelationshipDataService extends IdentifiableDataService>> { - let linksToFollow: FollowLinkConfig[] = itemLinksToFollow(options.fetchThumbnail); + let linksToFollow: FollowLinkConfig[] = itemLinksToFollow(options.fetchThumbnail, this.appConfig.item.showAccessStatuses); linksToFollow.push(followLink('relationshipType')); return this.getItemRelationshipsByLabel(item, label, options, true, true, ...linksToFollow).pipe(this.paginatedRelationsToItems(item.uuid)); diff --git a/src/app/home-page/recent-item-list/recent-item-list.component.ts b/src/app/home-page/recent-item-list/recent-item-list.component.ts index 9a8535d970..da7d8141e3 100644 --- a/src/app/home-page/recent-item-list/recent-item-list.component.ts +++ b/src/app/home-page/recent-item-list/recent-item-list.component.ts @@ -64,6 +64,9 @@ export class RecentItemListComponent implements OnInit { if (this.appConfig.browseBy.showThumbnails) { linksToFollow.push(followLink('thumbnail')); } + if (this.appConfig.item.showAccessStatuses) { + linksToFollow.push(followLink('accessStatus')); + } this.itemRD$ = this.searchService.search( new PaginatedSearchOptions({ diff --git a/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index 80002f614b..3a07632346 100644 --- a/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -13,7 +13,7 @@ import { environment } from '../../../../environments/environment'; import { getItemPageRoute } from '../../item-page-routing-paths'; import { getAllSucceededRemoteData } from '../../../core/shared/operators'; import { hasValue } from '../../../shared/empty.util'; -import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item.resolver'; +import { getItemPageLinksToFollow } from '../../item.resolver'; import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model'; @@ -72,7 +72,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl this.item = rd.payload; }), switchMap((rd: RemoteData) => { - return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...ITEM_PAGE_LINKS_TO_FOLLOW); + return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...getItemPageLinksToFollow()); }), getAllSucceededRemoteData() ).subscribe((rd: RemoteData) => { diff --git a/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts b/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts index 9012ebe7d7..8b26b71fef 100644 --- a/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts +++ b/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts @@ -236,8 +236,8 @@ export class ItemDeleteComponent this.linkService.resolveLinks( relationship, followLink('relationshipType'), - followLink('leftItem'), - followLink('rightItem'), + followLink('leftItem', undefined, followLink('accessStatus')), + followLink('rightItem', undefined, followLink('accessStatus')), ); return relationship.relationshipType.pipe( getFirstSucceededRemoteData(), diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index 4cd663f0fb..29851c9e93 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -32,6 +32,7 @@ import { ConfigurationProperty } from '../../../../core/shared/configuration-pro import { Router } from '@angular/router'; import { RouterMock } from '../../../../shared/mocks/router.mock'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; +import { environment } from '../../../../../environments/environment.test'; let comp: EditRelationshipListComponent; let fixture: ComponentFixture; @@ -202,11 +203,11 @@ describe('EditRelationshipListComponent', () => { })) }); - const environmentUseThumbs = { + const environmentUseThumbs = Object.assign({}, environment, { browseBy: { showThumbnails: true } - }; + }); TestBed.configureTestingModule({ imports: [SharedModule, TranslateModule.forRoot()], diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index b8542f5806..b27e57f773 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -493,7 +493,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { ); // this adds thumbnail images when required by configuration - let linksToFollow: FollowLinkConfig[] = itemLinksToFollow(this.fetchThumbnail); + let linksToFollow: FollowLinkConfig[] = itemLinksToFollow(this.fetchThumbnail, this.appConfig.item.showAccessStatuses); this.subs.push( observableCombineLatest([ diff --git a/src/app/item-page/item.resolver.ts b/src/app/item-page/item.resolver.ts index ca6a6c5958..ffeef57ecb 100644 --- a/src/app/item-page/item.resolver.ts +++ b/src/app/item-page/item.resolver.ts @@ -8,20 +8,27 @@ import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { Store } from '@ngrx/store'; import { ResolvedAction } from '../core/resolving/resolver.actions'; +import { environment } from '../../environments/environment'; /** * The self links defined in this list are expected to be requested somewhere in the near future * Requesting them as embeds will limit the number of requests */ -export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ - followLink('owningCollection', {}, - followLink('parentCommunity', {}, - followLink('parentCommunity')) - ), - followLink('relationships'), - followLink('version', {}, followLink('versionhistory')), - followLink('thumbnail') -]; +export function getItemPageLinksToFollow(): FollowLinkConfig[] { + const followLinks: FollowLinkConfig[] = [ + followLink('owningCollection', {}, + followLink('parentCommunity', {}, + followLink('parentCommunity')) + ), + followLink('relationships'), + followLink('version', {}, followLink('versionhistory')), + followLink('thumbnail'), + ]; + if (environment.item.showAccessStatuses) { + followLinks.push(followLink('accessStatus')); + } + return followLinks; +} /** * This class represents a resolver that requests a specific item before the route is activated @@ -46,7 +53,7 @@ export class ItemResolver implements Resolve> { const itemRD$ = this.itemService.findById(route.params.id, true, false, - ...ITEM_PAGE_LINKS_TO_FOLLOW + ...getItemPageLinksToFollow(), ).pipe( getFirstCompletedRemoteData(), ); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index ff5a119b6f..a04c10bf3a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -328,7 +328,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo const relationship$ = this.relationshipService.findById(this.value.virtualValue, true, true, - ... itemLinksToFollow(this.fetchThumbnail)).pipe( + ... itemLinksToFollow(this.fetchThumbnail, this.appConfig.item.showAccessStatuses)).pipe( getAllSucceededRemoteData(), getRemoteDataPayload()); this.relationshipValue$ = observableCombineLatest([this.item$.pipe(take(1)), relationship$]).pipe( diff --git a/src/app/shared/utils/relation-query.utils.ts b/src/app/shared/utils/relation-query.utils.ts index 62a69075fc..635ae946da 100644 --- a/src/app/shared/utils/relation-query.utils.ts +++ b/src/app/shared/utils/relation-query.utils.ts @@ -1,5 +1,6 @@ import { followLink, FollowLinkConfig } from './follow-link-config.model'; import { Relationship } from '../../core/shared/item-relationships/relationship.model'; +import { Item } from '../../core/shared/item.model'; /** * Get the query for looking up items by relation type @@ -21,19 +22,22 @@ export function getFilterByRelation(relationType: string, itemUUID: string): str } /** - * Creates links to follow for the leftItem and rightItem. Links will include - * @param showThumbnail thumbnail image configuration - * @returns followLink array + * Creates links to follow for the leftItem and rightItem. Optionally additional links for `thumbnail` & `accessStatus` + * can be embedded as well. + * + * @param showThumbnail Whether the `thumbnail` needs to be embedded on the {@link Item} + * @param showAccessStatus Whether the `accessStatus` needs to be embedded on the {@link Item} */ -export function itemLinksToFollow(showThumbnail: boolean): FollowLinkConfig[] { - let linksToFollow: FollowLinkConfig[]; +export function itemLinksToFollow(showThumbnail: boolean, showAccessStatus: boolean): FollowLinkConfig[] { + const conditionalLinksToFollow: FollowLinkConfig[] = []; if (showThumbnail) { - linksToFollow = [ - followLink('leftItem',{}, followLink('thumbnail')), - followLink('rightItem',{}, followLink('thumbnail')) - ]; - } else { - linksToFollow = [followLink('leftItem'), followLink('rightItem')]; + conditionalLinksToFollow.push(followLink('thumbnail')); } - return linksToFollow; + if (showAccessStatus) { + conditionalLinksToFollow.push(followLink('accessStatus')); + } + return [ + followLink('leftItem', undefined, ...conditionalLinksToFollow), + followLink('rightItem', undefined, ...conditionalLinksToFollow), + ]; } From d674bcc3908c4cf8469853778fc167295320a51c Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Mon, 16 Sep 2024 15:47:42 +0200 Subject: [PATCH 030/123] 118220: Integrate live-region component into item-edit-bitsream page --- .../item-edit-bitstream-bundle.component.html | 2 +- .../item-edit-bitstream-bundle.component.ts | 21 ++++++++++++------- src/assets/i18n/en.json5 | 4 ++++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index 0338055df5..d530fb38d1 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -68,7 +68,7 @@
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index bd99fc1a09..24319f4a83 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -30,6 +30,8 @@ import { RequestService } from '../../../../core/data/request.service'; import { ItemBitstreamsService } from '../item-bitstreams.service'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { hasValue } from '../../../../shared/empty.util'; +import { LiveRegionService } from '../../../../shared/live-region/live-region.service'; +import { TranslateService } from '@ngx-translate/core'; /** * Interface storing all the information necessary to create a row in the bitstream edit table @@ -166,11 +168,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ pageSize$: BehaviorSubject; - /** - * Whether the table has multiple pages - */ - hasMultiplePages = false; - /** * The self url of the bundle, also used when retrieving fieldUpdates */ @@ -190,6 +187,8 @@ export class ItemEditBitstreamBundleComponent implements OnInit { protected paginationService: PaginationService, protected requestService: RequestService, protected itemBitstreamsService: ItemBitstreamsService, + protected liveRegionService: LiveRegionService, + protected translateService: TranslateService, ) { } @@ -301,7 +300,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.paginationComponent.doPageSizeChange(pageSize); } - dragStart() { + dragStart(bitstreamName: string) { // Only open the drag tooltip when there are multiple pages this.paginationComponent.shouldShowBottomPager.pipe( take(1), @@ -309,10 +308,18 @@ export class ItemEditBitstreamBundleComponent implements OnInit { ).subscribe(() => { this.dragTooltip.open(); }); + + const message = this.translateService.instant('item.edit.bitstreams.edit.live.drag', + { bitstream: bitstreamName }); + this.liveRegionService.addMessage(message); } - dragEnd() { + dragEnd(bitstreamName: string) { this.dragTooltip.close(); + + const message = this.translateService.instant('item.edit.bitstreams.edit.live.drop', + { bitstream: bitstreamName }); + this.liveRegionService.addMessage(message); } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index f2dbf9b2d1..48fd581bdf 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1950,6 +1950,10 @@ "item.edit.bitstreams.edit.buttons.undo": "Undo changes", + "item.edit.bitstreams.edit.live.drag": "{{ bitstream }} grabbed", + + "item.edit.bitstreams.edit.live.drop": "{{ bitstream }} dropped", + "item.edit.bitstreams.empty": "This item doesn't contain any bitstreams. Click the upload button to create one.", "item.edit.bitstreams.headers.actions": "Actions", From 1f909dc6ea0d84859fbeef690c9bd212417c7a99 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 17 Sep 2024 13:34:08 +0200 Subject: [PATCH 031/123] 118223: Add instructive alert to item-bitstreams edit page --- .../item-bitstreams/item-bitstreams.component.html | 7 ++++++- .../item-bitstreams/item-bitstreams.component.ts | 4 ++++ src/assets/i18n/en.json5 | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index 3527f2f5b8..1c13154bfa 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -1,4 +1,8 @@
+
+ +
+
Date: Wed, 18 Sep 2024 11:57:41 +0200 Subject: [PATCH 032/123] 118223: Implement bitstream reordering with keyboard --- .../item-bitstreams.component.html | 1 - .../item-bitstreams.component.spec.ts | 43 +- .../item-bitstreams.component.ts | 87 ++-- .../item-bitstreams.service.spec.ts | 19 + .../item-bitstreams.service.ts | 303 ++++++++++++- .../item-edit-bitstream-bundle.component.html | 33 +- ...em-edit-bitstream-bundle.component.spec.ts | 1 + .../item-edit-bitstream-bundle.component.ts | 405 ++++++++++++------ .../live-region/live-region.service.stub.ts | 30 ++ src/assets/i18n/en.json5 | 8 +- 10 files changed, 727 insertions(+), 203 deletions(-) create mode 100644 src/app/shared/live-region/live-region.service.stub.ts diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index 1c13154bfa..b9af2a7d18 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -33,7 +33,6 @@ [item]="item" [columnSizes]="columnSizes" [isFirstTable]="isFirst" - (dropObject)="dropBitstream(bundle, $event)" aria-describedby="reorder-description">
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index 6ce7394473..a5549a6ba0 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -25,6 +25,10 @@ import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } f import { createPaginatedList } from '../../../shared/testing/utils.test'; import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub'; +import { ItemBitstreamsService } from './item-bitstreams.service'; +import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; let comp: ItemBitstreamsComponent; let fixture: ComponentFixture; @@ -76,6 +80,7 @@ let objectCache: ObjectCacheService; let requestService: RequestService; let searchConfig: SearchConfigurationService; let bundleService: BundleDataService; +let itemBitstreamsService: ItemBitstreamsService; describe('ItemBitstreamsComponent', () => { beforeEach(waitForAsync(() => { @@ -147,6 +152,19 @@ describe('ItemBitstreamsComponent', () => { patch: createSuccessfulRemoteDataObject$({}), }); + itemBitstreamsService = jasmine.createSpyObj('itemBitstreamsService', { + getColumnSizes: new ResponsiveTableSizes([ + new ResponsiveColumnSizes(2, 2, 3, 4, 4), + new ResponsiveColumnSizes(2, 3, 3, 3, 3), + new ResponsiveColumnSizes(2, 2, 2, 2, 2), + new ResponsiveColumnSizes(6, 5, 4, 3, 3) + ]), + getSelectedBitstream$: observableOf({}), + getInitialBundlesPaginationOptions: new PaginationComponentOptions(), + removeMarkedBitstreams: createSuccessfulRemoteDataObject$({}), + displayNotifications: undefined, + }); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], declarations: [ItemBitstreamsComponent, ObjectValuesPipe, VarDirective], @@ -161,6 +179,7 @@ describe('ItemBitstreamsComponent', () => { { provide: RequestService, useValue: requestService }, { provide: SearchConfigurationService, useValue: searchConfig }, { provide: BundleDataService, useValue: bundleService }, + { provide: ItemBitstreamsService, useValue: itemBitstreamsService }, ChangeDetectorRef ], schemas: [ NO_ERRORS_SCHEMA @@ -181,28 +200,8 @@ describe('ItemBitstreamsComponent', () => { comp.submit(); }); - it('should call removeMultiple on the bitstreamService for the marked field', () => { - expect(bitstreamService.removeMultiple).toHaveBeenCalledWith([bitstream2]); - }); - - it('should not call removeMultiple on the bitstreamService for the unmarked field', () => { - expect(bitstreamService.removeMultiple).not.toHaveBeenCalledWith([bitstream1]); - }); - }); - - describe('when dropBitstream is called', () => { - beforeEach((done) => { - comp.dropBitstream(bundle, { - fromIndex: 0, - toIndex: 50, - finish: () => { - done(); - } - }); - }); - - it('should send out a patch for the move operation', () => { - expect(bundleService.patch).toHaveBeenCalled(); + it('should call removeMarkedBitstreams on the itemBitstreamsService', () => { + expect(itemBitstreamsService.removeMarkedBitstreams).toHaveBeenCalled(); }); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 7757170f4e..4ced3dd649 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core'; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, HostListener } from '@angular/core'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { map, switchMap, take } from 'rxjs/operators'; import { Observable, Subscription, zip as observableZip } from 'rxjs'; @@ -8,13 +8,11 @@ import { ActivatedRoute, Router } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; -import { hasValue } from '../../../shared/empty.util'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { RequestService } from '../../../core/data/request.service'; import { getFirstSucceededRemoteData, getRemoteDataPayload, - getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; @@ -23,7 +21,6 @@ import { BundleDataService } from '../../../core/data/bundle-data.service'; import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; import { NoContent } from '../../../core/shared/NoContent.model'; -import { Operation } from 'fast-json-patch'; import { ItemBitstreamsService } from './item-bitstreams.service'; import { AlertType } from '../../../shared/alert/aletr-type'; @@ -88,13 +85,63 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme postItemInit(): void { const bundlesOptions = this.itemBitstreamsService.getInitialBundlesPaginationOptions(); - this. bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: bundlesOptions})).pipe( + this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: bundlesOptions})).pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), map((bundlePage: PaginatedList) => bundlePage.page) ); } + /** + * Handles keyboard events that should move the currently selected bitstream up + */ + @HostListener('document:keydown.arrowUp', ['$event']) + moveUp(event: KeyboardEvent) { + if (this.itemBitstreamsService.hasSelectedBitstream()) { + event.preventDefault(); + this.itemBitstreamsService.moveSelectedBitstreamUp(); + } + } + + /** + * Handles keyboard events that should move the currently selected bitstream down + */ + @HostListener('document:keydown.arrowDown', ['$event']) + moveDown(event: KeyboardEvent) { + if (this.itemBitstreamsService.hasSelectedBitstream()) { + event.preventDefault(); + this.itemBitstreamsService.moveSelectedBitstreamDown(); + } + } + + /** + * Handles keyboard events that should cancel the currently selected bitstream. + * A cancel means that the selected bitstream is returned to its original position and is no longer selected. + * @param event + */ + @HostListener('document:keyup.escape', ['$event']) + cancelSelection(event: KeyboardEvent) { + if (this.itemBitstreamsService.hasSelectedBitstream()) { + event.preventDefault(); + this.itemBitstreamsService.cancelSelection(); + } + } + + /** + * Handles keyboard events that should clear the currently selected bitstream. + * A clear means that the selected bitstream remains in its current position but is no longer selected. + */ + @HostListener('document:keydown.enter', ['$event']) + @HostListener('document:keydown.space', ['$event']) + clearSelection(event: KeyboardEvent) { + // Only when no specific element is in focus do we want to clear the currently selected bitstream + // Otherwise we might clear the selection when a different action was intended, e.g. clicking a button or selecting + // a different bitstream. + if (event.target instanceof Element && event.target.tagName === 'BODY') { + this.itemBitstreamsService.clearSelection(); + } + } + /** * Initialize the notification messages prefix */ @@ -120,36 +167,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme }); } - /** - * A bitstream was dropped in a new location. Send out a Move Patch request to the REST API, display notifications, - * refresh the bundle's cache (so the lists can properly reload) and call the event's callback function (which will - * navigate the user to the correct page) - * @param bundle The bundle to send patch requests to - * @param event The event containing the index the bitstream came from and was dropped to - */ - dropBitstream(bundle: Bundle, event: any) { - this.zone.runOutsideAngular(() => { - if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) { - const moveOperation = { - op: 'move', - from: `/_links/bitstreams/${event.fromIndex}/href`, - path: `/_links/bitstreams/${event.toIndex}/href` - } as Operation; - this.bundleService.patch(bundle, [moveOperation]).pipe( - getFirstCompletedRemoteData(), - ).subscribe((response: RemoteData) => { - this.zone.run(() => { - this.itemBitstreamsService.displayNotifications('item.edit.bitstreams.notifications.move', [response]); - // Remove all cached requests from this bundle and call the event's callback when the requests are cleared - this.requestService.setStaleByHrefSubstring(bundle.self).pipe( - take(1) - ).subscribe(() => event.finish()); - }); - }); - } - }); - } - /** * Request the object updates service to discard all current changes to this item * Shows a notification to remind the user that they can undo this diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts index 89ecfb518f..e144e81ec7 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts @@ -16,6 +16,12 @@ import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { BundleDataService } from '../../../core/data/bundle-data.service'; +import { RequestService } from '../../../core/data/request.service'; +import { LiveRegionService } from '../../../shared/live-region/live-region.service'; +import { Bundle } from '../../../core/shared/bundle.model'; +import { of } from 'rxjs'; +import { getLiveRegionServiceStub } from '../../../shared/live-region/live-region.service.stub'; describe('ItemBitstreamsService', () => { let service: ItemBitstreamsService; @@ -23,21 +29,34 @@ describe('ItemBitstreamsService', () => { let translateService: TranslateService; let objectUpdatesService: ObjectUpdatesService; let bitstreamDataService: BitstreamDataService; + let bundleDataService: BundleDataService; let dsoNameService: DSONameService; + let requestService: RequestService; + let liveRegionService: LiveRegionService; beforeEach(() => { notificationsService = new NotificationsServiceStub() as any; translateService = getMockTranslateService(); objectUpdatesService = new ObjectUpdatesServiceStub() as any; bitstreamDataService = new BitstreamDataServiceStub() as any; + bundleDataService = jasmine.createSpyObj('bundleDataService', { + patch: createSuccessfulRemoteDataObject$(new Bundle()), + }); dsoNameService = new DSONameServiceMock() as any; + requestService = jasmine.createSpyObj('requestService', { + setStaleByHrefSubstring: of(true), + }); + liveRegionService = getLiveRegionServiceStub(); service = new ItemBitstreamsService( notificationsService, translateService, objectUpdatesService, bitstreamDataService, + bundleDataService, dsoNameService, + requestService, + liveRegionService, ); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts index 487df77b28..21dc415198 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -3,38 +3,277 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; import { RemoteData } from '../../../core/data/remote-data'; -import { isNotEmpty, hasValue } from '../../../shared/empty.util'; +import { isNotEmpty, hasValue, hasNoValue } from '../../../shared/empty.util'; import { Bundle } from '../../../core/shared/bundle.model'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; -import { Observable, zip as observableZip } from 'rxjs'; +import { Observable, zip as observableZip, BehaviorSubject } from 'rxjs'; import { NoContent } from '../../../core/shared/NoContent.model'; -import { take, switchMap, map } from 'rxjs/operators'; +import { take, switchMap, map, tap } from 'rxjs/operators'; import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model'; import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; import { Bitstream } from '../../../core/shared/bitstream.model'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; -import { BitstreamTableEntry } from './item-edit-bitstream-bundle/item-edit-bitstream-bundle.component'; -import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { getFirstSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getBitstreamDownloadRoute } from '../../../app-routing-paths'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; +import { MoveOperation } from 'fast-json-patch'; +import { BundleDataService } from '../../../core/data/bundle-data.service'; +import { RequestService } from '../../../core/data/request.service'; +import { LiveRegionService } from '../../../shared/live-region/live-region.service'; +/** + * Interface storing all the information necessary to create a row in the bitstream edit table + */ +export interface BitstreamTableEntry { + /** + * The bitstream + */ + bitstream: Bitstream, + /** + * The uuid of the Bitstream + */ + id: string, + /** + * The name of the Bitstream + */ + name: string, + /** + * The name of the Bitstream with all whitespace removed + */ + nameStripped: string, + /** + * The description of the Bitstream + */ + description: string, + /** + * Observable emitting the Format of the Bitstream + */ + format: Observable, + /** + * The download url of the Bitstream + */ + downloadUrl: string, +} + +/** + * Interface storing information necessary to highlight and reorder the selected bitstream entry + */ +export interface SelectedBitstreamTableEntry { + /** + * The selected entry + */ + bitstream: BitstreamTableEntry, + /** + * The bundle the bitstream belongs to + */ + bundle: Bundle, + /** + * The total number of bitstreams in the bundle + */ + bundleSize: number, + /** + * The original position of the bitstream within the bundle. + */ + originalPosition: number, + /** + * The current position of the bitstream within the bundle. + */ + currentPosition: number, +} + +/** + * This service handles the selection and updating of the bitstreams and their order on the + * 'Edit Item' -> 'Bitstreams' page. + */ @Injectable( { providedIn: 'root' }, ) export class ItemBitstreamsService { + /** + * BehaviorSubject which emits every time the selected bitstream changes. + */ + protected selectedBitstream$: BehaviorSubject = new BehaviorSubject(null); + + protected isPerformingMoveRequest = false; + constructor( protected notificationsService: NotificationsService, protected translateService: TranslateService, protected objectUpdatesService: ObjectUpdatesService, protected bitstreamService: BitstreamDataService, + protected bundleService: BundleDataService, protected dsoNameService: DSONameService, + protected requestService: RequestService, + protected liveRegionService: LiveRegionService, ) { } + /** + * Returns the observable emitting the currently selected bitstream + */ + getSelectedBitstream$(): Observable { + return this.selectedBitstream$; + } + + /** + * Returns a copy of the currently selected bitstream + */ + getSelectedBitstream(): SelectedBitstreamTableEntry { + const selected = this.selectedBitstream$.getValue(); + + if (hasNoValue(selected)) { + return selected; + } + + return Object.assign({}, selected); + } + + hasSelectedBitstream(): boolean { + return hasValue(this.getSelectedBitstream()); + } + + /** + * Select the provided entry + */ + selectBitstreamEntry(entry: SelectedBitstreamTableEntry) { + if (entry !== this.selectedBitstream$.getValue()) { + this.announceSelect(entry.bitstream.name); + this.updateSelectedBitstream(entry); + } + } + + /** + * Makes the {@link selectedBitstream$} observable emit the provided {@link SelectedBitstreamTableEntry}. + * @protected + */ + protected updateSelectedBitstream(entry: SelectedBitstreamTableEntry) { + this.selectedBitstream$.next(entry); + } + + /** + * Unselects the selected bitstream. Does nothing if no bitstream is selected. + */ + clearSelection() { + const selected = this.getSelectedBitstream(); + + if (hasValue(selected)) { + this.updateSelectedBitstream(null); + this.announceClear(selected.bitstream.name); + } + } + + /** + * Returns the currently selected bitstream to its original position and unselects the bitstream. + * Does nothing if no bitstream is selected. + */ + cancelSelection() { + const selected = this.getSelectedBitstream(); + + if (hasNoValue(selected) || this.isPerformingMoveRequest) { + return; + } + + this.selectedBitstream$.next(null); + + const originalPosition = selected.originalPosition; + const currentPosition = selected.currentPosition; + + // If the selected bitstream has not moved, there is no need to return it to its original position + if (currentPosition === originalPosition) { + this.announceClear(selected.bitstream.name); + } else { + this.announceCancel(selected.bitstream.name, originalPosition); + this.performBitstreamMoveRequest(selected.bundle, currentPosition, originalPosition); + } + } + + /** + * Moves the selected bitstream one position up in the bundle. Does nothing if no bitstream is selected or the + * selected bitstream already is at the beginning of the bundle. + */ + moveSelectedBitstreamUp() { + const selected = this.getSelectedBitstream(); + + if (hasNoValue(selected) || this.isPerformingMoveRequest) { + return; + } + + const originalPosition = selected.currentPosition; + if (originalPosition > 0) { + const newPosition = originalPosition - 1; + selected.currentPosition = newPosition; + + const onRequestCompleted = () => { + this.announceMove(selected.bitstream.name, newPosition); + }; + + this.performBitstreamMoveRequest(selected.bundle, originalPosition, newPosition, onRequestCompleted); + this.updateSelectedBitstream(selected); + } + } + + /** + * Moves the selected bitstream one position down in the bundle. Does nothing if no bitstream is selected or the + * selected bitstream already is at the end of the bundle. + */ + moveSelectedBitstreamDown() { + const selected = this.getSelectedBitstream(); + + if (hasNoValue(selected) || this.isPerformingMoveRequest) { + return; + } + + const originalPosition = selected.currentPosition; + if (originalPosition < selected.bundleSize - 1) { + const newPosition = originalPosition + 1; + selected.currentPosition = newPosition; + + const onRequestCompleted = () => { + this.announceMove(selected.bitstream.name, newPosition); + }; + + this.performBitstreamMoveRequest(selected.bundle, originalPosition, newPosition, onRequestCompleted); + this.updateSelectedBitstream(selected); + } + } + + /** + * Sends out a Move Patch request to the REST API, display notifications, + * refresh the bundle's cache (so the lists can properly reload) + * @param bundle The bundle to send patch requests to + * @param fromIndex The index to move from + * @param toIndex The index to move to + * @param finish Optional: Function to execute once the response has been received + */ + performBitstreamMoveRequest(bundle: Bundle, fromIndex: number, toIndex: number, finish?: () => void) { + if (this.isPerformingMoveRequest) { + console.warn('Attempted to perform move request while previous request has not completed yet'); + return; + } + + const moveOperation: MoveOperation = { + op: 'move', + from: `/_links/bitstreams/${fromIndex}/href`, + path: `/_links/bitstreams/${toIndex}/href`, + }; + + this.isPerformingMoveRequest = true; + this.bundleService.patch(bundle, [moveOperation]).pipe( + getFirstCompletedRemoteData(), + tap((response: RemoteData) => this.displayNotifications('item.edit.bitstreams.notifications.move', [response])), + switchMap(() => this.requestService.setStaleByHrefSubstring(bundle.self)), + take(1), + ).subscribe(() => { + this.isPerformingMoveRequest = false; + finish?.(); + }); + } + /** * Returns the pagination options to use when fetching the bundles */ @@ -46,6 +285,10 @@ export class ItemBitstreamsService { }); } + /** + * Returns the initial pagination options to use when fetching the bitstreams + * @param bundleName The name of the bundle, will be as pagination id. + */ getInitialBitstreamsPaginationOptions(bundleName: string): PaginationComponentOptions { return Object.assign(new PaginationComponentOptions(),{ id: bundleName, // This might behave unexpectedly if the item contains two bundles with the same name @@ -118,6 +361,10 @@ export class ItemBitstreamsService { ); } + /** + * Creates an array of {@link BitstreamTableEntry}s from an array of {@link Bitstream}s + * @param bitstreams The bitstreams array to map to table entries + */ mapBitstreamsToTableEntries(bitstreams: Bitstream[]): BitstreamTableEntry[] { return bitstreams.map((bitstream) => { const name = this.dsoNameService.getName(bitstream); @@ -143,7 +390,7 @@ export class ItemBitstreamsService { // To make it clear which headers are relevant for a specific field in the table, the 'headers' attribute is used to // refer to specific headers. The Bitstream's name is used as header ID for the row containing information regarding // that bitstream. As the 'headers' attribute contains a space-separated string of header IDs, the Bitstream's header - // ID can not contain strings itself. + // ID can not contain spaces itself. return this.stripWhiteSpace(name); } @@ -155,4 +402,48 @@ export class ItemBitstreamsService { // '/\s+/g' matches all occurrences of any amount of whitespace characters return str.replace(/\s+/g, ''); } + + /** + * Adds a message to the live region mentioning that the bitstream with the provided name was selected. + * @param bitstreamName The name of the bitstream that was selected. + */ + announceSelect(bitstreamName: string) { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.select', + { bitstream: bitstreamName }); + this.liveRegionService.addMessage(message); + } + + /** + * Adds a message to the live region mentioning that the bitstream with the provided name was moved to the provided + * position. + * @param bitstreamName The name of the bitstream that moved. + * @param toPosition The zero-indexed position that the bitstream moved to. + */ + announceMove(bitstreamName: string, toPosition: number) { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.move', + { bitstream: bitstreamName, toIndex: toPosition + 1 }); + this.liveRegionService.addMessage(message); + } + + /** + * Adds a message to the live region mentioning that the bitstream with the provided name is no longer selected and + * was returned to the provided position. + * @param bitstreamName The name of the bitstream that is no longer selected + * @param toPosition The zero-indexed position the bitstream returned to. + */ + announceCancel(bitstreamName: string, toPosition: number) { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.cancel', + { bitstream: bitstreamName, toIndex: toPosition + 1 }); + this.liveRegionService.addMessage(message); + } + + /** + * Adds a message to the live region mentioning that the bitstream with the provided name is no longer selected. + * @param bitstreamName The name of the bitstream that is no longer selected. + */ + announceClear(bitstreamName: string) { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.clear', + { bitstream: bitstreamName }); + this.liveRegionService.addMessage(message); + } } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index d530fb38d1..06fb571ce4 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -5,7 +5,8 @@ [hidePagerWhenSinglePage]="true" [hidePaginationDetail]="true" [paginationOptions]="paginationOptions" - [collectionSize]="bitstreamsList.totalElements"> + [collectionSize]="bitstreamsList.totalElements" + [retainScrollPosition]="true"> -
- -
+ diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts index 1502ad2311..25274b8941 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts @@ -58,6 +58,7 @@ describe('ItemEditBitstreamBundleComponent', () => { currentPage: 1, pageSize: 9999 }), + getSelectedBitstream$: observableOf({}), }); beforeEach(waitForAsync(() => { diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 24319f4a83..7d2a519baf 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -1,4 +1,10 @@ -import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core'; +import { + Component, + Input, + OnInit, + ViewChild, + ViewContainerRef, OnDestroy, +} from '@angular/core'; import { Bundle } from '../../../../core/shared/bundle.model'; import { Item } from '../../../../core/shared/item.model'; import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; @@ -8,7 +14,7 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { RemoteData } from 'src/app/core/data/remote-data'; import { PaginatedList } from 'src/app/core/data/paginated-list.model'; import { Bitstream } from 'src/app/core/shared/bitstream.model'; -import { Observable, BehaviorSubject, switchMap } from 'rxjs'; +import { Observable, BehaviorSubject, switchMap, shareReplay, Subscription } from 'rxjs'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { FieldUpdates } from '../../../../core/data/object-updates/field-updates.model'; import { PaginatedSearchOptions } from '../../../../shared/search/models/paginated-search-options.model'; @@ -17,55 +23,17 @@ import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { getAllSucceededRemoteData, paginatedListToArray, - getFirstSucceededRemoteData } from '../../../../core/shared/operators'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; -import { map, take, filter } from 'rxjs/operators'; +import { map, take, filter, tap, pairwise } from 'rxjs/operators'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { RequestService } from '../../../../core/data/request.service'; -import { ItemBitstreamsService } from '../item-bitstreams.service'; -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; -import { hasValue } from '../../../../shared/empty.util'; -import { LiveRegionService } from '../../../../shared/live-region/live-region.service'; -import { TranslateService } from '@ngx-translate/core'; - -/** - * Interface storing all the information necessary to create a row in the bitstream edit table - */ -export interface BitstreamTableEntry { - /** - * The bitstream - */ - bitstream: Bitstream, - /** - * The uuid of the Bitstream - */ - id: string, - /** - * The name of the Bitstream - */ - name: string, - /** - * The name of the Bitstream with all whitespace removed - */ - nameStripped: string, - /** - * The description of the Bitstream - */ - description: string, - /** - * Observable emitting the Format of the Bitstream - */ - format: Observable, - /** - * The download url of the Bitstream - */ - downloadUrl: string, -} +import { ItemBitstreamsService, BitstreamTableEntry, SelectedBitstreamTableEntry } from '../item-bitstreams.service'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { hasValue, hasNoValue } from '../../../../shared/empty.util'; @Component({ selector: 'ds-item-edit-bitstream-bundle', @@ -77,7 +45,7 @@ export interface BitstreamTableEntry { * Creates an embedded view of the contents. This is to ensure the table structure won't break. * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-bundle element) */ -export class ItemEditBitstreamBundleComponent implements OnInit { +export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { protected readonly FieldChangeType = FieldChangeType; /** @@ -115,13 +83,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ @Input() isFirstTable = false; - /** - * Send an event when the user drops an object on the pagination - * The event contains details about the index the object came from and is dropped to (across the entirety of the list, - * not just within a single page) - */ - @Output() dropObject: EventEmitter = new EventEmitter(); - /** * The bootstrap sizes used for the Bundle Name column * This column stretches over the first 3 columns and thus is a combination of their sizes processed in ngOnInit @@ -138,6 +99,11 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ bundleName: string; + /** + * The number of bitstreams in the bundle + */ + bundleSize: number; + /** * The bitstreams to show in the table */ @@ -146,7 +112,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { /** * The data to show in the table */ - tableEntries$: BehaviorSubject = new BehaviorSubject(null); + tableEntries$: BehaviorSubject = new BehaviorSubject([]); /** * The initial page options to use for fetching the bitstreams @@ -158,11 +124,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ currentPaginationOptions$: BehaviorSubject; - /** - * The available page size options - */ - pageSizeOptions: number[]; - /** * The currently selected page size */ @@ -178,6 +139,11 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ updates$: BehaviorSubject = new BehaviorSubject(null); + /** + * Array containing all subscriptions created by this component + */ + subscriptions: Subscription[] = []; + constructor( protected viewContainerRef: ViewContainerRef, @@ -187,8 +153,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit { protected paginationService: PaginationService, protected requestService: RequestService, protected itemBitstreamsService: ItemBitstreamsService, - protected liveRegionService: LiveRegionService, - protected translateService: TranslateService, ) { } @@ -201,23 +165,27 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.initializePagination(); this.initializeBitstreams(); + this.initializeSelectionActions(); + } - // this.bitstreamsRD = this. + ngOnDestroy() { + this.subscriptions.forEach(sub => sub?.unsubscribe()); } protected initializePagination() { this.paginationOptions = this.itemBitstreamsService.getInitialBitstreamsPaginationOptions(this.bundleName); - this.pageSizeOptions = this.paginationOptions.pageSizeOptions; - this.currentPaginationOptions$ = new BehaviorSubject(this.paginationOptions); this.pageSize$ = new BehaviorSubject(this.paginationOptions.pageSize); - this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions) - .subscribe((pagination) => { - this.currentPaginationOptions$.next(pagination); - this.pageSize$.next(pagination.pageSize); - }); + this.subscriptions.push( + this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions) + .subscribe((pagination) => { + this.currentPaginationOptions$.next(pagination); + this.pageSize$.next(pagination.pageSize); + }) + ); + } protected initializeBitstreams() { @@ -233,26 +201,88 @@ export class ItemEditBitstreamBundleComponent implements OnInit { )) ); }), + getAllSucceededRemoteData(), + shareReplay(1), ); - this.bitstreamsRD$.pipe( - getFirstSucceededRemoteData(), - paginatedListToArray(), - ).subscribe((bitstreams) => { - this.objectUpdatesService.initialize(this.bundleUrl, bitstreams, new Date()); - }); + this.subscriptions.push( + this.bitstreamsRD$.pipe( + take(1), + tap(bitstreamsRD => this.bundleSize = bitstreamsRD.payload.totalElements), + paginatedListToArray(), + ).subscribe((bitstreams) => { + this.objectUpdatesService.initialize(this.bundleUrl, bitstreams, new Date()); + }), - this.bitstreamsRD$.pipe( - getAllSucceededRemoteData(), - paginatedListToArray(), - switchMap((bitstreams) => this.objectUpdatesService.getFieldUpdatesExclusive(this.bundleUrl, bitstreams)) - ).subscribe((updates) => this.updates$.next(updates)); + this.bitstreamsRD$.pipe( + paginatedListToArray(), + switchMap((bitstreams) => this.objectUpdatesService.getFieldUpdatesExclusive(this.bundleUrl, bitstreams)) + ).subscribe((updates) => this.updates$.next(updates)), - this.bitstreamsRD$.pipe( - getAllSucceededRemoteData(), - paginatedListToArray(), - map((bitstreams) => this.itemBitstreamsService.mapBitstreamsToTableEntries(bitstreams)), - ).subscribe((tableEntries) => this.tableEntries$.next(tableEntries)); + this.bitstreamsRD$.pipe( + paginatedListToArray(), + map((bitstreams) => this.itemBitstreamsService.mapBitstreamsToTableEntries(bitstreams)), + ).subscribe((tableEntries) => this.tableEntries$.next(tableEntries)), + ); + } + + protected initializeSelectionActions() { + this.subscriptions.push( + this.itemBitstreamsService.getSelectedBitstream$().pipe(pairwise()).subscribe( + ([previousSelection, currentSelection]) => + this.handleSelectedEntryChange(previousSelection, currentSelection)) + ); + } + + /** + * Handles a change in selected bitstream by changing the pagination if the change happened on a different page + * @param previousSelectedEntry The previously selected entry + * @param currentSelectedEntry The currently selected entry + * @protected + */ + protected handleSelectedEntryChange( + previousSelectedEntry: SelectedBitstreamTableEntry, + currentSelectedEntry: SelectedBitstreamTableEntry + ) { + if (hasValue(currentSelectedEntry) && currentSelectedEntry.bundle === this.bundle) { + // If the currently selected bitstream belongs to this bundle, it has possibly moved to a different page. + // In that case we want to change the pagination to the new page. + this.redirectToCurrentPage(currentSelectedEntry); + } + + // If the selection is cancelled or cleared, it is possible the selected bitstream is currently on a different page + // In that case we want to change the pagination to the place where the bitstream was returned to + if (hasNoValue(currentSelectedEntry) && hasValue(previousSelectedEntry) && previousSelectedEntry.bundle === this.bundle) { + this.redirectToOriginalPage(previousSelectedEntry); + } + } + + /** + * Redirect the user to the current page of the provided bitstream if it is on a different page. + * @param bitstreamEntry The entry that the current position will be taken from to determine the page to move to + * @protected + */ + protected redirectToCurrentPage(bitstreamEntry: SelectedBitstreamTableEntry) { + const currentPage = this.getCurrentPage(); + const selectedEntryPage = this.bundleIndexToPage(bitstreamEntry.currentPosition); + + if (currentPage !== selectedEntryPage) { + this.changeToPage(selectedEntryPage); + } + } + + /** + * Redirect the user to the original page of the provided bitstream if it is on a different page. + * @param bitstreamEntry The entry that the original position will be taken from to determine the page to move to + * @protected + */ + protected redirectToOriginalPage(bitstreamEntry: SelectedBitstreamTableEntry) { + const currentPage = this.getCurrentPage(); + const originPage = this.bundleIndexToPage(bitstreamEntry.originalPosition); + + if (currentPage !== originPage) { + this.changeToPage(originPage); + } } /** @@ -283,7 +313,18 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.objectUpdatesService.removeSingleFieldUpdate(this.bundleUrl, bitstream.uuid); } - getRowClass(update: FieldUpdate): string { + /** + * Returns the css class for a table row depending on the state of the table entry. + * @param update + * @param bitstream + */ + getRowClass(update: FieldUpdate, bitstream: BitstreamTableEntry): string { + const selected = this.itemBitstreamsService.getSelectedBitstream(); + + if (hasValue(selected) && bitstream.id === selected.bitstream.id) { + return 'table-info'; + } + switch (update.changeType) { case FieldChangeType.UPDATE: return 'table-warning'; @@ -296,11 +337,19 @@ export class ItemEditBitstreamBundleComponent implements OnInit { } } + /** + * Changes the page size to the provided page size. + * @param pageSize + */ public doPageSizeChange(pageSize: number) { this.paginationComponent.doPageSizeChange(pageSize); } - dragStart(bitstreamName: string) { + /** + * Handles start of dragging by opening the tooltip mentioning that it is possible to drag a bitstream to a different + * page by dropping it on the page number, only if there are multiple pages. + */ + dragStart() { // Only open the drag tooltip when there are multiple pages this.paginationComponent.shouldShowBottomPager.pipe( take(1), @@ -308,66 +357,170 @@ export class ItemEditBitstreamBundleComponent implements OnInit { ).subscribe(() => { this.dragTooltip.open(); }); - - const message = this.translateService.instant('item.edit.bitstreams.edit.live.drag', - { bitstream: bitstreamName }); - this.liveRegionService.addMessage(message); } - dragEnd(bitstreamName: string) { + /** + * Handles end of dragging by closing the tooltip. + */ + dragEnd() { this.dragTooltip.close(); - - const message = this.translateService.instant('item.edit.bitstreams.edit.live.drop', - { bitstream: bitstreamName }); - this.liveRegionService.addMessage(message); } - + /** + * Handles dropping by calculation the target position, and changing the page if the bitstream was dropped on a + * different page. + * @param event + */ drop(event: CdkDragDrop) { const dragIndex = event.previousIndex; let dropIndex = event.currentIndex; - const dragPage = this.currentPaginationOptions$.value.currentPage - 1; - let dropPage = this.currentPaginationOptions$.value.currentPage - 1; + const dragPage = this.getCurrentPage(); + let dropPage = this.getCurrentPage(); // Check if the user is hovering over any of the pagination's pages at the time of dropping the object const droppedOnElement = document.elementFromPoint(event.dropPoint.x, event.dropPoint.y); if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent) && droppedOnElement.classList.contains('page-link')) { // The user is hovering over a page, fetch the page's number from the element - const droppedPage = Number(droppedOnElement.textContent); + let droppedPage = Number(droppedOnElement.textContent); if (hasValue(droppedPage) && !Number.isNaN(droppedPage)) { - dropPage = droppedPage - 1; - dropIndex = 0; + droppedPage -= 1; + + if (droppedPage !== dragPage) { + dropPage = droppedPage; + + if (dropPage > dragPage) { + // When moving to later page, place bitstream at the top + dropIndex = 0; + } else { + // When moving to earlier page, place bitstream at the bottom + dropIndex = this.getCurrentPageSize() - 1; + } + } } } - const isNewPage = dragPage !== dropPage; - // Move the object in the custom order array if the drop happened within the same page - // This allows us to instantly display a change in the order, instead of waiting for the REST API's response first - if (!isNewPage && dragIndex !== dropIndex) { - const currentEntries = [...this.tableEntries$.value]; - moveItemInArray(currentEntries, dragIndex, dropIndex); - this.tableEntries$.next(currentEntries); + const fromIndex = this.pageIndexToBundleIndex(dragIndex, dragPage); + const toIndex = this.pageIndexToBundleIndex(dropIndex, dropPage); + + if (fromIndex === toIndex) { + return; } - const pageSize = this.currentPaginationOptions$.value.pageSize; - const redirectPage = dropPage + 1; - const fromIndex = (dragPage * pageSize) + dragIndex; - const toIndex = (dropPage * pageSize) + dropIndex; - // Send out a drop event (and navigate to the new page) when the "from" and "to" indexes are different from each other - if (fromIndex !== toIndex) { - // if (isNewPage) { - // this.loading$.next(true); - // } - this.dropObject.emit(Object.assign({ - fromIndex, - toIndex, - finish: () => { - if (isNewPage) { - this.paginationComponent.doPageChange(redirectPage); - } - } - })); + const selectedBitstream = this.tableEntries$.value[dragIndex]; + + const finish = () => { + this.itemBitstreamsService.announceMove(selectedBitstream.name, toIndex); + + if (dropPage !== this.getCurrentPage()) { + this.changeToPage(dropPage); + } + }; + + this.itemBitstreamsService.performBitstreamMoveRequest(this.bundle, fromIndex, toIndex, finish); + } + + /** + * Handles a select action for the provided bitstream entry. + * If the selected bitstream is currently selected, the selection is cleared. + * If no, or a different bitstream, is selected, the provided bitstream becomes the selected bitstream. + * @param bitstream + */ + select(bitstream: BitstreamTableEntry) { + const selectedBitstream = this.itemBitstreamsService.getSelectedBitstream(); + + if (hasValue(selectedBitstream) && selectedBitstream.bitstream === bitstream) { + this.itemBitstreamsService.cancelSelection(); + } else { + const selectionObject = this.createBitstreamSelectionObject(bitstream); + + if (hasNoValue(selectionObject)) { + console.warn('Failed to create selection object'); + return; + } + + this.itemBitstreamsService.selectBitstreamEntry(selectionObject); } } + /** + * Creates a {@link SelectedBitstreamTableEntry} from the provided {@link BitstreamTableEntry} so it can be given + * to the {@link ItemBitstreamsService} to select the table entry. + * @param bitstream The table entry to create the selection object from. + * @protected + */ + protected createBitstreamSelectionObject(bitstream: BitstreamTableEntry): SelectedBitstreamTableEntry { + const pageIndex = this.findBitstreamPageIndex(bitstream); + + if (pageIndex === -1) { + return null; + } + + const position = this.pageIndexToBundleIndex(pageIndex, this.getCurrentPage()); + + return { + bitstream: bitstream, + bundle: this.bundle, + bundleSize: this.bundleSize, + currentPosition: position, + originalPosition: position, + }; + } + + /** + * Returns the index of the provided {@link BitstreamTableEntry} relative to the current page + * If the current page size is 10, it will return a value from 0 to 9 (inclusive) + * Returns -1 if the provided bitstream could not be found + * @protected + */ + protected findBitstreamPageIndex(bitstream: BitstreamTableEntry): number { + const entries = this.tableEntries$.value; + return entries.findIndex(entry => entry === bitstream); + } + + /** + * Returns the current zero-indexed page + * @protected + */ + protected getCurrentPage(): number { + // The pagination component uses one-based numbering while zero-based numbering is more convenient for calculations + return this.currentPaginationOptions$.value.currentPage - 1; + } + + /** + * Returns the current page size + * @protected + */ + protected getCurrentPageSize(): number { + return this.currentPaginationOptions$.value.pageSize; + } + + /** + * Converts an index relative to the page to an index relative to the bundle + * @param index The index relative to the page + * @param page The zero-indexed page number + * @protected + */ + protected pageIndexToBundleIndex(index: number, page: number) { + return page * this.getCurrentPageSize() + index; + } + + /** + * Calculates the zero-indexed page number from the index relative to the bundle + * @param index The index relative to the bundle + * @protected + */ + protected bundleIndexToPage(index: number) { + return Math.floor(index / this.getCurrentPageSize()); + } + + /** + * Change the pagination for this bundle to the provided zero-indexed page + * @param page The zero-indexed page to change to + * @protected + */ + protected changeToPage(page: number) { + // Increments page by one because zero-indexing is way easier for calculations but the pagination component + // uses one-indexing. + this.paginationComponent.doPageChange(page + 1); + } } diff --git a/src/app/shared/live-region/live-region.service.stub.ts b/src/app/shared/live-region/live-region.service.stub.ts new file mode 100644 index 0000000000..4f10b46a4c --- /dev/null +++ b/src/app/shared/live-region/live-region.service.stub.ts @@ -0,0 +1,30 @@ +import { of } from 'rxjs'; +import { LiveRegionService } from './live-region.service'; + +export function getLiveRegionServiceStub(): LiveRegionService { + return new LiveRegionServiceStub() as unknown as LiveRegionService; +} + +export class LiveRegionServiceStub { + getMessages = jasmine.createSpy('getMessages').and.returnValue( + ['Message One', 'Message Two'] + ); + + getMessages$ = jasmine.createSpy('getMessages$').and.returnValue( + of(['Message One', 'Message Two']) + ); + + addMessage = jasmine.createSpy('addMessage').and.returnValue('messageId'); + + clear = jasmine.createSpy('clear'); + + clearMessageByUUID = jasmine.createSpy('clearMessageByUUID'); + + getLiveRegionVisibility = jasmine.createSpy('getLiveRegionVisibility').and.returnValue(false); + + setLiveRegionVisibility = jasmine.createSpy('setLiveRegionVisibility'); + + getMessageTimeOutMs = jasmine.createSpy('getMessageTimeOutMs').and.returnValue(30000); + + setMessageTimeOutMs = jasmine.createSpy('setMessageTimeOutMs'); +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index ab6f3792ca..519189ed69 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1950,9 +1950,13 @@ "item.edit.bitstreams.edit.buttons.undo": "Undo changes", - "item.edit.bitstreams.edit.live.drag": "{{ bitstream }} grabbed", + "item.edit.bitstreams.edit.live.cancel": "{{ bitstream }} was returned to position {{ toIndex }} and is no longer selected.", - "item.edit.bitstreams.edit.live.drop": "{{ bitstream }} dropped", + "item.edit.bitstreams.edit.live.clear": "{{ bitstream }} is no longer selected.", + + "item.edit.bitstreams.edit.live.select": "{{ bitstream }} is selected.", + + "item.edit.bitstreams.edit.live.move": "{{ bitstream }} is now in position {{ toIndex }}.", "item.edit.bitstreams.empty": "This item doesn't contain any bitstreams. Click the upload button to create one.", From 2e1b1489b65c6c6715774ef482242e8f14d990f0 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 4 Oct 2024 09:40:20 +0200 Subject: [PATCH 033/123] 118223: Stop space from scrolling down page --- .../item-bitstreams/item-bitstreams.component.ts | 1 + .../item-edit-bitstream-bundle.component.html | 2 +- .../item-edit-bitstream-bundle.component.ts | 12 ++++++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 4ced3dd649..f77eda02fb 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -138,6 +138,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme // Otherwise we might clear the selection when a different action was intended, e.g. clicking a button or selecting // a different bitstream. if (event.target instanceof Element && event.target.tagName === 'BODY') { + event.preventDefault(); this.itemBitstreamsService.clearSelection(); } } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index 06fb571ce4..9afdd2d41c 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -82,7 +82,7 @@
-
- +
+
{{ entry.name }}
+ (keydown.enter)="select($event, entry)" (keydown.space)="select($event, entry)" (click)="select($event, entry)">
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 7d2a519baf..e2dff2f018 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -423,9 +423,17 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { * Handles a select action for the provided bitstream entry. * If the selected bitstream is currently selected, the selection is cleared. * If no, or a different bitstream, is selected, the provided bitstream becomes the selected bitstream. - * @param bitstream + * @param event The event that triggered the select action + * @param bitstream The bitstream that is the target of the select action */ - select(bitstream: BitstreamTableEntry) { + select(event: UIEvent, bitstream: BitstreamTableEntry) { + event.preventDefault(); + + if (event instanceof KeyboardEvent && event.repeat) { + // Don't handle hold events, otherwise it would change rapidly between being selected and unselected + return; + } + const selectedBitstream = this.itemBitstreamsService.getSelectedBitstream(); if (hasValue(selectedBitstream) && selectedBitstream.bitstream === bitstream) { From 0920a218762a4aa547c8b9bf72e795e8321e73ff Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 4 Oct 2024 10:53:42 +0200 Subject: [PATCH 034/123] 118223: Stop sending success notificiations on every move --- .../item-bitstreams.service.ts | 60 +++++++++++++++---- .../item-edit-bitstream-bundle.component.ts | 9 ++- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts index 21dc415198..f8091d616a 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -3,7 +3,7 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; import { RemoteData } from '../../../core/data/remote-data'; -import { isNotEmpty, hasValue, hasNoValue } from '../../../shared/empty.util'; +import { hasValue, hasNoValue } from '../../../shared/empty.util'; import { Bundle } from '../../../core/shared/bundle.model'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; @@ -25,6 +25,8 @@ import { BundleDataService } from '../../../core/data/bundle-data.service'; import { RequestService } from '../../../core/data/request.service'; import { LiveRegionService } from '../../../shared/live-region/live-region.service'; +export const MOVE_KEY = 'item.edit.bitstreams.notifications.move'; + /** * Interface storing all the information necessary to create a row in the bitstream edit table */ @@ -164,6 +166,10 @@ export class ItemBitstreamsService { if (hasValue(selected)) { this.updateSelectedBitstream(null); this.announceClear(selected.bitstream.name); + + if (selected.currentPosition !== selected.originalPosition) { + this.displaySuccessNotification(MOVE_KEY); + } } } @@ -265,7 +271,7 @@ export class ItemBitstreamsService { this.isPerformingMoveRequest = true; this.bundleService.patch(bundle, [moveOperation]).pipe( getFirstCompletedRemoteData(), - tap((response: RemoteData) => this.displayNotifications('item.edit.bitstreams.notifications.move', [response])), + tap((response: RemoteData) => this.displayFailedResponseNotifications(MOVE_KEY, [response])), switchMap(() => this.requestService.setStaleByHrefSubstring(bundle.self)), take(1), ).subscribe(() => { @@ -321,19 +327,51 @@ export class ItemBitstreamsService { * @param responses The returned responses to display notifications for */ displayNotifications(key: string, responses: RemoteData[]) { - if (isNotEmpty(responses)) { - const failedResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasFailed); - const successfulResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasSucceeded); + this.displayFailedResponseNotifications(key, responses); + this.displaySuccessFulResponseNotifications(key, responses); + } - failedResponses.forEach((response: RemoteData) => { - this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage); - }); - if (successfulResponses.length > 0) { - this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`)); - } + /** + * Display an error notification for each failed response with their message + * @param key The i18n key for the notification messages + * @param responses The returned responses to display notifications for + */ + displayFailedResponseNotifications(key: string, responses: RemoteData[]) { + const failedResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasFailed); + failedResponses.forEach((response: RemoteData) => { + this.displayErrorNotification(key, response.errorMessage); + }); + } + + /** + * Display an error notification with the provided key and message + * @param key The i18n key for the notification messages + * @param errorMessage The error message to display + */ + displayErrorNotification(key: string, errorMessage: string) { + this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), errorMessage); + } + + /** + * Display a success notification in case there's at least one successful response + * @param key The i18n key for the notification messages + * @param responses The returned responses to display notifications for + */ + displaySuccessFulResponseNotifications(key: string, responses: RemoteData[]) { + const successfulResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasSucceeded); + if (successfulResponses.length > 0) { + this.displaySuccessNotification(key); } } + /** + * Display a success notification with the provided key + * @param key The i18n key for the notification messages + */ + displaySuccessNotification(key: string) { + this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`)); + } + /** * Removes the bitstreams marked for deletion from the Bundles emitted by the provided observable. * @param bundles$ diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index e2dff2f018..4079ad225b 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -31,7 +31,12 @@ import { FieldUpdate } from '../../../../core/data/object-updates/field-update.m import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { RequestService } from '../../../../core/data/request.service'; -import { ItemBitstreamsService, BitstreamTableEntry, SelectedBitstreamTableEntry } from '../item-bitstreams.service'; +import { + ItemBitstreamsService, + BitstreamTableEntry, + SelectedBitstreamTableEntry, + MOVE_KEY +} from '../item-bitstreams.service'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { hasValue, hasNoValue } from '../../../../shared/empty.util'; @@ -414,6 +419,8 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { if (dropPage !== this.getCurrentPage()) { this.changeToPage(dropPage); } + + this.itemBitstreamsService.displaySuccessNotification(MOVE_KEY); }; this.itemBitstreamsService.performBitstreamMoveRequest(this.bundle, fromIndex, toIndex, finish); From b158c5c2a275ff64108229c37083da7e7f78720e Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 4 Oct 2024 10:58:52 +0200 Subject: [PATCH 035/123] 118223: Move drag tooltip to center of pagination numbers --- .../item-edit-bitstream-bundle.component.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index 9afdd2d41c..efbdd8c69b 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -6,7 +6,9 @@ [hidePaginationDetail]="true" [paginationOptions]="paginationOptions" [collectionSize]="bitstreamsList.totalElements" - [retainScrollPosition]="true"> + [retainScrollPosition]="true" + [ngbTooltip]="'item.edit.bitstreams.bundle.tooltip' | translate" placement="bottom" + [autoClose]="false" triggers="manual" #dragTooltip="ngbTooltip"> - + +
{{'item.edit.bitstreams.bundle.name' | translate:{ name: bundleName } }} From 1dcc5d1ec5a21667e90322fb2a6dc4528d8f5c0b Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 4 Oct 2024 15:16:16 +0200 Subject: [PATCH 036/123] 118223: Add ItemBitstreams service tests --- .../item-bitstreams.service.spec.ts | 443 +++++++++++++++++- .../item-bitstreams.service.ts | 4 +- 2 files changed, 444 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts index e144e81ec7..94adb5f23a 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts @@ -1,4 +1,4 @@ -import { ItemBitstreamsService } from './item-bitstreams.service'; +import { ItemBitstreamsService, SelectedBitstreamTableEntry } from './item-bitstreams.service'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; import { ObjectUpdatesServiceStub } from '../../../core/data/object-updates/object-updates.service.stub'; @@ -22,6 +22,9 @@ import { LiveRegionService } from '../../../shared/live-region/live-region.servi import { Bundle } from '../../../core/shared/bundle.model'; import { of } from 'rxjs'; import { getLiveRegionServiceStub } from '../../../shared/live-region/live-region.service.stub'; +import { fakeAsync, tick } from '@angular/core/testing'; +import createSpy = jasmine.createSpy; +import { MoveOperation } from 'fast-json-patch'; describe('ItemBitstreamsService', () => { let service: ItemBitstreamsService; @@ -60,6 +63,444 @@ describe('ItemBitstreamsService', () => { ); }); + const defaultEntry: SelectedBitstreamTableEntry = { + bitstream: { + name: 'bitstream name', + } as any, + bundle: Object.assign(new Bundle(), { + _links: { self: { href: 'self_link' }}, + }), + bundleSize: 10, + currentPosition: 0, + originalPosition: 0, + }; + + describe('selectBitstreamEntry', () => { + it('should correctly make getSelectedBitstream$ emit', fakeAsync(() => { + const emittedEntries = []; + + service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + + expect(emittedEntries.length).toBe(1); + expect(emittedEntries[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedEntries.length).toBe(2); + expect(emittedEntries[1]).toEqual(entry); + })); + + it('should correctly make getSelectedBitstream return the bitstream', () => { + expect(service.getSelectedBitstream()).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + expect(service.getSelectedBitstream()).toEqual(entry); + }); + + it('should correctly make hasSelectedBitstream return', () => { + expect(service.hasSelectedBitstream()).toBeFalse(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + expect(service.hasSelectedBitstream()).toBeTrue(); + }); + + it('should do nothing if no entry was provided', fakeAsync(() => { + const emittedEntries = []; + + service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + + expect(emittedEntries.length).toBe(1); + expect(emittedEntries[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedEntries.length).toBe(2); + expect(emittedEntries[1]).toEqual(entry); + + service.selectBitstreamEntry(null); + tick(); + + expect(emittedEntries.length).toBe(2); + expect(emittedEntries[1]).toEqual(entry); + })); + + it('should announce the selected bitstream', () => { + const entry = Object.assign({}, defaultEntry); + + spyOn(service, 'announceSelect'); + + service.selectBitstreamEntry(entry); + expect(service.announceSelect).toHaveBeenCalledWith(entry.bitstream.name); + }); + }); + + describe('clearSelection', () => { + it('should clear the selected bitstream', fakeAsync(() => { + const emittedEntries = []; + + service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + + expect(emittedEntries.length).toBe(1); + expect(emittedEntries[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedEntries.length).toBe(2); + expect(emittedEntries[1]).toEqual(entry); + + service.clearSelection(); + tick(); + + expect(emittedEntries.length).toBe(3); + expect(emittedEntries[2]).toBeNull(); + })); + + it('should not do anything if there is no selected bitstream', fakeAsync(() => { + const emittedEntries = []; + + service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + + expect(emittedEntries.length).toBe(1); + expect(emittedEntries[0]).toBeNull(); + + service.clearSelection(); + tick(); + + expect(emittedEntries.length).toBe(1); + expect(emittedEntries[0]).toBeNull(); + })); + + it('should announce the cleared bitstream', () => { + const entry = Object.assign({}, defaultEntry); + + spyOn(service, 'announceClear'); + service.selectBitstreamEntry(entry); + service.clearSelection(); + + expect(service.announceClear).toHaveBeenCalledWith(entry.bitstream.name); + }); + + it('should display a notification if the selected bitstream was moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 7, + } + ); + + spyOn(service, 'displaySuccessNotification'); + service.selectBitstreamEntry(entry); + service.clearSelection(); + + expect(service.displaySuccessNotification).toHaveBeenCalled(); + }); + + it('should not display a notification if the selected bitstream is in its original position', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 7, + currentPosition: 7, + } + ); + + spyOn(service, 'displaySuccessNotification'); + service.selectBitstreamEntry(entry); + service.clearSelection(); + + expect(service.displaySuccessNotification).not.toHaveBeenCalled(); + }); + }); + + describe('cancelSelection', () => { + it('should clear the selected bitstream', fakeAsync(() => { + const emittedEntries = []; + + service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + + expect(emittedEntries.length).toBe(1); + expect(emittedEntries[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedEntries.length).toBe(2); + expect(emittedEntries[1]).toEqual(entry); + + service.cancelSelection(); + tick(); + + expect(emittedEntries.length).toBe(3); + expect(emittedEntries[2]).toBeNull(); + })); + + it('should announce a clear if the bitstream has not moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 7, + currentPosition: 7, + } + ); + + spyOn(service, 'announceClear'); + spyOn(service, 'announceCancel'); + + service.selectBitstreamEntry(entry); + service.cancelSelection(); + + expect(service.announceClear).toHaveBeenCalledWith(entry.bitstream.name); + expect(service.announceCancel).not.toHaveBeenCalled(); + }); + + it('should announce a cancel if the bitstream has moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 7, + } + ); + + spyOn(service, 'announceClear'); + spyOn(service, 'announceCancel'); + + service.selectBitstreamEntry(entry); + service.cancelSelection(); + + expect(service.announceClear).not.toHaveBeenCalled(); + expect(service.announceCancel).toHaveBeenCalledWith(entry.bitstream.name, entry.originalPosition); + }); + + it('should return the bitstream to its original position if it has moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 7, + } + ); + + spyOn(service, 'performBitstreamMoveRequest'); + + service.selectBitstreamEntry(entry); + service.cancelSelection(); + + expect(service.performBitstreamMoveRequest).toHaveBeenCalledWith(entry.bundle, entry.currentPosition, entry.originalPosition); + }); + + it('should not move the bitstream if it has not moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 7, + currentPosition: 7, + } + ); + + spyOn(service, 'performBitstreamMoveRequest'); + + service.selectBitstreamEntry(entry); + service.cancelSelection(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + + it('should not do anything if there is no selected bitstream', () => { + spyOn(service, 'announceClear'); + spyOn(service, 'announceCancel'); + spyOn(service, 'performBitstreamMoveRequest'); + + service.cancelSelection(); + + expect(service.announceClear).not.toHaveBeenCalled(); + expect(service.announceCancel).not.toHaveBeenCalled(); + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + }); + + describe('moveSelectedBitstream', () => { + beforeEach(() => { + spyOn(service, 'performBitstreamMoveRequest').and.callThrough(); + }); + + describe('up', () => { + it('should move the selected bitstream one position up', () => { + const startPosition = 7; + const endPosition = startPosition - 1; + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + } + ); + + const movedEntry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: endPosition, + } + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamUp(); + expect(service.performBitstreamMoveRequest).toHaveBeenCalledWith(entry.bundle, startPosition, endPosition, jasmine.any(Function)); + expect(service.getSelectedBitstream()).toEqual(movedEntry); + }); + + it('should announce the move', () => { + const startPosition = 7; + const endPosition = startPosition - 1; + + spyOn(service, 'announceMove'); + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + } + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamUp(); + + expect(service.announceMove).toHaveBeenCalledWith(entry.bitstream.name, endPosition); + }); + + it('should not do anything if the bitstream is already at the top', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 0, + } + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamUp(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + + it('should not do anything if there is no selected bitstream', () => { + service.moveSelectedBitstreamUp(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + }); + + describe('down', () => { + it('should move the selected bitstream one position down', () => { + const startPosition = 7; + const endPosition = startPosition + 1; + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + } + ); + + const movedEntry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: endPosition, + } + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamDown(); + expect(service.performBitstreamMoveRequest).toHaveBeenCalledWith(entry.bundle, startPosition, endPosition, jasmine.any(Function)); + expect(service.getSelectedBitstream()).toEqual(movedEntry); + }); + + it('should announce the move', () => { + const startPosition = 7; + const endPosition = startPosition + 1; + + spyOn(service, 'announceMove'); + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + } + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamDown(); + + expect(service.announceMove).toHaveBeenCalledWith(entry.bitstream.name, endPosition); + }); + + it('should not do anything if the bitstream is already at the bottom of the bundle', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 9, + } + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamDown(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + + it('should not do anything if there is no selected bitstream', () => { + service.moveSelectedBitstreamDown(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + }); + }); + + describe('performBitstreamMoveRequest', () => { + const bundle: Bundle = defaultEntry.bundle; + const from = 5; + const to = 7; + const callback = createSpy('callbackFunction'); + + console.log('bundle:', bundle); + + it('should correctly create the Move request', () => { + const expectedOperation: MoveOperation = { + op: 'move', + from: `/_links/bitstreams/${from}/href`, + path: `/_links/bitstreams/${to}/href`, + }; + + service.performBitstreamMoveRequest(bundle, from, to, callback); + expect(bundleDataService.patch).toHaveBeenCalledWith(bundle, [expectedOperation]); + }); + + it('should correctly make the bundle\'s self link stale', () => { + service.performBitstreamMoveRequest(bundle, from, to, callback); + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(bundle._links.self.href); + }); + + it('should attempt to show a message should the request have failed', () => { + spyOn(service, 'displayFailedResponseNotifications'); + service.performBitstreamMoveRequest(bundle, from, to, callback); + expect(service.displayFailedResponseNotifications).toHaveBeenCalled(); + }); + + it('should correctly call the provided function once the request has finished', () => { + service.performBitstreamMoveRequest(bundle, from, to, callback); + expect(callback).toHaveBeenCalled(); + }); + }); + describe('displayNotifications', () => { it('should display an error notification if a response failed', () => { const responses = [ diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts index f8091d616a..5b5fb7d63c 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -143,7 +143,7 @@ export class ItemBitstreamsService { * Select the provided entry */ selectBitstreamEntry(entry: SelectedBitstreamTableEntry) { - if (entry !== this.selectedBitstream$.getValue()) { + if (hasValue(entry) && entry !== this.selectedBitstream$.getValue()) { this.announceSelect(entry.bitstream.name); this.updateSelectedBitstream(entry); } @@ -184,7 +184,7 @@ export class ItemBitstreamsService { return; } - this.selectedBitstream$.next(null); + this.updateSelectedBitstream(null); const originalPosition = selected.originalPosition; const currentPosition = selected.currentPosition; From 0bdb5742e064ca35325aa03530506012e36b1dc4 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 4 Oct 2024 15:22:43 +0200 Subject: [PATCH 037/123] 118223: Remove unused item-edit-bitstream component --- .../edit-item-page/edit-item-page.module.ts | 2 - .../item-edit-bitstream.component.html | 51 ------ .../item-edit-bitstream.component.spec.ts | 145 ------------------ .../item-edit-bitstream.component.ts | 117 -------------- 4 files changed, 315 deletions(-) delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts diff --git a/src/app/item-page/edit-item-page/edit-item-page.module.ts b/src/app/item-page/edit-item-page/edit-item-page.module.ts index 4ae5ebe666..c38b480622 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.module.ts @@ -15,7 +15,6 @@ import { ItemPrivateComponent } from './item-private/item-private.component'; import { ItemPublicComponent } from './item-public/item-public.component'; import { ItemDeleteComponent } from './item-delete/item-delete.component'; import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; -import { ItemEditBitstreamComponent } from './item-bitstreams/item-edit-bitstream/item-edit-bitstream.component'; import { SearchPageModule } from '../../search-page/search-page.module'; import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component'; import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; @@ -78,7 +77,6 @@ import { ItemRelationshipsComponent, ItemBitstreamsComponent, ItemVersionHistoryComponent, - ItemEditBitstreamComponent, ItemEditBitstreamBundleComponent, EditRelationshipComponent, EditRelationshipListComponent, diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html deleted file mode 100644 index 0f0fad2199..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html +++ /dev/null @@ -1,51 +0,0 @@ - -
- -
- - {{ bitstreamName }} - -
-
-
-
-
- {{ bitstream?.firstMetadataValue('dc.description') }} -
-
-
-
-
- - {{ (format$ | async)?.shortDescription }} - -
-
-
-
-
- - - - - - -
-
-
-
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts deleted file mode 100644 index aafa5a4fe4..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { ItemEditBitstreamComponent } from './item-edit-bitstream.component'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { of as observableOf } from 'rxjs'; -import { Bitstream } from '../../../../core/shared/bitstream.model'; -import { TranslateModule } from '@ngx-translate/core'; -import { VarDirective } from '../../../../shared/utils/var.directive'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; -import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; -import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; -import { By } from '@angular/platform-browser'; -import { BrowserOnlyMockPipe } from '../../../../shared/testing/browser-only-mock.pipe'; - -let comp: ItemEditBitstreamComponent; -let fixture: ComponentFixture; - -const columnSizes = new ResponsiveTableSizes([ - new ResponsiveColumnSizes(2, 2, 3, 4, 4), - new ResponsiveColumnSizes(2, 3, 3, 3, 3), - new ResponsiveColumnSizes(2, 2, 2, 2, 2), - new ResponsiveColumnSizes(6, 5, 4, 3, 3) -]); - -const format = Object.assign(new BitstreamFormat(), { - shortDescription: 'PDF' -}); -const bitstream = Object.assign(new Bitstream(), { - uuid: 'bitstreamUUID', - name: 'Fake Bitstream', - bundleName: 'ORIGINAL', - description: 'Description', - _links: { - content: { href: 'content-link' } - }, - - format: createSuccessfulRemoteDataObject$(format) -}); -const fieldUpdate = { - field: bitstream, - changeType: undefined -}; -const date = new Date(); -const url = 'thisUrl'; - -let objectUpdatesService: ObjectUpdatesService; - -describe('ItemEditBitstreamComponent', () => { - beforeEach(waitForAsync(() => { - objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', - { - getFieldUpdates: observableOf({ - [bitstream.uuid]: fieldUpdate, - }), - getFieldUpdatesExclusive: observableOf({ - [bitstream.uuid]: fieldUpdate, - }), - saveRemoveFieldUpdate: {}, - removeSingleFieldUpdate: {}, - saveAddFieldUpdate: {}, - discardFieldUpdates: {}, - reinstateFieldUpdates: observableOf(true), - initialize: {}, - getUpdatedFields: observableOf([bitstream]), - getLastModified: observableOf(date), - hasUpdates: observableOf(true), - isReinstatable: observableOf(false), - isValidPage: observableOf(true) - } - ); - - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [ - ItemEditBitstreamComponent, - VarDirective, - BrowserOnlyMockPipe, - ], - providers: [ - { provide: ObjectUpdatesService, useValue: objectUpdatesService } - ], schemas: [ - NO_ERRORS_SCHEMA - ] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ItemEditBitstreamComponent); - comp = fixture.componentInstance; - comp.fieldUpdate = fieldUpdate; - comp.bundleUrl = url; - comp.columnSizes = columnSizes; - comp.ngOnChanges(undefined); - fixture.detectChanges(); - }); - - describe('when remove is called', () => { - beforeEach(() => { - comp.remove(); - }); - - it('should call saveRemoveFieldUpdate on objectUpdatesService', () => { - expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(url, bitstream); - }); - }); - - describe('when undo is called', () => { - beforeEach(() => { - comp.undo(); - }); - - it('should call removeSingleFieldUpdate on objectUpdatesService', () => { - expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, bitstream.uuid); - }); - }); - - describe('when canRemove is called', () => { - it('should return true', () => { - expect(comp.canRemove()).toEqual(true); - }); - }); - - describe('when canUndo is called', () => { - it('should return false', () => { - expect(comp.canUndo()).toEqual(false); - }); - }); - - describe('when the component loads', () => { - it('should contain download button with a valid link to the bitstreams download page', () => { - fixture.detectChanges(); - const downloadBtnHref = fixture.debugElement.query(By.css('[data-test="download-button"]')).nativeElement.getAttribute('href'); - expect(downloadBtnHref).toEqual(comp.bitstreamDownloadUrl); - }); - }); - - describe('when the bitstreamDownloadUrl property gets populated', () => { - it('should contain the bitstream download page route', () => { - expect(comp.bitstreamDownloadUrl).not.toEqual(bitstream._links.content.href); - expect(comp.bitstreamDownloadUrl).toEqual(getBitstreamDownloadRoute(bitstream)); - }); - }); -}); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts deleted file mode 100644 index fcb5c706ac..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core'; -import { Bitstream } from '../../../../core/shared/bitstream.model'; -import cloneDeep from 'lodash/cloneDeep'; -import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { Observable } from 'rxjs'; -import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; -import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../../core/shared/operators'; -import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; -import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; -import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; -import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; - -@Component({ - selector: 'ds-item-edit-bitstream', - styleUrls: ['../item-bitstreams.component.scss'], - templateUrl: './item-edit-bitstream.component.html', -}) -/** - * Component that displays a single bitstream of an item on the edit page - * Creates an embedded view of the contents - * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream element) - */ -export class ItemEditBitstreamComponent implements OnChanges, OnInit { - - /** - * The view on the bitstream - */ - @ViewChild('bitstreamView', {static: true}) bitstreamView; - - /** - * The current field, value and state of the bitstream - */ - @Input() fieldUpdate: FieldUpdate; - - /** - * The url of the bundle - */ - @Input() bundleUrl: string; - - /** - * The bootstrap sizes used for the columns within this table - */ - @Input() columnSizes: ResponsiveTableSizes; - - /** - * The bitstream of this field - */ - bitstream: Bitstream; - - /** - * The bitstream's name - */ - bitstreamName: string; - - /** - * The bitstream's download url - */ - bitstreamDownloadUrl: string; - - /** - * The format of the bitstream - */ - format$: Observable; - - constructor(private objectUpdatesService: ObjectUpdatesService, - private dsoNameService: DSONameService, - private viewContainerRef: ViewContainerRef) { - } - - ngOnInit(): void { - this.viewContainerRef.createEmbeddedView(this.bitstreamView); - } - - /** - * Update the current bitstream and its format on changes - * @param changes - */ - ngOnChanges(changes: SimpleChanges): void { - this.bitstream = cloneDeep(this.fieldUpdate.field) as Bitstream; - this.bitstreamName = this.dsoNameService.getName(this.bitstream); - this.bitstreamDownloadUrl = getBitstreamDownloadRoute(this.bitstream); - this.format$ = this.bitstream.format.pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload() - ); - } - - /** - * Sends a new remove update for this field to the object updates service - */ - remove(): void { - this.objectUpdatesService.saveRemoveFieldUpdate(this.bundleUrl, this.bitstream); - } - - /** - * Cancels the current update for this field in the object updates service - */ - undo(): void { - this.objectUpdatesService.removeSingleFieldUpdate(this.bundleUrl, this.bitstream.uuid); - } - - /** - * Check if a user should be allowed to remove this field - */ - canRemove(): boolean { - return this.fieldUpdate.changeType !== FieldChangeType.REMOVE; - } - - /** - * Check if a user should be allowed to cancel the update to this field - */ - canUndo(): boolean { - return this.fieldUpdate.changeType >= 0; - } - -} From e8379db987317c5cb892989a21297ab5edce8669 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Mon, 7 Oct 2024 11:49:54 +0200 Subject: [PATCH 038/123] 118223: Add item-bitstreams component tests --- .../item-bitstreams.component.spec.ts | 129 +++++++++++++++--- .../item-bitstreams.component.ts | 6 +- .../item-bitstreams.service.stub.ts | 74 ++++++++++ 3 files changed, 192 insertions(+), 17 deletions(-) create mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index a5549a6ba0..d26f815316 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -26,9 +26,7 @@ import { createPaginatedList } from '../../../shared/testing/utils.test'; import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub'; import { ItemBitstreamsService } from './item-bitstreams.service'; -import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; -import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { getItemBitstreamsServiceStub, ItemBitstreamsServiceStub } from './item-bitstreams.service.stub'; let comp: ItemBitstreamsComponent; let fixture: ComponentFixture; @@ -80,7 +78,7 @@ let objectCache: ObjectCacheService; let requestService: RequestService; let searchConfig: SearchConfigurationService; let bundleService: BundleDataService; -let itemBitstreamsService: ItemBitstreamsService; +let itemBitstreamsService: ItemBitstreamsServiceStub; describe('ItemBitstreamsComponent', () => { beforeEach(waitForAsync(() => { @@ -152,18 +150,7 @@ describe('ItemBitstreamsComponent', () => { patch: createSuccessfulRemoteDataObject$({}), }); - itemBitstreamsService = jasmine.createSpyObj('itemBitstreamsService', { - getColumnSizes: new ResponsiveTableSizes([ - new ResponsiveColumnSizes(2, 2, 3, 4, 4), - new ResponsiveColumnSizes(2, 3, 3, 3, 3), - new ResponsiveColumnSizes(2, 2, 2, 2, 2), - new ResponsiveColumnSizes(6, 5, 4, 3, 3) - ]), - getSelectedBitstream$: observableOf({}), - getInitialBundlesPaginationOptions: new PaginationComponentOptions(), - removeMarkedBitstreams: createSuccessfulRemoteDataObject$({}), - displayNotifications: undefined, - }); + itemBitstreamsService = getItemBitstreamsServiceStub(); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], @@ -218,4 +205,114 @@ describe('ItemBitstreamsComponent', () => { expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(bundle.self); }); }); + + describe('moveUp', () => { + it('should move the selected bitstream up', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.moveUp(event); + + expect(itemBitstreamsService.moveSelectedBitstreamUp).toHaveBeenCalled(); + }); + + it('should not do anything if no bitstream is selected', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(false); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.moveUp(event); + + expect(itemBitstreamsService.moveSelectedBitstreamUp).not.toHaveBeenCalled(); + }); + }); + + describe('moveDown', () => { + it('should move the selected bitstream down', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.moveDown(event); + + expect(itemBitstreamsService.moveSelectedBitstreamDown).toHaveBeenCalled(); + }); + + it('should not do anything if no bitstream is selected', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(false); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.moveDown(event); + + expect(itemBitstreamsService.moveSelectedBitstreamDown).not.toHaveBeenCalled(); + }); + }); + + describe('cancelSelection', () => { + it('should cancel the selection', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.cancelSelection(event); + + expect(itemBitstreamsService.cancelSelection).toHaveBeenCalled(); + }); + + it('should not do anything if no bitstream is selected', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(false); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.cancelSelection(event); + + expect(itemBitstreamsService.cancelSelection).not.toHaveBeenCalled(); + }); + }); + + describe('clearSelection', () => { + it('should clear the selection', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + target: document.createElement('BODY'), + preventDefault: () => {/* Intentionally empty */}, + } as unknown as KeyboardEvent; + comp.clearSelection(event); + + expect(itemBitstreamsService.clearSelection).toHaveBeenCalled(); + }); + + it('should not do anything if no bitstream is selected', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(false); + + const event = { + target: document.createElement('BODY'), + preventDefault: () => {/* Intentionally empty */}, + } as unknown as KeyboardEvent; + comp.clearSelection(event); + + expect(itemBitstreamsService.clearSelection).not.toHaveBeenCalled(); + }); + + it('should not do anything if the event target is not \'BODY\'', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + target: document.createElement('NOT-BODY'), + preventDefault: () => {/* Intentionally empty */}, + } as unknown as KeyboardEvent; + comp.clearSelection(event); + + expect(itemBitstreamsService.clearSelection).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index f77eda02fb..6ee5dcb545 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -137,7 +137,11 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme // Only when no specific element is in focus do we want to clear the currently selected bitstream // Otherwise we might clear the selection when a different action was intended, e.g. clicking a button or selecting // a different bitstream. - if (event.target instanceof Element && event.target.tagName === 'BODY') { + if ( + this.itemBitstreamsService.hasSelectedBitstream() && + event.target instanceof Element && + event.target.tagName === 'BODY' + ) { event.preventDefault(); this.itemBitstreamsService.clearSelection(); } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts new file mode 100644 index 0000000000..0521bf47f6 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts @@ -0,0 +1,74 @@ +import { of } from 'rxjs'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; + +export function getItemBitstreamsServiceStub(): ItemBitstreamsServiceStub { + return new ItemBitstreamsServiceStub(); +} + +export class ItemBitstreamsServiceStub { + getSelectedBitstream$ = jasmine.createSpy('getSelectedBitstream$').and + .returnValue(of(null)); + + getSelectedBitstream = jasmine.createSpy('getSelectedBitstream').and + .returnValue(null); + + hasSelectedBitstream = jasmine.createSpy('hasSelectedBitstream').and + .returnValue(false); + + selectBitstreamEntry = jasmine.createSpy('selectBitstreamEntry'); + + clearSelection = jasmine.createSpy('clearSelection'); + + cancelSelection = jasmine.createSpy('cancelSelection'); + + moveSelectedBitstreamUp = jasmine.createSpy('moveSelectedBitstreamUp'); + + moveSelectedBitstreamDown = jasmine.createSpy('moveSelectedBitstreamDown'); + + performBitstreamMoveRequest = jasmine.createSpy('performBitstreamMoveRequest'); + + getInitialBundlesPaginationOptions = jasmine.createSpy('getInitialBundlesPaginationOptions').and + .returnValue(new PaginationComponentOptions()); + + getInitialBitstreamsPaginationOptions = jasmine.createSpy('getInitialBitstreamsPaginationOptions').and + .returnValue(new PaginationComponentOptions()); + + getColumnSizes = jasmine.createSpy('getColumnSizes').and + .returnValue( + new ResponsiveTableSizes([ + new ResponsiveColumnSizes(2, 2, 3, 4, 4), + new ResponsiveColumnSizes(2, 3, 3, 3, 3), + new ResponsiveColumnSizes(2, 2, 2, 2, 2), + new ResponsiveColumnSizes(6, 5, 4, 3, 3) + ]) + ); + + displayNotifications = jasmine.createSpy('displayNotifications'); + + displayFailedResponseNotifications = jasmine.createSpy('displayFailedResponseNotifications'); + + displayErrorNotification = jasmine.createSpy('displayErrorNotification'); + + displaySuccessFulResponseNotifications = jasmine.createSpy('displaySuccessFulResponseNotifications'); + + displaySuccessNotification = jasmine.createSpy('displaySuccessNotification'); + + removeMarkedBitstreams = jasmine.createSpy('removeMarkedBitstreams').and + .returnValue(createSuccessfulRemoteDataObject$({})); + + mapBitstreamsToTableEntries = jasmine.createSpy('mapBitstreamsToTableEntries').and + .returnValue([]); + + nameToHeader = jasmine.createSpy('nameToHeader').and.returnValue('header'); + + stripWhiteSpace = jasmine.createSpy('stripWhiteSpace').and.returnValue('string'); + + announceSelect = jasmine.createSpy('announceSelect'); + + announceMove = jasmine.createSpy('announceMove'); + + announceCancel = jasmine.createSpy('announceCancel'); +} From 7fb4755abaa1759f175ea2d2cff6d93af00d946e Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 8 Oct 2024 15:51:00 +0200 Subject: [PATCH 039/123] 118223: Add item-edit-bitstream-bundle component tests --- ...em-edit-bitstream-bundle.component.spec.ts | 302 ++++++++++++++++-- .../item-edit-bitstream-bundle.component.ts | 3 +- 2 files changed, 285 insertions(+), 20 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts index 25274b8941..6008b5431f 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts @@ -7,14 +7,19 @@ import { Bundle } from '../../../../core/shared/bundle.model'; import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; import { BundleDataService } from '../../../../core/data/bundle-data.service'; -import { of as observableOf } from 'rxjs'; +import { of as observableOf, of, Subject } from 'rxjs'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; import { RequestService } from '../../../../core/data/request.service'; import { getMockRequestService } from '../../../../shared/mocks/request.service.mock'; -import { ItemBitstreamsService } from '../item-bitstreams.service'; +import { ItemBitstreamsService, BitstreamTableEntry, SelectedBitstreamTableEntry } from '../item-bitstreams.service'; import { PaginationService } from '../../../../core/pagination/pagination.service'; -import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { getItemBitstreamsServiceStub, ItemBitstreamsServiceStub } from '../item-bitstreams.service.stub'; +import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; +import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../../shared/testing/utils.test'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; describe('ItemEditBitstreamBundleComponent', () => { let comp: ItemEditBitstreamBundleComponent; @@ -43,25 +48,20 @@ describe('ItemEditBitstreamBundleComponent', () => { const restEndpoint = 'fake-rest-endpoint'; const bundleService = jasmine.createSpyObj('bundleService', { getBitstreamsEndpoint: observableOf(restEndpoint), - getBitstreams: null, + getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([])), }); - const objectUpdatesService = { - initialize: () => { - // do nothing - }, - }; - - const itemBitstreamsService = jasmine.createSpyObj('itemBitstreamsService', { - getInitialBitstreamsPaginationOptions: Object.assign(new PaginationComponentOptions(), { - id: 'bundles-pagination-options', - currentPage: 1, - pageSize: 9999 - }), - getSelectedBitstream$: observableOf({}), - }); + let objectUpdatesService: any; + let itemBitstreamsService: ItemBitstreamsServiceStub; beforeEach(waitForAsync(() => { + objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', { + initialize: undefined, + getFieldUpdatesExclusive: of(null), + }); + + itemBitstreamsService = getItemBitstreamsServiceStub(); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], declarations: [ItemEditBitstreamBundleComponent], @@ -92,4 +92,270 @@ describe('ItemEditBitstreamBundleComponent', () => { it('should create an embedded view of the component', () => { expect(viewContainerRef.createEmbeddedView).toHaveBeenCalled(); }); + + describe('on selected entry change', () => { + let paginationComponent: any; + let testSubject: Subject = new Subject(); + + beforeEach(() => { + paginationComponent = jasmine.createSpyObj('paginationComponent', { + doPageChange: undefined, + }); + comp.paginationComponent = paginationComponent; + + spyOn(comp, 'getCurrentPageSize').and.returnValue(2); + }); + + it('should move to the page the selected entry is on if were not on that page', () => { + const selectedA: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 1, + currentPosition: 1, + }; + + const selectedB: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 1, + currentPosition: 2, + }; + + comp.handleSelectedEntryChange(selectedA, selectedB); + expect(paginationComponent.doPageChange).toHaveBeenCalledWith(2); + }); + + it('should not change page when we are already on the correct page', () => { + const selectedA: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 0, + currentPosition: 0, + }; + + const selectedB: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 0, + currentPosition: 1, + }; + + comp.handleSelectedEntryChange(selectedA, selectedB); + expect(paginationComponent.doPageChange).not.toHaveBeenCalled(); + }); + + it('should change to the original page when cancelling', () => { + const selectedA: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 3, + currentPosition: 0, + }; + + const selectedB = null; + + comp.handleSelectedEntryChange(selectedA, selectedB); + expect(paginationComponent.doPageChange).toHaveBeenCalledWith(2); + }); + + it('should not change page when we are already on the correct page when cancelling', () => { + const selectedA: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 0, + currentPosition: 3, + }; + + const selectedB = null; + + comp.handleSelectedEntryChange(selectedA, selectedB); + expect(paginationComponent.doPageChange).not.toHaveBeenCalled(); + }); + }); + + describe('getRowClass', () => { + it('should return \'table-info\' when the bitstream is the selected bitstream', () => { + itemBitstreamsService.getSelectedBitstream.and.returnValue({ + bitstream: { id: 'bitstream-id'} + }); + + const bitstreamEntry = { + id: 'bitstream-id', + } as BitstreamTableEntry; + + expect(comp.getRowClass(undefined, bitstreamEntry)).toEqual('table-info'); + }); + + it('should return \'table-warning\' when the update is of type \'UPDATE\'', () => { + const update = { + changeType: FieldChangeType.UPDATE, + } as FieldUpdate; + + expect(comp.getRowClass(update, undefined)).toEqual('table-warning'); + }); + + it('should return \'table-success\' when the update is of type \'ADD\'', () => { + const update = { + changeType: FieldChangeType.ADD, + } as FieldUpdate; + + expect(comp.getRowClass(update, undefined)).toEqual('table-success'); + }); + + it('should return \'table-danger\' when the update is of type \'REMOVE\'', () => { + const update = { + changeType: FieldChangeType.REMOVE, + } as FieldUpdate; + + expect(comp.getRowClass(update, undefined)).toEqual('table-danger'); + }); + + it('should return \'bg-white\' in any other case', () => { + const update = { + changeType: undefined, + } as FieldUpdate; + + expect(comp.getRowClass(update, undefined)).toEqual('bg-white'); + }); + }); + + describe('drag', () => { + let dragTooltip; + let paginationComponent; + + beforeEach(() => { + dragTooltip = jasmine.createSpyObj('dragTooltip', { + open: undefined, + close: undefined, + }); + comp.dragTooltip = dragTooltip; + }); + + describe('Start', () => { + it('should open the tooltip when there are multiple pages', () => { + paginationComponent = jasmine.createSpyObj('paginationComponent', { + doPageChange: undefined, + }, { + shouldShowBottomPager: of(true), + }); + comp.paginationComponent = paginationComponent; + + comp.dragStart(); + expect(dragTooltip.open).toHaveBeenCalled(); + }); + + it('should not open the tooltip when there is only a single page', () => { + paginationComponent = jasmine.createSpyObj('paginationComponent', { + doPageChange: undefined, + }, { + shouldShowBottomPager: of(false), + }); + comp.paginationComponent = paginationComponent; + + comp.dragStart(); + expect(dragTooltip.open).not.toHaveBeenCalled(); + }); + }); + + describe('end', () => { + it('should always close the tooltip', () => { + paginationComponent = jasmine.createSpyObj('paginationComponent', { + doPageChange: undefined, + }, { + shouldShowBottomPager: of(false), + }); + comp.paginationComponent = paginationComponent; + + comp.dragEnd(); + expect(dragTooltip.close).toHaveBeenCalled(); + }); + }); + }); + + describe('drop', () => { + it('should correctly move the bitstream on drop', () => { + const event = { + previousIndex: 1, + currentIndex: 8, + dropPoint: { x: 100, y: 200 }, + } as CdkDragDrop; + + comp.drop(event); + expect(itemBitstreamsService.performBitstreamMoveRequest).toHaveBeenCalledWith(jasmine.any(Bundle), 1, 8, jasmine.any(Function)); + }); + + it('should not move the bitstream if dropped in the same place', () => { + const event = { + previousIndex: 1, + currentIndex: 1, + dropPoint: { x: 100, y: 200 }, + } as CdkDragDrop; + + comp.drop(event); + expect(itemBitstreamsService.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + + it('should move to a different page if dropped on a page number', () => { + spyOn(document, 'elementFromPoint').and.returnValue({ + textContent: '2', + classList: { contains: (token: string) => true }, + } as Element); + + const event = { + previousIndex: 1, + currentIndex: 1, + dropPoint: { x: 100, y: 200 }, + } as CdkDragDrop; + + comp.drop(event); + expect(itemBitstreamsService.performBitstreamMoveRequest).toHaveBeenCalledWith(jasmine.any(Bundle), 1, 20, jasmine.any(Function)); + }); + }); + + describe('select', () => { + it('should select the bitstream', () => { + const event = new KeyboardEvent('keydown'); + spyOnProperty(event, 'repeat', 'get').and.returnValue(false); + + const entry = { } as BitstreamTableEntry; + comp.tableEntries$.next([entry]); + + comp.select(event, entry); + expect(itemBitstreamsService.selectBitstreamEntry).toHaveBeenCalledWith(jasmine.objectContaining({ bitstream: entry })); + }); + + it('should cancel the selection if the bitstream already is selected', () => { + const event = new KeyboardEvent('keydown'); + spyOnProperty(event, 'repeat', 'get').and.returnValue(false); + + const entry = { } as BitstreamTableEntry; + comp.tableEntries$.next([entry]); + + itemBitstreamsService.getSelectedBitstream.and.returnValue({ bitstream: entry }); + + comp.select(event, entry); + expect(itemBitstreamsService.selectBitstreamEntry).not.toHaveBeenCalled(); + expect(itemBitstreamsService.cancelSelection).toHaveBeenCalled(); + }); + + it('should not do anything if the user is holding down the select key', () => { + const event = new KeyboardEvent('keydown'); + spyOnProperty(event, 'repeat', 'get').and.returnValue(true); + + const entry = { } as BitstreamTableEntry; + comp.tableEntries$.next([entry]); + + itemBitstreamsService.getSelectedBitstream.and.returnValue({ bitstream: entry }); + + comp.select(event, entry); + expect(itemBitstreamsService.selectBitstreamEntry).not.toHaveBeenCalled(); + expect(itemBitstreamsService.cancelSelection).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 4079ad225b..7a70ba80dd 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -243,9 +243,8 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { * Handles a change in selected bitstream by changing the pagination if the change happened on a different page * @param previousSelectedEntry The previously selected entry * @param currentSelectedEntry The currently selected entry - * @protected */ - protected handleSelectedEntryChange( + handleSelectedEntryChange( previousSelectedEntry: SelectedBitstreamTableEntry, currentSelectedEntry: SelectedBitstreamTableEntry ) { From 2b1b9d83d7d2e58a8f26b9843202f1705110fea5 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 9 Oct 2024 11:14:28 +0200 Subject: [PATCH 040/123] 118223: Include selection action with selection --- .../item-bitstreams.service.spec.ts | 176 ++++++++++++++---- .../item-bitstreams.service.stub.ts | 2 +- .../item-bitstreams.service.ts | 86 ++++++--- ...em-edit-bitstream-bundle.component.spec.ts | 35 +--- .../item-edit-bitstream-bundle.component.ts | 44 +++-- 5 files changed, 237 insertions(+), 106 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts index 94adb5f23a..f2af25f22f 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts @@ -77,20 +77,20 @@ describe('ItemBitstreamsService', () => { describe('selectBitstreamEntry', () => { it('should correctly make getSelectedBitstream$ emit', fakeAsync(() => { - const emittedEntries = []; + const emittedActions = []; - service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); - expect(emittedEntries.length).toBe(1); - expect(emittedEntries[0]).toBeNull(); + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); const entry = Object.assign({}, defaultEntry); service.selectBitstreamEntry(entry); tick(); - expect(emittedEntries.length).toBe(2); - expect(emittedEntries[1]).toEqual(entry); + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); })); it('should correctly make getSelectedBitstream return the bitstream', () => { @@ -112,26 +112,26 @@ describe('ItemBitstreamsService', () => { }); it('should do nothing if no entry was provided', fakeAsync(() => { - const emittedEntries = []; + const emittedActions = []; - service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); - expect(emittedEntries.length).toBe(1); - expect(emittedEntries[0]).toBeNull(); + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); const entry = Object.assign({}, defaultEntry); service.selectBitstreamEntry(entry); tick(); - expect(emittedEntries.length).toBe(2); - expect(emittedEntries[1]).toEqual(entry); + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); service.selectBitstreamEntry(null); tick(); - expect(emittedEntries.length).toBe(2); - expect(emittedEntries[1]).toEqual(entry); + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); })); it('should announce the selected bitstream', () => { @@ -146,41 +146,41 @@ describe('ItemBitstreamsService', () => { describe('clearSelection', () => { it('should clear the selected bitstream', fakeAsync(() => { - const emittedEntries = []; + const emittedActions = []; - service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); - expect(emittedEntries.length).toBe(1); - expect(emittedEntries[0]).toBeNull(); + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); const entry = Object.assign({}, defaultEntry); service.selectBitstreamEntry(entry); tick(); - expect(emittedEntries.length).toBe(2); - expect(emittedEntries[1]).toEqual(entry); + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); service.clearSelection(); tick(); - expect(emittedEntries.length).toBe(3); - expect(emittedEntries[2]).toBeNull(); + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Cleared', selectedEntry: entry }); })); it('should not do anything if there is no selected bitstream', fakeAsync(() => { - const emittedEntries = []; + const emittedActions = []; - service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); - expect(emittedEntries.length).toBe(1); - expect(emittedEntries[0]).toBeNull(); + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); service.clearSelection(); tick(); - expect(emittedEntries.length).toBe(1); - expect(emittedEntries[0]).toBeNull(); + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); })); it('should announce the cleared bitstream', () => { @@ -225,27 +225,53 @@ describe('ItemBitstreamsService', () => { }); describe('cancelSelection', () => { - it('should clear the selected bitstream', fakeAsync(() => { - const emittedEntries = []; + it('should clear the selected bitstream if it has not moved', fakeAsync(() => { + const emittedActions = []; - service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); - expect(emittedEntries.length).toBe(1); - expect(emittedEntries[0]).toBeNull(); + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); const entry = Object.assign({}, defaultEntry); service.selectBitstreamEntry(entry); tick(); - expect(emittedEntries.length).toBe(2); - expect(emittedEntries[1]).toEqual(entry); + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); service.cancelSelection(); tick(); - expect(emittedEntries.length).toBe(3); - expect(emittedEntries[2]).toBeNull(); + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Cleared', selectedEntry: entry }); + })); + + it('should cancel the selected bitstream if it has moved', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry, { + originalPosition: 0, + currentPosition: 3, + }); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + + service.cancelSelection(); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Cancelled', selectedEntry: entry }); })); it('should announce a clear if the bitstream has not moved', () => { @@ -359,6 +385,44 @@ describe('ItemBitstreamsService', () => { expect(service.getSelectedBitstream()).toEqual(movedEntry); }); + it('should emit the move', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + const startPosition = 7; + const endPosition = startPosition - 1; + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + } + ); + + const movedEntry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: endPosition, + } + ); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + + service.moveSelectedBitstreamUp(); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Moved', selectedEntry: movedEntry }); + })); + it('should announce the move', () => { const startPosition = 7; const endPosition = startPosition - 1; @@ -424,6 +488,44 @@ describe('ItemBitstreamsService', () => { expect(service.getSelectedBitstream()).toEqual(movedEntry); }); + it('should emit the move', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + const startPosition = 7; + const endPosition = startPosition + 1; + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + } + ); + + const movedEntry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: endPosition, + } + ); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + + service.moveSelectedBitstreamDown(); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Moved', selectedEntry: movedEntry }); + })); + it('should announce the move', () => { const startPosition = 7; const endPosition = startPosition + 1; diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts index 0521bf47f6..7aac79fe69 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts @@ -9,7 +9,7 @@ export function getItemBitstreamsServiceStub(): ItemBitstreamsServiceStub { } export class ItemBitstreamsServiceStub { - getSelectedBitstream$ = jasmine.createSpy('getSelectedBitstream$').and + getSelectionAction$ = jasmine.createSpy('getSelectedBitstream$').and .returnValue(of(null)); getSelectedBitstream = jasmine.createSpy('getSelectedBitstream').and diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts index 5b5fb7d63c..2329107c29 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -87,6 +87,24 @@ export interface SelectedBitstreamTableEntry { currentPosition: number, } +/** + * Interface storing data regarding a change in selected bitstream + */ +export interface SelectionAction { + /** + * The different types of actions: + * - Selected: Bitstream was selected + * - Moved: Bitstream was moved + * - Cleared: Selection was cleared, bitstream remains at its current position + * - Cancelled: Selection was cancelled, bitstream returns to its original position + */ + action: 'Selected' | 'Moved' | 'Cleared' | 'Cancelled' + /** + * The table entry to which the selection action applies + */ + selectedEntry: SelectedBitstreamTableEntry, +} + /** * This service handles the selection and updating of the bitstreams and their order on the * 'Edit Item' -> 'Bitstreams' page. @@ -99,7 +117,7 @@ export class ItemBitstreamsService { /** * BehaviorSubject which emits every time the selected bitstream changes. */ - protected selectedBitstream$: BehaviorSubject = new BehaviorSubject(null); + protected selectionAction$: BehaviorSubject = new BehaviorSubject(null); protected isPerformingMoveRequest = false; @@ -116,45 +134,68 @@ export class ItemBitstreamsService { } /** - * Returns the observable emitting the currently selected bitstream + * Returns the observable emitting the selection actions */ - getSelectedBitstream$(): Observable { - return this.selectedBitstream$; + getSelectionAction$(): Observable { + return this.selectionAction$; + } + + /** + * Returns the latest selection action + */ + getSelectionAction(): SelectionAction { + const action = this.selectionAction$.value; + + if (hasNoValue(action)) { + return null; + } + + return Object.assign({}, action); + } + + /** + * Returns true if there currently is a selected bitstream + */ + hasSelectedBitstream(): boolean { + const selectionAction = this.getSelectionAction(); + + if (hasNoValue(selectionAction)) { + return false; + } + + const action = selectionAction.action; + + return action === 'Selected' || action === 'Moved'; } /** * Returns a copy of the currently selected bitstream */ getSelectedBitstream(): SelectedBitstreamTableEntry { - const selected = this.selectedBitstream$.getValue(); - - if (hasNoValue(selected)) { - return selected; + if (!this.hasSelectedBitstream()) { + return null; } - return Object.assign({}, selected); - } - - hasSelectedBitstream(): boolean { - return hasValue(this.getSelectedBitstream()); + const selectionAction = this.getSelectionAction(); + return Object.assign({}, selectionAction.selectedEntry); } /** * Select the provided entry */ selectBitstreamEntry(entry: SelectedBitstreamTableEntry) { - if (hasValue(entry) && entry !== this.selectedBitstream$.getValue()) { + if (hasValue(entry) && entry.bitstream !== this.getSelectedBitstream()?.bitstream) { this.announceSelect(entry.bitstream.name); - this.updateSelectedBitstream(entry); + this.updateSelectionAction({ action: 'Selected', selectedEntry: entry }); } } /** - * Makes the {@link selectedBitstream$} observable emit the provided {@link SelectedBitstreamTableEntry}. + * Makes the {@link selectionAction$} observable emit the provided {@link SelectedBitstreamTableEntry}. * @protected */ - protected updateSelectedBitstream(entry: SelectedBitstreamTableEntry) { - this.selectedBitstream$.next(entry); + protected updateSelectionAction(action: SelectionAction) { + this.selectionAction$.next(action); } /** @@ -164,7 +205,7 @@ export class ItemBitstreamsService { const selected = this.getSelectedBitstream(); if (hasValue(selected)) { - this.updateSelectedBitstream(null); + this.updateSelectionAction({ action: 'Cleared', selectedEntry: selected }); this.announceClear(selected.bitstream.name); if (selected.currentPosition !== selected.originalPosition) { @@ -184,7 +225,6 @@ export class ItemBitstreamsService { return; } - this.updateSelectedBitstream(null); const originalPosition = selected.originalPosition; const currentPosition = selected.currentPosition; @@ -192,9 +232,11 @@ export class ItemBitstreamsService { // If the selected bitstream has not moved, there is no need to return it to its original position if (currentPosition === originalPosition) { this.announceClear(selected.bitstream.name); + this.updateSelectionAction({ action: 'Cleared', selectedEntry: selected }); } else { this.announceCancel(selected.bitstream.name, originalPosition); this.performBitstreamMoveRequest(selected.bundle, currentPosition, originalPosition); + this.updateSelectionAction({ action: 'Cancelled', selectedEntry: selected }); } } @@ -219,7 +261,7 @@ export class ItemBitstreamsService { }; this.performBitstreamMoveRequest(selected.bundle, originalPosition, newPosition, onRequestCompleted); - this.updateSelectedBitstream(selected); + this.updateSelectionAction({ action: 'Moved', selectedEntry: selected }); } } @@ -244,7 +286,7 @@ export class ItemBitstreamsService { }; this.performBitstreamMoveRequest(selected.bundle, originalPosition, newPosition, onRequestCompleted); - this.updateSelectedBitstream(selected); + this.updateSelectionAction({ action: 'Moved', selectedEntry: selected }); } } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts index 6008b5431f..26a1b0e913 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts @@ -107,15 +107,8 @@ describe('ItemEditBitstreamBundleComponent', () => { }); it('should move to the page the selected entry is on if were not on that page', () => { - const selectedA: SelectedBitstreamTableEntry = { - bitstream: null, - bundle: bundle, - bundleSize: 5, - originalPosition: 1, - currentPosition: 1, - }; - const selectedB: SelectedBitstreamTableEntry = { + const entry: SelectedBitstreamTableEntry = { bitstream: null, bundle: bundle, bundleSize: 5, @@ -123,20 +116,12 @@ describe('ItemEditBitstreamBundleComponent', () => { currentPosition: 2, }; - comp.handleSelectedEntryChange(selectedA, selectedB); + comp.handleSelectionAction({ action: 'Moved', selectedEntry: entry }); expect(paginationComponent.doPageChange).toHaveBeenCalledWith(2); }); it('should not change page when we are already on the correct page', () => { - const selectedA: SelectedBitstreamTableEntry = { - bitstream: null, - bundle: bundle, - bundleSize: 5, - originalPosition: 0, - currentPosition: 0, - }; - - const selectedB: SelectedBitstreamTableEntry = { + const entry: SelectedBitstreamTableEntry = { bitstream: null, bundle: bundle, bundleSize: 5, @@ -144,12 +129,12 @@ describe('ItemEditBitstreamBundleComponent', () => { currentPosition: 1, }; - comp.handleSelectedEntryChange(selectedA, selectedB); + comp.handleSelectionAction({ action: 'Moved', selectedEntry: entry }); expect(paginationComponent.doPageChange).not.toHaveBeenCalled(); }); it('should change to the original page when cancelling', () => { - const selectedA: SelectedBitstreamTableEntry = { + const entry: SelectedBitstreamTableEntry = { bitstream: null, bundle: bundle, bundleSize: 5, @@ -157,14 +142,12 @@ describe('ItemEditBitstreamBundleComponent', () => { currentPosition: 0, }; - const selectedB = null; - - comp.handleSelectedEntryChange(selectedA, selectedB); + comp.handleSelectionAction({ action: 'Cancelled', selectedEntry: entry }); expect(paginationComponent.doPageChange).toHaveBeenCalledWith(2); }); it('should not change page when we are already on the correct page when cancelling', () => { - const selectedA: SelectedBitstreamTableEntry = { + const entry: SelectedBitstreamTableEntry = { bitstream: null, bundle: bundle, bundleSize: 5, @@ -172,9 +155,7 @@ describe('ItemEditBitstreamBundleComponent', () => { currentPosition: 3, }; - const selectedB = null; - - comp.handleSelectedEntryChange(selectedA, selectedB); + comp.handleSelectionAction({ action: 'Cancelled', selectedEntry: entry }); expect(paginationComponent.doPageChange).not.toHaveBeenCalled(); }); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 7a70ba80dd..2c7d8ca60f 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -25,7 +25,7 @@ import { paginatedListToArray, } from '../../../../core/shared/operators'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { map, take, filter, tap, pairwise } from 'rxjs/operators'; +import { map, take, filter, tap } from 'rxjs/operators'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; @@ -35,7 +35,7 @@ import { ItemBitstreamsService, BitstreamTableEntry, SelectedBitstreamTableEntry, - MOVE_KEY + MOVE_KEY, SelectionAction } from '../item-bitstreams.service'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { hasValue, hasNoValue } from '../../../../shared/empty.util'; @@ -233,31 +233,37 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { protected initializeSelectionActions() { this.subscriptions.push( - this.itemBitstreamsService.getSelectedBitstream$().pipe(pairwise()).subscribe( - ([previousSelection, currentSelection]) => - this.handleSelectedEntryChange(previousSelection, currentSelection)) + this.itemBitstreamsService.getSelectionAction$().subscribe( + selectionAction => this.handleSelectionAction(selectionAction)) ); } /** * Handles a change in selected bitstream by changing the pagination if the change happened on a different page - * @param previousSelectedEntry The previously selected entry - * @param currentSelectedEntry The currently selected entry + * @param selectionAction */ - handleSelectedEntryChange( - previousSelectedEntry: SelectedBitstreamTableEntry, - currentSelectedEntry: SelectedBitstreamTableEntry - ) { - if (hasValue(currentSelectedEntry) && currentSelectedEntry.bundle === this.bundle) { - // If the currently selected bitstream belongs to this bundle, it has possibly moved to a different page. - // In that case we want to change the pagination to the new page. - this.redirectToCurrentPage(currentSelectedEntry); + handleSelectionAction(selectionAction: SelectionAction) { + if (hasNoValue(selectionAction) || selectionAction.selectedEntry.bundle !== this.bundle) { + return; } - // If the selection is cancelled or cleared, it is possible the selected bitstream is currently on a different page - // In that case we want to change the pagination to the place where the bitstream was returned to - if (hasNoValue(currentSelectedEntry) && hasValue(previousSelectedEntry) && previousSelectedEntry.bundle === this.bundle) { - this.redirectToOriginalPage(previousSelectedEntry); + if (selectionAction.action === 'Moved') { + // If the currently selected bitstream belongs to this bundle, it has possibly moved to a different page. + // In that case we want to change the pagination to the new page. + this.redirectToCurrentPage(selectionAction.selectedEntry); + } + + if (selectionAction.action === 'Cancelled') { + // If the selection is cancelled (and returned to its original position), it is possible the previously selected + // bitstream is returned to a different page. In that case we want to change the pagination to the place where + // the bitstream was returned to. + this.redirectToOriginalPage(selectionAction.selectedEntry); + } + + if (selectionAction.action === 'Cleared') { + // If the selection is cleared, it is possible the previously selected bitstream is on a different page. In that + // case we want to change the pagination to the place where the bitstream is. + this.redirectToCurrentPage(selectionAction.selectedEntry); } } From 8d93f22767edbf18396edc49b16629967f04c287 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 9 Oct 2024 12:01:33 +0200 Subject: [PATCH 041/123] 119176: Make table horizontally scrollable For most screen sizes, the ResponsiveTableSizes is enough to resize the table columns. On very small screens, or when zoomed in a lot, even the smallest column sizes are too big. To make it possible to view the rest of the content even in these situations, the ability to scroll horizontally is added. --- .../item-bitstreams/item-bitstreams.component.html | 2 +- .../item-bitstreams/item-bitstreams.component.scss | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index b9af2a7d18..7789b68278 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -27,7 +27,7 @@ -
+
Date: Mon, 14 Oct 2024 11:30:48 +0200 Subject: [PATCH 042/123] 119176: Announce notification content in live region --- .../models/notification-options.model.ts | 12 +++- .../notifications-board.component.spec.ts | 49 ++++++++++++++- .../notifications-board.component.ts | 60 +++++++++++++------ 3 files changed, 97 insertions(+), 24 deletions(-) diff --git a/src/app/shared/notifications/models/notification-options.model.ts b/src/app/shared/notifications/models/notification-options.model.ts index 65011496b3..c891781d9d 100644 --- a/src/app/shared/notifications/models/notification-options.model.ts +++ b/src/app/shared/notifications/models/notification-options.model.ts @@ -4,19 +4,25 @@ export interface INotificationOptions { timeOut: number; clickToClose: boolean; animate: NotificationAnimationsType | string; + announceContentInLiveRegion: boolean; } export class NotificationOptions implements INotificationOptions { public timeOut: number; public clickToClose: boolean; public animate: any; + public announceContentInLiveRegion: boolean; - constructor(timeOut = 5000, - clickToClose = true, - animate: NotificationAnimationsType | string = NotificationAnimationsType.Scale) { + constructor( + timeOut = 5000, + clickToClose = true, + animate: NotificationAnimationsType | string = NotificationAnimationsType.Scale, + announceContentInLiveRegion: boolean = true, + ) { this.timeOut = timeOut; this.clickToClose = clickToClose; this.animate = animate; + this.announceContentInLiveRegion = announceContentInLiveRegion; } } diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts b/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts index 08b9585a8c..73f4e6b1b1 100644 --- a/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, inject, TestBed, waitForAsync, fakeAsync, flush } from '@angular/core/testing'; import { BrowserModule, By } from '@angular/platform-browser'; import { ChangeDetectorRef } from '@angular/core'; @@ -15,14 +15,20 @@ import uniqueId from 'lodash/uniqueId'; import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces'; import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { cold } from 'jasmine-marbles'; +import { LiveRegionService } from '../../live-region/live-region.service'; +import { LiveRegionServiceStub } from '../../live-region/live-region.service.stub'; +import { NotificationOptions } from '../models/notification-options.model'; export const bools = { f: false, t: true }; describe('NotificationsBoardComponent', () => { let comp: NotificationsBoardComponent; let fixture: ComponentFixture; + let liveRegionService: LiveRegionServiceStub; beforeEach(waitForAsync(() => { + liveRegionService = new LiveRegionServiceStub(); + TestBed.configureTestingModule({ imports: [ BrowserModule, @@ -36,7 +42,9 @@ describe('NotificationsBoardComponent', () => { declarations: [NotificationsBoardComponent, NotificationComponent], // declare the test component providers: [ { provide: NotificationsService, useClass: NotificationsServiceStub }, - ChangeDetectorRef] + { provide: LiveRegionService, useValue: liveRegionService }, + ChangeDetectorRef, + ] }).compileComponents(); // compile template and css })); @@ -106,5 +114,42 @@ describe('NotificationsBoardComponent', () => { }); }); + describe('add', () => { + beforeEach(() => { + liveRegionService.addMessage.calls.reset(); + }); + + it('should announce content to the live region', fakeAsync(() => { + const notification = new Notification('id', NotificationType.Info, 'title', 'content'); + comp.add(notification); + + flush(); + + expect(liveRegionService.addMessage).toHaveBeenCalledWith('content'); + })); + + it('should not announce anything if there is no content', fakeAsync(() => { + const notification = new Notification('id', NotificationType.Info, 'title'); + comp.add(notification); + + flush(); + + expect(liveRegionService.addMessage).not.toHaveBeenCalled(); + })); + + it('should not announce the content if disabled', fakeAsync(() => { + const options = new NotificationOptions(); + options.announceContentInLiveRegion = false; + + const notification = new Notification('id', NotificationType.Info, 'title', 'content'); + notification.options = options; + comp.add(notification); + + flush(); + + expect(liveRegionService.addMessage).not.toHaveBeenCalled(); + })); + }); + }) ; diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.ts b/src/app/shared/notifications/notifications-board/notifications-board.component.ts index 97ae09c1a6..eaba659678 100644 --- a/src/app/shared/notifications/notifications-board/notifications-board.component.ts +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.ts @@ -9,7 +9,7 @@ import { } from '@angular/core'; import { select, Store } from '@ngrx/store'; -import { BehaviorSubject, Subscription } from 'rxjs'; +import { BehaviorSubject, Subscription, of as observableOf } from 'rxjs'; import difference from 'lodash/difference'; import { NotificationsService } from '../notifications.service'; @@ -18,6 +18,9 @@ import { notificationsStateSelector } from '../selectors'; import { INotification } from '../models/notification.model'; import { NotificationsState } from '../notifications.reducers'; import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces'; +import { LiveRegionService } from '../../live-region/live-region.service'; +import { hasNoValue, isNotEmptyOperator } from '../../empty.util'; +import { take } from 'rxjs/operators'; @Component({ selector: 'ds-notifications-board', @@ -49,9 +52,12 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy { */ public isPaused$: BehaviorSubject = new BehaviorSubject(false); - constructor(private service: NotificationsService, - private store: Store, - private cdr: ChangeDetectorRef) { + constructor( + private service: NotificationsService, + private store: Store, + private cdr: ChangeDetectorRef, + protected liveRegionService: LiveRegionService, + ) { } ngOnInit(): void { @@ -85,6 +91,7 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy { this.notifications.splice(this.notifications.length - 1, 1); } this.notifications.splice(0, 0, item); + this.addContentToLiveRegion(item); } else { // Remove the notification from the store // This notification was in the store, but not in this.notifications @@ -93,29 +100,44 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy { } } + /** + * Adds the content of the notification (if any) to the live region, so it can be announced by screen readers. + */ + private addContentToLiveRegion(item: INotification) { + let content = item.content; + + if (!item.options.announceContentInLiveRegion || hasNoValue(content)) { + return; + } + + if (typeof content === 'string') { + content = observableOf(content); + } + + content.pipe( + isNotEmptyOperator(), + take(1), + ).subscribe(contentStr => this.liveRegionService.addMessage(contentStr)); + } + + /** + * Whether to block the provided item because a duplicate notification with the exact same information already + * exists within the notifications array. + * @param item The item to check + * @return true if the notifications array already contains a notification with the exact same information as the + * provided item. false otherwise. + * @private + */ private block(item: INotification): boolean { const toCheck = item.html ? this.checkHtml : this.checkStandard; + this.notifications.forEach((notification) => { if (toCheck(notification, item)) { return true; } }); - if (this.notifications.length > 0) { - this.notifications.forEach((notification) => { - if (toCheck(notification, item)) { - return true; - } - }); - } - - let comp: INotification; - if (this.notifications.length > 0) { - comp = this.notifications[0]; - } else { - return false; - } - return toCheck(comp, item); + return false; } private checkStandard(checker: INotification, item: INotification): boolean { From 93f9341387755efc14d3272f7f9cca452ab91996 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 16 Oct 2024 14:11:01 +0200 Subject: [PATCH 043/123] 119176: Add aria-labels to buttons --- .../item-edit-bitstream-bundle.component.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index efbdd8c69b..06201b1cbe 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -40,6 +40,7 @@
-
+
+ + diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss index 985516ab12..7fd1f4b31e 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss @@ -43,3 +43,13 @@ .scrollable-table { overflow-x: auto; } + +.disabled-overlay { + opacity: 0.6; +} + +.loading-overlay { + position: fixed; + top: 50%; + left: 50%; +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 6ee5dcb545..72f85675c9 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -59,6 +59,11 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ itemUpdateSubscription: Subscription; + /** + * An observable which emits a boolean which represents whether the service is currently handling a 'move' request + */ + isProcessingMoveRequest: Observable; + constructor( public itemService: ItemDataService, public objectUpdatesService: ObjectUpdatesService, @@ -84,6 +89,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ postItemInit(): void { const bundlesOptions = this.itemBitstreamsService.getInitialBundlesPaginationOptions(); + this.isProcessingMoveRequest = this.itemBitstreamsService.getPerformingMoveRequest$(); this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: bundlesOptions})).pipe( getFirstSucceededRemoteData(), diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts index f2af25f22f..a0277ef064 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts @@ -573,8 +573,6 @@ describe('ItemBitstreamsService', () => { const to = 7; const callback = createSpy('callbackFunction'); - console.log('bundle:', bundle); - it('should correctly create the Move request', () => { const expectedOperation: MoveOperation = { op: 'move', @@ -601,6 +599,22 @@ describe('ItemBitstreamsService', () => { service.performBitstreamMoveRequest(bundle, from, to, callback); expect(callback).toHaveBeenCalled(); }); + + it('should emit at the start and end of the request', fakeAsync(() => { + const emittedActions = []; + + service.getPerformingMoveRequest$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeFalse(); + + service.performBitstreamMoveRequest(bundle, from, to, callback); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[1]).toBeTrue(); + expect(emittedActions[2]).toBeFalse(); + })); }); describe('displayNotifications', () => { diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts index 7aac79fe69..f60693f726 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts @@ -30,6 +30,10 @@ export class ItemBitstreamsServiceStub { performBitstreamMoveRequest = jasmine.createSpy('performBitstreamMoveRequest'); + getPerformingMoveRequest = jasmine.createSpy('getPerformingMoveRequest').and.returnValue(false); + + getPerformingMoveRequest$ = jasmine.createSpy('getPerformingMoveRequest$').and.returnValue(of(false)); + getInitialBundlesPaginationOptions = jasmine.createSpy('getInitialBundlesPaginationOptions').and .returnValue(new PaginationComponentOptions()); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts index 2329107c29..9bbf380487 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -119,7 +119,7 @@ export class ItemBitstreamsService { */ protected selectionAction$: BehaviorSubject = new BehaviorSubject(null); - protected isPerformingMoveRequest = false; + protected isPerformingMoveRequest: BehaviorSubject = new BehaviorSubject(false); constructor( protected notificationsService: NotificationsService, @@ -221,7 +221,7 @@ export class ItemBitstreamsService { cancelSelection() { const selected = this.getSelectedBitstream(); - if (hasNoValue(selected) || this.isPerformingMoveRequest) { + if (hasNoValue(selected) || this.getPerformingMoveRequest()) { return; } @@ -247,7 +247,7 @@ export class ItemBitstreamsService { moveSelectedBitstreamUp() { const selected = this.getSelectedBitstream(); - if (hasNoValue(selected) || this.isPerformingMoveRequest) { + if (hasNoValue(selected) || this.getPerformingMoveRequest()) { return; } @@ -272,7 +272,7 @@ export class ItemBitstreamsService { moveSelectedBitstreamDown() { const selected = this.getSelectedBitstream(); - if (hasNoValue(selected) || this.isPerformingMoveRequest) { + if (hasNoValue(selected) || this.getPerformingMoveRequest()) { return; } @@ -299,7 +299,7 @@ export class ItemBitstreamsService { * @param finish Optional: Function to execute once the response has been received */ performBitstreamMoveRequest(bundle: Bundle, fromIndex: number, toIndex: number, finish?: () => void) { - if (this.isPerformingMoveRequest) { + if (this.getPerformingMoveRequest()) { console.warn('Attempted to perform move request while previous request has not completed yet'); return; } @@ -310,18 +310,34 @@ export class ItemBitstreamsService { path: `/_links/bitstreams/${toIndex}/href`, }; - this.isPerformingMoveRequest = true; + this.announceLoading(); + this.isPerformingMoveRequest.next(true); this.bundleService.patch(bundle, [moveOperation]).pipe( getFirstCompletedRemoteData(), tap((response: RemoteData) => this.displayFailedResponseNotifications(MOVE_KEY, [response])), switchMap(() => this.requestService.setStaleByHrefSubstring(bundle.self)), take(1), ).subscribe(() => { - this.isPerformingMoveRequest = false; + console.log('got here!'); + this.isPerformingMoveRequest.next(false); finish?.(); }); } + /** + * Whether the service currently is processing a 'move' request + */ + getPerformingMoveRequest(): boolean { + return this.isPerformingMoveRequest.value; + } + + /** + * Returns an observable which emits when the service starts, or ends, processing a 'move' request + */ + getPerformingMoveRequest$(): Observable { + return this.isPerformingMoveRequest; + } + /** * Returns the pagination options to use when fetching the bundles */ @@ -526,4 +542,12 @@ export class ItemBitstreamsService { { bitstream: bitstreamName }); this.liveRegionService.addMessage(message); } + + /** + * Adds a message to the live region mentioning that the + */ + announceLoading() { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.loading'); + this.liveRegionService.addMessage(message); + } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 519189ed69..9007982a72 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1954,6 +1954,8 @@ "item.edit.bitstreams.edit.live.clear": "{{ bitstream }} is no longer selected.", + "item.edit.bitstreams.edit.live.loading": "Waiting for move to complete.", + "item.edit.bitstreams.edit.live.select": "{{ bitstream }} is selected.", "item.edit.bitstreams.edit.live.move": "{{ bitstream }} is now in position {{ toIndex }}.", From 59e5f71a73053f8633446a1f43965ee99e4693f6 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Tue, 29 Oct 2024 17:02:42 +0100 Subject: [PATCH 046/123] 117287: Fixed group form not working correctly anymore when switching between different groups Also fixed edit ePerson not showing the group names --- .../eperson-form/eperson-form.component.html | 4 +- .../group-form/group-form.component.ts | 46 ++++++++++++------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html index 3c5b2d13b4..16948919ba 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html @@ -71,7 +71,9 @@ {{ dsoNameService.getName(group) }} -
{{ dsoNameService.getName(undefined) }} + {{ dsoNameService.getName((group.object | async)?.payload) }} +
diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts index 37ce30473f..c0ea034fbb 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.ts @@ -13,7 +13,7 @@ import { Observable, Subscription, combineLatest, } from 'rxjs'; -import { map, switchMap, take, debounceTime, startWith, filter } from 'rxjs/operators'; +import { map, switchMap, take, debounceTime } from 'rxjs/operators'; import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths'; import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths'; import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; @@ -35,7 +35,7 @@ import { } from '../../../core/shared/operators'; import { AlertType } from '../../../shared/alert/aletr-type'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; -import { hasValue, isNotEmpty, hasValueOperator, hasNoValue } from '../../../shared/empty.util'; +import { hasValue, isNotEmpty, hasValueOperator } from '../../../shared/empty.util'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { followLink } from '../../../shared/utils/follow-link-config.model'; @@ -164,11 +164,16 @@ export class GroupFormComponent implements OnInit, OnDestroy { this.activeGroupLinkedDSO$ = this.getActiveGroupLinkedDSO(); this.linkedEditRolesRoute$ = this.getLinkedEditRolesRoute(); this.canEdit$ = this.activeGroupLinkedDSO$.pipe( - filter((dso: DSpaceObject) => hasNoValue(dso)), - switchMap(() => this.activeGroup$), - hasValueOperator(), - switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self)), - startWith(false), + switchMap((dso: DSpaceObject) => { + if (hasValue(dso)) { + return [false]; + } else { + return this.activeGroup$.pipe( + hasValueOperator(), + switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self)), + ); + } + }), ); this.initialisePage(); } @@ -216,7 +221,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { combineLatest([ this.activeGroup$, this.canEdit$, - this.activeGroupLinkedDSO$.pipe(take(1)), + this.activeGroupLinkedDSO$, ]).subscribe(([activeGroup, canEdit, linkedObject]) => { if (activeGroup != null) { @@ -224,25 +229,31 @@ export class GroupFormComponent implements OnInit, OnDestroy { // Disable group name exists validator this.formGroup.controls.groupName.clearAsyncValidators(); - if (linkedObject?.name) { - this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel); - this.groupDescription = this.formGroup.get('groupCommunity'); + if (isNotEmpty(linkedObject?.name)) { + if (!this.formGroup.controls.groupCommunity) { + this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel); + this.groupDescription = this.formGroup.get('groupCommunity'); + } this.formGroup.patchValue({ groupName: activeGroup.name, groupCommunity: linkedObject?.name ?? '', groupDescription: activeGroup.firstMetadataValue('dc.description'), }); } else { + this.formModel = [ + groupNameModel, + groupDescriptionModel, + ]; this.formGroup.patchValue({ groupName: activeGroup.name, groupDescription: activeGroup.firstMetadataValue('dc.description'), }); } - setTimeout(() => { - if (!canEdit || activeGroup.permanent) { - this.formGroup.disable(); - } - }, 200); + if (!canEdit || activeGroup.permanent) { + this.formGroup.disable(); + } else { + this.formGroup.enable(); + } } }) ); @@ -471,6 +482,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { */ getLinkedEditRolesRoute(): Observable { return this.activeGroupLinkedDSO$.pipe( + hasValueOperator(), map((dso: DSpaceObject) => { switch ((dso as any).type) { case Community.type.value: @@ -478,7 +490,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { case Collection.type.value: return getCollectionEditRolesRoute(dso.id); } - }) + }), ); } } From 26f4d1d329d21f1661b6e10bd06362897c7521d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 02:46:18 +0000 Subject: [PATCH 047/123] Bump sass from 1.80.4 to 1.80.6 in the sass group Bumps the sass group with 1 update: [sass](https://github.com/sass/dart-sass). Updates `sass` from 1.80.4 to 1.80.6 - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.80.4...1.80.6) --- updated-dependencies: - dependency-name: sass dependency-type: direct:development update-type: version-update:semver-patch dependency-group: sass ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e2923712c0..67e416e6e1 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "react-copy-to-clipboard": "^5.1.0", "react-dom": "^16.14.0", "rimraf": "^3.0.2", - "sass": "~1.80.4", + "sass": "~1.80.6", "sass-loader": "^12.6.0", "sass-resources-loader": "^2.2.5", "ts-node": "^8.10.2", diff --git a/yarn.lock b/yarn.lock index 21dcdd6fa5..69b50f026e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10234,15 +10234,16 @@ sass@1.58.1: immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" -sass@^1.25.0, sass@~1.80.4: - version "1.80.4" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.80.4.tgz#bc0418fd796cad2f1a1309d8b4d7fe44b7027de0" - integrity sha512-rhMQ2tSF5CsuuspvC94nPM9rToiAFw2h3JTrLlgmNw1MH79v8Cr3DH6KF6o6r+8oofY3iYVPUf66KzC8yuVN1w== +sass@^1.25.0, sass@~1.80.6: + version "1.80.6" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.80.6.tgz#5d0aa55763984effe41e40019c9571ab73e6851f" + integrity sha512-ccZgdHNiBF1NHBsWvacvT5rju3y1d/Eu+8Ex6c21nHp2lZGLBEtuwc415QfiI1PJa1TpCo3iXwwSRjRpn2Ckjg== dependencies: - "@parcel/watcher" "^2.4.1" chokidar "^4.0.0" immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" + optionalDependencies: + "@parcel/watcher" "^2.4.1" sax@^1.2.4: version "1.2.4" From 2eb120909b6b917d3f82852382429e391ace7bfd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 02:46:50 +0000 Subject: [PATCH 048/123] Bump compression from 1.7.4 to 1.7.5 Bumps [compression](https://github.com/expressjs/compression) from 1.7.4 to 1.7.5. - [Release notes](https://github.com/expressjs/compression/releases) - [Changelog](https://github.com/expressjs/compression/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/compression/compare/1.7.4...1.7.5) --- updated-dependencies: - dependency-name: compression dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 36 ++++++++++++++++++------------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index e2923712c0..24c674d8d6 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "cerialize": "0.1.18", "cli-progress": "^3.12.0", "colors": "^1.4.0", - "compression": "^1.7.4", + "compression": "^1.7.5", "cookie-parser": "1.4.7", "core-js": "^3.38.1", "date-fns": "^2.30.0", diff --git a/yarn.lock b/yarn.lock index 21dcdd6fa5..0208ffe1a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2983,7 +2983,7 @@ abbrev@1, abbrev@^1.0.0: resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: +accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -3710,11 +3710,6 @@ builtins@^5.0.0: dependencies: semver "^7.0.0" -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" - integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== - bytes@3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" @@ -4119,7 +4114,7 @@ commondir@^1.0.1: resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== -compressible@~2.0.16: +compressible@~2.0.18: version "2.0.18" resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== @@ -4134,17 +4129,17 @@ compression-webpack-plugin@^9.2.0: schema-utils "^4.0.0" serialize-javascript "^6.0.0" -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== +compression@^1.7.4, compression@^1.7.5: + version "1.7.5" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.5.tgz#fdd256c0a642e39e314c478f6c2cd654edd74c93" + integrity sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q== dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" + bytes "3.1.2" + compressible "~2.0.18" debug "2.6.9" + negotiator "~0.6.4" on-headers "~1.0.2" - safe-buffer "5.1.2" + safe-buffer "5.2.1" vary "~1.1.2" concat-map@0.0.1: @@ -8351,11 +8346,16 @@ needle@^3.1.0: iconv-lite "^0.6.3" sax "^1.2.4" -negotiator@0.6.3, negotiator@^0.6.3: +negotiator@0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +negotiator@^0.6.3, negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + neo-async@^2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" @@ -10158,12 +10158,12 @@ safe-array-concat@^1.1.2: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@5.1.2, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== From 1b77530a3187f5e991a686876053b72a16d9ddcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 02:47:05 +0000 Subject: [PATCH 049/123] Bump @types/lodash from 4.17.12 to 4.17.13 Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.17.12 to 4.17.13. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash) --- updated-dependencies: - dependency-name: "@types/lodash" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e2923712c0..6aef716bd5 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "@types/grecaptcha": "^3.0.9", "@types/jasmine": "~3.6.0", "@types/js-cookie": "2.2.6", - "@types/lodash": "^4.17.12", + "@types/lodash": "^4.17.13", "@types/node": "^14.18.63", "@types/sanitize-html": "^2.13.0", "@typescript-eslint/eslint-plugin": "^5.62.0", diff --git a/yarn.lock b/yarn.lock index 21dcdd6fa5..a44db790f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2539,10 +2539,10 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@^4.17.12": - version "4.17.12" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.12.tgz#25d71312bf66512105d71e55d42e22c36bcfc689" - integrity sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ== +"@types/lodash@^4.17.13": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.13.tgz#786e2d67cfd95e32862143abe7463a7f90c300eb" + integrity sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg== "@types/mime@*": version "3.0.1" From 87f5b502010581ad4d8f3152aa126dbc6c6bbe62 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Thu, 7 Nov 2024 19:58:01 +0100 Subject: [PATCH 050/123] update comment to correctly describe component's purpose (cherry picked from commit 33dddc697fb86d449998900ab82e4f876631eaac) --- .../bitstream-authorizations.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts b/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts index adc0638780..90b4151a9d 100644 --- a/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts +++ b/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts @@ -12,7 +12,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model'; templateUrl: './bitstream-authorizations.component.html', }) /** - * Component that handles the Collection Authorizations + * Component that handles the Bitstream Authorizations */ export class BitstreamAuthorizationsComponent implements OnInit { From 9f879d38501f68e068350b567dafbe9b0a858d92 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Thu, 7 Nov 2024 20:02:39 +0100 Subject: [PATCH 051/123] fix invalid selector (cherry picked from commit 752951ce3b05436a13d689fc492fa0b95563862e) --- .../bitstream-authorizations.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts b/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts index 90b4151a9d..72683f5d74 100644 --- a/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts +++ b/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts @@ -8,7 +8,7 @@ import { RemoteData } from '../../core/data/remote-data'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; @Component({ - selector: 'ds-collection-authorizations', + selector: 'ds-bitstream-authorizations', templateUrl: './bitstream-authorizations.component.html', }) /** From 4353c57cd045f912f27bf8c3dce2a73497ff7d42 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 29 Oct 2024 13:58:50 -0500 Subject: [PATCH 052/123] Fix Klaro translations by forcing Klaro to use a 'zy' language key which DSpace will translate (cherry picked from commit 6076423907e22707a4c31c7c96d1b74ca6b0d81c) --- .../cookies/browser-klaro.service.spec.ts | 6 +++--- .../shared/cookies/browser-klaro.service.ts | 8 +++---- src/app/shared/cookies/klaro-configuration.ts | 21 +++++++++++++------ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/app/shared/cookies/browser-klaro.service.spec.ts b/src/app/shared/cookies/browser-klaro.service.spec.ts index 7fd72b54b3..3da3a8b7a3 100644 --- a/src/app/shared/cookies/browser-klaro.service.spec.ts +++ b/src/app/shared/cookies/browser-klaro.service.spec.ts @@ -101,7 +101,7 @@ describe('BrowserKlaroService', () => { mockConfig = { translations: { - zz: { + zy: { purposes: {}, test: { testeritis: testKey @@ -159,8 +159,8 @@ describe('BrowserKlaroService', () => { it('addAppMessages', () => { service.addAppMessages(); - expect(mockConfig.translations.zz[appName]).toBeDefined(); - expect(mockConfig.translations.zz.purposes[purpose]).toBeDefined(); + expect(mockConfig.translations.zy[appName]).toBeDefined(); + expect(mockConfig.translations.zy.purposes[purpose]).toBeDefined(); }); it('translateConfiguration', () => { diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts index 2b09c0bf15..adcb59e146 100644 --- a/src/app/shared/cookies/browser-klaro.service.ts +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -91,7 +91,7 @@ export class BrowserKlaroService extends KlaroService { initialize() { if (!environment.info.enablePrivacyStatement) { delete this.klaroConfig.privacyPolicy; - this.klaroConfig.translations.zz.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy'; + this.klaroConfig.translations.zy.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy'; } const hideGoogleAnalytics$ = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe( @@ -238,12 +238,12 @@ export class BrowserKlaroService extends KlaroService { */ addAppMessages() { this.klaroConfig.services.forEach((app) => { - this.klaroConfig.translations.zz[app.name] = { + this.klaroConfig.translations.zy[app.name] = { title: this.getTitleTranslation(app.name), description: this.getDescriptionTranslation(app.name) }; app.purposes.forEach((purpose) => { - this.klaroConfig.translations.zz.purposes[purpose] = this.getPurposeTranslation(purpose); + this.klaroConfig.translations.zy.purposes[purpose] = this.getPurposeTranslation(purpose); }); }); } @@ -257,7 +257,7 @@ export class BrowserKlaroService extends KlaroService { */ this.translateService.setDefaultLang(environment.defaultLanguage); - this.translate(this.klaroConfig.translations.zz); + this.translate(this.klaroConfig.translations.zy); } /** diff --git a/src/app/shared/cookies/klaro-configuration.ts b/src/app/shared/cookies/klaro-configuration.ts index f527f7f096..6ec4855e28 100644 --- a/src/app/shared/cookies/klaro-configuration.ts +++ b/src/app/shared/cookies/klaro-configuration.ts @@ -17,7 +17,7 @@ export const GOOGLE_ANALYTICS_KLARO_KEY = 'google-analytics'; /** * Klaro configuration - * For more information see https://kiprotect.com/docs/klaro/annotated-config + * For more information see https://klaro.org/docs/integration/annotated-configuration */ export const klaroConfiguration: any = { storageName: ANONYMOUS_STORAGE_NAME_KLARO, @@ -47,21 +47,30 @@ export const klaroConfiguration: any = { htmlTexts: true, + /* + Force Klaro to use our custom "zy" lang configs defined below. + */ + lang: 'zy', + /* You can overwrite existing translations and add translations for your app descriptions and purposes. See `src/translations/` for a full list of translations that can be overwritten: - https://github.com/KIProtect/klaro/tree/master/src/translations + https://github.com/klaro-org/klaro-js/tree/master/src/translations */ translations: { /* - The `zz` key contains default translations that will be used as fallback values. - This can e.g. be useful for defining a fallback privacy policy URL. - FOR DSPACE: We use 'zz' to map to our own i18n translations for klaro, see + For DSpace we use this custom 'zy' key to map to our own i18n translations for klaro, see translateConfiguration() in browser-klaro.service.ts. All the below i18n keys are specified in your /src/assets/i18n/*.json5 translation pack. + This 'zy' key has no special meaning to Klaro & is not a valid language code. It just + allows DSpace to override Klaro's own translations in favor of DSpace's i18n keys. + NOTE: we do not use 'zz' as that has special meaning to Klaro and is ONLY used as a "fallback" + if no other translations can be found within Klaro. Currently, a bug in Klaro means that + 'zz' is never used as there's no way to exclude translations: + https://github.com/klaro-org/klaro-js/issues/515 */ - zz: { + zy: { acceptAll: 'cookies.consent.accept-all', acceptSelected: 'cookies.consent.accept-selected', close: 'cookies.consent.close', From 64628f7e0d92d8bbee3078719eabeea4aa4275a9 Mon Sep 17 00:00:00 2001 From: andreaNeki Date: Fri, 13 Sep 2024 16:34:16 -0300 Subject: [PATCH 053/123] DSpace#2668 - Adding and changing classes in global scss to make cookie settings more accessible --- src/styles/_custom_variables.scss | 1 + src/styles/_global-styles.scss | 34 +++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index 7171aea689..aa67acac1c 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -137,4 +137,5 @@ --green1: #1FB300; // This variable represents the success color for the Klaro cookie banner --button-text-color-cookie: #333; // This variable represents the text color for buttons in the Klaro cookie banner + --very-dark-cyan: #215E50; // This variable represents the background color of the save cookies button } diff --git a/src/styles/_global-styles.scss b/src/styles/_global-styles.scss index b3120c08cd..99cc075dbe 100644 --- a/src/styles/_global-styles.scss +++ b/src/styles/_global-styles.scss @@ -43,17 +43,39 @@ body { .cm-btn.cm-btn-success { color: var(--button-text-color-cookie); background-color: var(--green1); - } - .cm-btn.cm-btn-success.cm-btn-accept-all { - color: var(--button-text-color-cookie); - background-color: var(--green1); + font-weight: 600; } } } -.klaro .cookie-modal a, .klaro .context-notice a, .klaro .cookie-notice a -{ +.klaro .cookie-modal .cm-btn.cm-btn-success.cm-btn-accept-all { + color: var(--button-text-color-cookie); + background-color: var(--green1); + font-weight: 600; +} + +.klaro .cookie-modal a, +.klaro .context-notice a, +.klaro .cookie-notice a { color: var(--green1); + text-decoration: underline !important; +} + +.klaro .cookie-modal .cm-modal .cm-body span, +.klaro + .cookie-modal + .cm-modal + .cm-body + ul.cm-purposes + li.cm-purpose + span.cm-required, +p.purposes, +.klaro .cookie-modal .cm-modal .cm-footer .cm-powered-by a { + color: var(--bs-light) !important; +} + +.klaro .cookie-modal .cm-btn.cm-btn-info { + background-color: var(--very-dark-cyan) !important; } .media-viewer From bc50efec5e4387bfbae6f1405717d9d2c5bed0eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:35:42 +0000 Subject: [PATCH 054/123] Bump core-js from 3.38.1 to 3.39.0 Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.38.1 to 3.39.0. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/commits/v3.39.0/packages/core-js) --- updated-dependencies: - dependency-name: core-js dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b3dbb92115..c4fad1e06a 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "colors": "^1.4.0", "compression": "^1.7.5", "cookie-parser": "1.4.7", - "core-js": "^3.38.1", + "core-js": "^3.39.0", "date-fns": "^2.30.0", "date-fns-tz": "^1.3.7", "deepmerge": "^4.3.1", diff --git a/yarn.lock b/yarn.lock index e6c67e0e80..eedca951f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4272,10 +4272,10 @@ core-js-compat@^3.25.1: dependencies: browserslist "^4.21.5" -core-js@^3.38.1: - version "3.38.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.1.tgz#aa375b79a286a670388a1a363363d53677c0383e" - integrity sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw== +core-js@^3.39.0: + version "3.39.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.39.0.tgz#57f7647f4d2d030c32a72ea23a0555b2eaa30f83" + integrity sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g== core-util-is@1.0.2: version "1.0.2" From 06783364c41f66dc237f606ad294ef67fc87c89f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 03:15:03 +0000 Subject: [PATCH 055/123] Bump express-static-gzip from 2.1.8 to 2.2.0 Bumps [express-static-gzip](https://github.com/tkoenig89/express-static-gzip) from 2.1.8 to 2.2.0. - [Release notes](https://github.com/tkoenig89/express-static-gzip/releases) - [Commits](https://github.com/tkoenig89/express-static-gzip/compare/v2.1.8...v2.2.0) --- updated-dependencies: - dependency-name: express-static-gzip dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index f8f725fafc..7381cdfa97 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,7 @@ "eslint-plugin-jsonc": "^2.16.0", "eslint-plugin-lodash": "^7.4.0", "eslint-plugin-unused-imports": "^2.0.0", - "express-static-gzip": "^2.1.8", + "express-static-gzip": "^2.2.0", "jasmine-core": "^3.8.0", "jasmine-marbles": "0.9.2", "karma": "^6.4.4", diff --git a/yarn.lock b/yarn.lock index 0106427952..aa4fb3ed3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5609,11 +5609,12 @@ express-rate-limit@^5.1.3: resolved "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz" integrity sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg== -express-static-gzip@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/express-static-gzip/-/express-static-gzip-2.1.8.tgz#f37f0fe9e8113e56cfac63a98c0197ee6bd6458f" - integrity sha512-g8tiJuI9Y9Ffy59ehVXvqb0hhP83JwZiLxzanobPaMbkB5qBWA8nuVgd+rcd5qzH3GkgogTALlc0BaADYwnMbQ== +express-static-gzip@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/express-static-gzip/-/express-static-gzip-2.2.0.tgz#7c3f7dd89da68e51c591edf02e6de6169c017f5f" + integrity sha512-4ZQ0pHX0CAauxmzry2/8XFLM6aZA4NBvg9QezSlsEO1zLnl7vMFa48/WIcjzdfOiEUS4S1npPPKP2NHHYAp6qg== dependencies: + parseurl "^1.3.3" serve-static "^1.16.2" express@^4.17.3, express@^4.18.2, express@^4.21.1: @@ -8988,7 +8989,7 @@ parse5@^7.0.0, parse5@^7.1.1, parse5@^7.1.2: dependencies: entities "^4.4.0" -parseurl@~1.3.2, parseurl@~1.3.3: +parseurl@^1.3.3, parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== From 6b32d04aecc57586318b919fa5fadaeb032772a8 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 26 Jul 2024 15:32:57 +0200 Subject: [PATCH 056/123] Updated some messages following the lindat v5 and clarin-dspace v7 instance. (cherry picked from commit b10563ea5388e8c398ec8927dc562d6f841473db) --- src/assets/i18n/cs.json5 | 101 +++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 53 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 0b4168cca9..3824e9741a 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -1477,8 +1477,8 @@ // "bitstream.download.page": "Now downloading {{bitstream}}...", "bitstream.download.page": "Nyní se stahuje {{bitstream}}..." , - // "bitstream.download.page.back": "Back", - "bitstream.download.page.back": "Zpět" , + // "bitstream.download.page.back": "Back" , + "bitstream.download.page.back": "Zpět", // "bitstream.edit.authorizations.link": "Edit bitstream's Policies", "bitstream.edit.authorizations.link": "Upravit politiky souboru", @@ -1797,7 +1797,7 @@ // "claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow", "claimed-declined-task-search-result-list-element.title": "Zamítnuto, posláno zpět správci schvalovacího workflow", - // "collection.create.breadcrumbs": "Create collection", + // "collection.create.breadcrumbs": "Create collection", // TODO New key - Add a translation "collection.create.breadcrumbs": "Create collection", @@ -2210,8 +2210,7 @@ "collection.source.controls.harvest.last": "Naposledy harvestováno:", // "collection.source.controls.harvest.message": "Harvest info:", - "collection.source.controls.harvest.message": "Informace o harevstu:", - + "collection.source.controls.harvest.message": "Informace o harvestu:", // "collection.source.controls.harvest.no-information": "N/A", "collection.source.controls.harvest.no-information": "Není k dispozi", @@ -2703,7 +2702,7 @@ "dso-selector.create.community.head": "Nová komunita", // "dso-selector.create.community.or-divider": "or", - "dso-selector.create.community.or-divider": "or", + "dso-selector.create.community.or-divider": "nebo", // "dso-selector.create.community.sub-level": "Create a new community in", "dso-selector.create.community.sub-level": "Vytvořit novou komunitu v", @@ -2940,7 +2939,7 @@ "error.top-level-communities": "Chyba při načítání komunit nejvyšší úrovně", // "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", - "error.validation.license.notgranted": "eslání. Pokud tuto licenci v tuto chvíli nemůžete udělit, můžete svou práci uložit a vrátit se k ní později nebo podání odstranit.", + "error.validation.license.notgranted": "Pro dokončení zaslání musíte udělit licenci. Pokud v tuto chvíli tuto licenci nemůžete udělit, můžete svou práci uložit a později se k svému příspěveku vrátit nebo jej smazat.", // "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", "error.validation.pattern": "Toto zadání je omezeno aktuálním vzorcem: {{ pattern }}.", @@ -3252,7 +3251,7 @@ "grant-deny-request-copy.intro2": "Po výběru možnosti se zobrazí návrh e-mailové odpovědi, který můžete upravit.", // "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-deny-request-copy.processed": " Tato žádost již byla zpracována. Pomocí níže uvedeného tlačítka se můžete vrátit na domovskou stránku.", + "grant-deny-request-copy.processed": "Tato žádost již byla zpracována. Pomocí níže uvedeného tlačítka se můžete vrátit na domovskou stránku.", // "grant-request-copy.email.subject": "Request copy of document", "grant-request-copy.email.subject": "Žádost o kopii dokumentu", @@ -3432,8 +3431,7 @@ "info.coar-notify-support.breadcrumbs": "COAR Notify Support", // "item.alerts.private": "This item is non-discoverable", - // TODO Source message changed - Revise the translation - "item.alerts.private": "Tento záznam je nezobrazitelný", + "item.alerts.private": "Tento záznam je nevyhledatelný", // "item.alerts.withdrawn": "This item has been withdrawn", "item.alerts.withdrawn": "Tento záznam byl stažen", @@ -4168,7 +4166,7 @@ "item.page.journal-title": "Název časopisu", // "item.page.publisher": "Publisher", - "item.page.publisher": "Vydavatel", + "item.page.publisher": "Nakladatel", // "item.page.titleprefix": "Item: ", "item.page.titleprefix": "Záznam: ", @@ -4268,7 +4266,7 @@ "item.page.abstract": "Abstrakt", // "item.page.author": "Authors", - "item.page.author": "Autor", + "item.page.author": "Autoři", // "item.page.citation": "Citation", "item.page.citation": "Citace", @@ -4310,10 +4308,10 @@ "item.page.journal.search.title": "Články v tomto časopise", // "item.page.link.full": "Full item page", - "item.page.link.full": "Úplný záznam", + "item.page.link.full": "Zobrazit celý záznam", // "item.page.link.simple": "Simple item page", - "item.page.link.simple": "Jednoduchý záznam", + "item.page.link.simple": "Zobrazit minimální záznam", // "item.page.orcid.title": "ORCID", "item.page.orcid.title": "ORCID", @@ -4346,7 +4344,7 @@ "item.page.subject": "Klíčová slova", // "item.page.uri": "URI", - "item.page.uri": "URI", + "item.page.uri": "Identifikátor", // "item.page.bitstreams.view-more": "Show more", "item.page.bitstreams.view-more": "Zobrazit více", @@ -4461,10 +4459,10 @@ "item.preview.oaire.awardNumber": "Identifikátor zdroje financování:", // "item.preview.dc.title.alternative": "Acronym:", - "item.preview.dc.title.alternative": "Akronym:", + "item.preview.dc.title.alternative": "Zkratka:", // "item.preview.dc.coverage.spatial": "Jurisdiction:", - "item.preview.dc.coverage.spatial": "Jurisdikce:", + "item.preview.dc.coverage.spatial": "Příslušnost:", // "item.preview.oaire.fundingStream": "Funding Stream:", "item.preview.oaire.fundingStream": "Tok finančních prostředků:", @@ -4841,7 +4839,7 @@ "journal.page.issn": "ISSN", // "journal.page.publisher": "Publisher", - "journal.page.publisher": "Vydavatel", + "journal.page.publisher": "Nakladatel", // "journal.page.titleprefix": "Journal: ", "journal.page.titleprefix": "Časopis: ", @@ -4936,7 +4934,7 @@ "iiif.page.doi": "Trvalý odkaz: ", // "iiif.page.issue": "Issue: ", - "iiif.page.issue": "Číslo: ", + "iiif.page.issue": "Problém:", // "iiif.page.description": "Description: ", "iiif.page.description": "Popis: ", @@ -5042,8 +5040,7 @@ "menu.header.nav.description": "Admin navigation bar", // "menu.header.admin": "Management", - // TODO Source message changed - Revise the translation - "menu.header.admin": "Management", + "menu.header.admin": "Admin", // "menu.header.image.logo": "Repository logo", "menu.header.image.logo": "Logo úložiště", @@ -5378,7 +5375,7 @@ "mydspace.new-submission-external-short": "Importovat metadata", // "mydspace.results.head": "Your submissions", - "mydspace.results.head": "Váš příspěvek", + "mydspace.results.head": "Vaše zázmany", // "mydspace.results.no-abstract": "No Abstract", "mydspace.results.no-abstract": "Žádný abstrakt", @@ -5390,7 +5387,7 @@ "mydspace.results.no-collections": "Žádné kolekce", // "mydspace.results.no-date": "No Date", - "mydspace.results.no-date": "Žádné datum", + "mydspace.results.no-date": "Žádny datum", // "mydspace.results.no-files": "No Files", "mydspace.results.no-files": "Žádné soubory", @@ -5412,9 +5409,9 @@ // TODO Source message changed - Revise the translation "mydspace.show.workflow": "Úlohy workflow", - // "mydspace.show.workspace": "Your submissions", + // "mydspace.show.workspace": "Your Submissions", // TODO Source message changed - Revise the translation - "mydspace.show.workspace": "Váš příspěvek", + "mydspace.show.workspace": "Vaše záznamy", // "mydspace.show.supervisedWorkspace": "Supervised items", "mydspace.show.supervisedWorkspace": "Zkontrolované záznamy", @@ -5488,7 +5485,7 @@ "nav.user-profile-menu-and-logout": "Menu uživatelského profilu a odhlášení", // "nav.logout": "Log Out", - "nav.logout": "Menu uživatelského profilu a odhlášení", + "nav.logout": "Odhlásit se", // "nav.main.description": "Main navigation bar", "nav.main.description": "Hlavní navigační panel", @@ -6289,7 +6286,7 @@ "publication.page.journal-title": "Název časopisu", // "publication.page.publisher": "Publisher", - "publication.page.publisher": "Vydavatel", + "publication.page.publisher": "Nakladatel", // "publication.page.titleprefix": "Publication: ", "publication.page.titleprefix": "Publikace: ", @@ -6913,7 +6910,7 @@ "search.filters.applied.f.subject": "Předmět", // "search.filters.applied.f.submitter": "Submitter", - "search.filters.applied.f.submitter": "Vkladatel", + "search.filters.applied.f.submitter": "Předkladatel", // "search.filters.applied.f.jobTitle": "Job Title", "search.filters.applied.f.jobTitle": "Název pracovní pozice", @@ -7019,10 +7016,10 @@ "search.filters.filter.creativeWorkKeywords.label": "Předmět hledání", // "search.filters.filter.creativeWorkPublisher.head": "Publisher", - "search.filters.filter.creativeWorkPublisher.head": "Vydavatel", + "search.filters.filter.creativeWorkPublisher.head": "Nakladatel", // "search.filters.filter.creativeWorkPublisher.placeholder": "Publisher", - "search.filters.filter.creativeWorkPublisher.placeholder": "Vydavatel", + "search.filters.filter.creativeWorkPublisher.placeholder": "Nakladatel", // "search.filters.filter.creativeWorkPublisher.label": "Search publisher", "search.filters.filter.creativeWorkPublisher.label": "Hledat vydavatele", @@ -7058,7 +7055,7 @@ "search.filters.filter.discoverable.head": "Nedohledatelné", // "search.filters.filter.withdrawn.head": "Withdrawn", - "search.filters.filter.withdrawn.head": "Zrušeno", + "search.filters.filter.withdrawn.head": "Vyřazeno", // "search.filters.filter.entityType.head": "Item Type", "search.filters.filter.entityType.head": "Typ záznamu", @@ -7142,7 +7139,7 @@ "search.filters.filter.objectpeople.placeholder": "Lidé", // "search.filters.filter.objectpeople.label": "Search people", - "search.filters.filter.objectpeople.label": "Hledat lidi", + "search.filters.filter.objectpeople.label": "Hledat osoby", // "search.filters.filter.organizationAddressCountry.head": "Country", "search.filters.filter.organizationAddressCountry.head": "Stát", @@ -7178,7 +7175,7 @@ "search.filters.filter.scope.placeholder": "Filtr rozsahu", // "search.filters.filter.scope.label": "Search scope filter", - "search.filters.filter.scope.label": "Hledat filtr rozsahu", + "search.filters.filter.scope.label": "Filtr rozsahu hledání", // "search.filters.filter.show-less": "Collapse", "search.filters.filter.show-less": "Sbalit", @@ -7196,13 +7193,13 @@ "search.filters.filter.subject.label": "Hledat předmět", // "search.filters.filter.submitter.head": "Submitter", - "search.filters.filter.submitter.head": "Vkladatel", + "search.filters.filter.submitter.head": "Předkladatel", // "search.filters.filter.submitter.placeholder": "Submitter", - "search.filters.filter.submitter.placeholder": "Vkladatel", + "search.filters.filter.submitter.placeholder": "Předkladatel", // "search.filters.filter.submitter.label": "Search submitter", - "search.filters.filter.submitter.label": "Hledat vkladatele", + "search.filters.filter.submitter.label": "Hledat předkladatele", // "search.filters.filter.show-tree": "Browse {{ name }} tree", "search.filters.filter.show-tree": "Procházet {{ name }} podle", @@ -7286,8 +7283,8 @@ // "search.filters.withdrawn.false": "No", "search.filters.withdrawn.false": "Ne", - // "search.filters.head": "Filters", - "search.filters.head": "Filtry", + // "search.filters.head": "Limit your search", + "search.filters.head": "Zúžit hledání", // "search.filters.reset": "Reset filters", "search.filters.reset": "Resetovat filtry", @@ -7526,7 +7523,7 @@ "submission.general.cannot_submit": "Nemáte oprávnění k vytvoření nového příspěvku.", // "submission.general.deposit": "Deposit", - "submission.general.deposit": "Vložit do repozitáře", + "submission.general.deposit": "Nahrát", // "submission.general.discard.confirm.cancel": "Cancel", "submission.general.discard.confirm.cancel": "Zrušit", @@ -7554,7 +7551,7 @@ "submission.general.info.pending-changes": "Neuložené změny", // "submission.general.save": "Save", - "submission.general.save": "Průběžně uložit záznam", + "submission.general.save": "Uložit", // "submission.general.save-later": "Save for later", "submission.general.save-later": "Uložit na později", @@ -7709,10 +7706,10 @@ "submission.import-external.preview.subtitle": "Níže uvedená metadata byla importována z externího zdroje. Budou předvyplněna při zahájení odesílání..", // "submission.import-external.preview.button.import": "Start submission", - "submission.import-external.preview.button.import": "Zahájit odesílání", + "submission.import-external.preview.button.import": "Začít nový příspěvek", // "submission.import-external.preview.error.import.title": "Submission error", - "submission.import-external.preview.error.import.title": "Chyba při odesílání", + "submission.import-external.preview.error.import.title": "Chyba při vytváření nového příspěvku", // "submission.import-external.preview.error.import.body": "An error occurs during the external source entry import process.", "submission.import-external.preview.error.import.body": "Během procesu importu externí zdrojového záznamu došlo k chybě.", @@ -7893,7 +7890,7 @@ "submission.sections.describe.relationship-lookup.search-tab.placeholder": "Vyhledávací dotaz", // "submission.sections.describe.relationship-lookup.search-tab.search": "Go", - "submission.sections.describe.relationship-lookup.search-tab.search": "Hledání", + "submission.sections.describe.relationship-lookup.search-tab.search": "Přejít na", // "submission.sections.describe.relationship-lookup.search-tab.search-form.placeholder": "Search...", "submission.sections.describe.relationship-lookup.search-tab.search-form.placeholder": "Hledání...", @@ -8080,7 +8077,7 @@ "submission.sections.describe.relationship-lookup.title.Funding Agency": "Financující agentura", // "submission.sections.describe.relationship-lookup.title.isFundingOfPublication": "Funding", - "submission.sections.describe.relationship-lookup.title.isFundingOfPublication": "Projekt, ke kterému publikace náleží", + "submission.sections.describe.relationship-lookup.title.isFundingOfPublication": "Financování", // "submission.sections.describe.relationship-lookup.title.isChildOrgUnitOf": "Parent Organizational Unit", "submission.sections.describe.relationship-lookup.title.isChildOrgUnitOf": "Nadřazená organizační jednotka", @@ -8311,7 +8308,7 @@ "submission.sections.submit.progressbar.describe.recycle": "Opětovně použít", // "submission.sections.submit.progressbar.describe.stepcustom": "Describe", - "submission.sections.submit.progressbar.describe.stepcustom": "Popsat", + "submission.sections.submit.progressbar.describe.stepcustom": "Popis", // "submission.sections.submit.progressbar.describe.stepone": "Describe", "submission.sections.submit.progressbar.describe.stepone": "Základní informace o dokumentu", @@ -8627,8 +8624,8 @@ "submission.submit.breadcrumbs": "Nově podaný záznam", // "submission.submit.title": "New submission", - // TODO Source message changed - Revise the translation - "submission.submit.title": "Nově podaný záznam", + "submission.submit.title": "Nový příspěvek", + // "submission.workflow.generic.delete": "Delete", "submission.workflow.generic.delete": "Smazat", @@ -8710,7 +8707,7 @@ "submission.workflow.tasks.generic.processing": "Zpracování...", // "submission.workflow.tasks.generic.submitter": "Submitter", - "submission.workflow.tasks.generic.submitter": "Zadavatel", + "submission.workflow.tasks.generic.submitter": "Předkladatel", // "submission.workflow.tasks.generic.success": "Operation successful", "submission.workflow.tasks.generic.success": "Operace proběhla úspěšně", @@ -9074,10 +9071,10 @@ "workflow-item.scorereviewaction.notification.error.content": "Nebylo možné zkontrolovat tento záznam", // "workflow-item.scorereviewaction.title": "Rate this item", - "workflow-item.scorereviewaction.title": "Zkontrolovat tento záznam", + "workflow-item.scorereviewaction.title": "Ohodnotit tento záznam", // "workflow-item.scorereviewaction.header": "Rate this item", - "workflow-item.scorereviewaction.header": "Zkontrolovat tento záznam", + "workflow-item.scorereviewaction.header": "Ohodnotit tento záznam", // "workflow-item.scorereviewaction.button.cancel": "Cancel", "workflow-item.scorereviewaction.button.cancel": "Zrušit", @@ -11034,6 +11031,4 @@ // "item.page.cc.license.disclaimer": "Except where otherwised noted, this item's license is described as", // TODO New key - Add a translation "item.page.cc.license.disclaimer": "Except where otherwised noted, this item's license is described as", - - -} \ No newline at end of file +} From e5ea435cbe58186e6dc42d56753147ade7ce3d5a Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 26 Jul 2024 15:49:37 +0200 Subject: [PATCH 057/123] Fixed linting error. (cherry picked from commit 7e864d27b45c58ef566d31c5d0e2a478a540ecdd) --- src/assets/i18n/cs.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 3824e9741a..a957232a89 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -1797,7 +1797,7 @@ // "claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow", "claimed-declined-task-search-result-list-element.title": "Zamítnuto, posláno zpět správci schvalovacího workflow", - // "collection.create.breadcrumbs": "Create collection", + // "collection.create.breadcrumbs": "Create collection", // TODO New key - Add a translation "collection.create.breadcrumbs": "Create collection", From 865268e820fafcfe047a752d9effb9fcfd99c160 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Wed, 9 Oct 2024 15:08:31 +0200 Subject: [PATCH 058/123] Updated cs messages following review requirements (cherry picked from commit 813d644b32a28685f8a4205bc383268b2502afdb) --- src/assets/i18n/cs.json5 | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index a957232a89..5e52f751ec 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -1475,9 +1475,9 @@ "auth.messages.token-refresh-failed": "Nepodařilo se obnovit váš přístupový token. Prosím přihlašte se znovu.", // "bitstream.download.page": "Now downloading {{bitstream}}...", - "bitstream.download.page": "Nyní se stahuje {{bitstream}}..." , + "bitstream.download.page": "Nyní se stahuje {{bitstream}}...", - // "bitstream.download.page.back": "Back" , + // "bitstream.download.page.back": "Back", "bitstream.download.page.back": "Zpět", // "bitstream.edit.authorizations.link": "Edit bitstream's Policies", @@ -2939,7 +2939,7 @@ "error.top-level-communities": "Chyba při načítání komunit nejvyšší úrovně", // "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", - "error.validation.license.notgranted": "Pro dokončení zaslání musíte udělit licenci. Pokud v tuto chvíli tuto licenci nemůžete udělit, můžete svou práci uložit a později se k svému příspěveku vrátit nebo jej smazat.", + "error.validation.license.notgranted": "Bez udělení licence nelze záznam dokončit. Pokud v tuto chvíli nemůžete licenci udělit, uložte svou práci a vraťte se k příspěveku později nebo jej smažte.", // "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", "error.validation.pattern": "Toto zadání je omezeno aktuálním vzorcem: {{ pattern }}.", @@ -4308,7 +4308,7 @@ "item.page.journal.search.title": "Články v tomto časopise", // "item.page.link.full": "Full item page", - "item.page.link.full": "Zobrazit celý záznam", + "item.page.link.full": "Zobrazit úplný záznam", // "item.page.link.simple": "Simple item page", "item.page.link.simple": "Zobrazit minimální záznam", @@ -4344,7 +4344,7 @@ "item.page.subject": "Klíčová slova", // "item.page.uri": "URI", - "item.page.uri": "Identifikátor", + "item.page.uri": "Permanentní identifikátor", // "item.page.bitstreams.view-more": "Show more", "item.page.bitstreams.view-more": "Zobrazit více", @@ -4934,7 +4934,7 @@ "iiif.page.doi": "Trvalý odkaz: ", // "iiif.page.issue": "Issue: ", - "iiif.page.issue": "Problém:", + "iiif.page.issue": "Číslo:", // "iiif.page.description": "Description: ", "iiif.page.description": "Popis: ", @@ -5375,7 +5375,7 @@ "mydspace.new-submission-external-short": "Importovat metadata", // "mydspace.results.head": "Your submissions", - "mydspace.results.head": "Vaše zázmany", + "mydspace.results.head": "Vaše záznamy", // "mydspace.results.no-abstract": "No Abstract", "mydspace.results.no-abstract": "Žádný abstrakt", @@ -5387,7 +5387,7 @@ "mydspace.results.no-collections": "Žádné kolekce", // "mydspace.results.no-date": "No Date", - "mydspace.results.no-date": "Žádny datum", + "mydspace.results.no-date": "Žádné datum", // "mydspace.results.no-files": "No Files", "mydspace.results.no-files": "Žádné soubory", @@ -6910,7 +6910,7 @@ "search.filters.applied.f.subject": "Předmět", // "search.filters.applied.f.submitter": "Submitter", - "search.filters.applied.f.submitter": "Předkladatel", + "search.filters.applied.f.submitter": "Odesílatel", // "search.filters.applied.f.jobTitle": "Job Title", "search.filters.applied.f.jobTitle": "Název pracovní pozice", @@ -7193,13 +7193,13 @@ "search.filters.filter.subject.label": "Hledat předmět", // "search.filters.filter.submitter.head": "Submitter", - "search.filters.filter.submitter.head": "Předkladatel", + "search.filters.filter.submitter.head": "Odesílatel", // "search.filters.filter.submitter.placeholder": "Submitter", - "search.filters.filter.submitter.placeholder": "Předkladatel", + "search.filters.filter.submitter.placeholder": "Odesílatel", // "search.filters.filter.submitter.label": "Search submitter", - "search.filters.filter.submitter.label": "Hledat předkladatele", + "search.filters.filter.submitter.label": "Hledat odesílatele", // "search.filters.filter.show-tree": "Browse {{ name }} tree", "search.filters.filter.show-tree": "Procházet {{ name }} podle", @@ -8626,7 +8626,6 @@ // "submission.submit.title": "New submission", "submission.submit.title": "Nový příspěvek", - // "submission.workflow.generic.delete": "Delete", "submission.workflow.generic.delete": "Smazat", @@ -8692,7 +8691,7 @@ "submission.workflow.tasks.claimed.reject.submit": "Odmítnout", // "submission.workflow.tasks.claimed.reject_help": "If you have reviewed the item and found it is not suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.", - "submission.workflow.tasks.claimed.reject_help": "Pokud jste záznam zkontrolovali a zjistili, že není vhodný pro zařazení do kolekce, vyberte \"Odmítnout\". Poté budete vyzváni k zadání zprávy, ve které uvedete, proč je záznam nevhodný, a zda by měl vkladatel něco změnit a znovu předložit.", + "submission.workflow.tasks.claimed.reject_help": "Pokud jste záznam zkontrolovali a zjistili, že není vhodný pro zařazení do kolekce, vyberte \"Odmítnout\". Poté budete vyzváni k zadání zprávy, ve které uvedete, proč je záznam nevhodný, a zda by měl vkladatel něco změnit a znovu odeslat.", // "submission.workflow.tasks.claimed.return": "Return to pool", "submission.workflow.tasks.claimed.return": "Vrátit do fondu", @@ -8707,7 +8706,7 @@ "submission.workflow.tasks.generic.processing": "Zpracování...", // "submission.workflow.tasks.generic.submitter": "Submitter", - "submission.workflow.tasks.generic.submitter": "Předkladatel", + "submission.workflow.tasks.generic.submitter": "Odesílatel", // "submission.workflow.tasks.generic.success": "Operation successful", "submission.workflow.tasks.generic.success": "Operace proběhla úspěšně", From 33bc8ba1a3fe5934c10f648c2a756e65a6c59e27 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Wed, 9 Oct 2024 16:09:51 +0200 Subject: [PATCH 059/123] Fixed messages following the PR from the UFAL - https://github.com/dataquest-dev/dspace-angular/pull/669/commits/f18d45ce23ea9c42778885f59e5f7d25e548b2e9 (cherry picked from commit 5e5c627b8b4dbafc5454d91e24797f3ef6ef76aa) --- src/assets/i18n/cs.json5 | 220 +++++++++++++++++++-------------------- 1 file changed, 110 insertions(+), 110 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 5e52f751ec..2cb95f10d3 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -13,7 +13,7 @@ "403.help": "Nemáte povolení k přístupu na tuto stránku. Pro návrat na domovskou stránku můžete použít tlačítko níže.", // "403.link.home-page": "Take me to the home page", - "403.link.home-page": "Přesměrujte mě na domovskou stránku", + "403.link.home-page": "Návrat na domovskou stránku", // "403.forbidden": "Forbidden", "403.forbidden": "Přístup zakázán", @@ -46,7 +46,7 @@ "error-page.description.500": "Služba je nedostupná", // "error-page.description.404": "Page not found", - "error-page.description.404": "Stránka nebyla nenalezena", + "error-page.description.404": "Stránka nebyla nalezena", // "error-page.orcid.generic-error": "An error occurred during login via ORCID. Make sure you have shared your ORCID account email address with DSpace. If the error persists, contact the administrator", "error-page.orcid.generic-error": "Při přihlašování přes ORCID došlo k chybě. Ujistěte se, že jste sdíleli e-mailovou adresu připojenou ke svému účtu ORCID s DSpace. Pokud chyba přetrvává, kontaktujte správce", @@ -67,13 +67,13 @@ "access-status.unknown.listelement.badge": "Status neznámý", // "admin.curation-tasks.breadcrumbs": "System curation tasks", - "admin.curation-tasks.breadcrumbs": "Kurátorská úloha systému", + "admin.curation-tasks.breadcrumbs": "Systémové úlohy správy", // "admin.curation-tasks.title": "System curation tasks", - "admin.curation-tasks.title": "Kurátorská úloha systému", + "admin.curation-tasks.title": "Systémové úlohy správy", // "admin.curation-tasks.header": "System curation tasks", - "admin.curation-tasks.header": "Kurátorská úloha systému", + "admin.curation-tasks.header": "Systémové úlohy správy", // "admin.registries.bitstream-formats.breadcrumbs": "Format registry", "admin.registries.bitstream-formats.breadcrumbs": "Registr formátů", @@ -172,7 +172,7 @@ "admin.registries.bitstream-formats.edit.supportLevel.label": "Úroveň podpory", // "admin.registries.bitstream-formats.head": "Bitstream Format Registry", - "admin.registries.bitstream-formats.head": "Registr formátu souboru", + "admin.registries.bitstream-formats.head": "Registr formátů souboru", // "admin.registries.bitstream-formats.no-items": "No bitstream formats to show.", "admin.registries.bitstream-formats.no-items": "Žádné formáty souboru k zobrazení.", @@ -373,7 +373,7 @@ "admin.access-control.bulk-access.breadcrumbs": "Hromadná správa přístupu", // "administrativeBulkAccess.search.results.head": "Search Results", - "administrativeBulkAccess.search.results.head": "Prohledávat výsledky", + "administrativeBulkAccess.search.results.head": "Výsledky vyhledávání", // "admin.access-control.bulk-access": "Bulk Access Management", "admin.access-control.bulk-access": "Hromadná správa přístupu", @@ -403,7 +403,7 @@ "admin.access-control.epeople.actions.reset": "Resetovat heslo", // "admin.access-control.epeople.actions.stop-impersonating": "Stop impersonating EPerson", - "admin.access-control.epeople.actions.stop-impersonating": "Přestat simulovat jiného uživatele", + "admin.access-control.epeople.actions.stop-impersonating": "Přestat vystupovat jako jiný uživatel", // "admin.access-control.epeople.breadcrumbs": "EPeople", "admin.access-control.epeople.breadcrumbs": "Uživatelé", @@ -612,7 +612,7 @@ "admin.access-control.groups.table.edit.buttons.remove": "Odstranit \"{{name}}\"", // "admin.access-control.groups.no-items": "No groups found with this in their name or this as UUID", - "admin.access-control.groups.no-items": "Nebyly nalezeny žádné skupiny, které by měly toto ve svém názvu nebo toto jako UUID,", + "admin.access-control.groups.no-items": "Nebyla nalezena žádná skupina s tímto v návzvu nebo UUID", // "admin.access-control.groups.notification.deleted.success": "Successfully deleted group \"{{name}}\"", "admin.access-control.groups.notification.deleted.success": "Úspěšně odstraněna skupina \"{{name}}\"", @@ -696,7 +696,7 @@ "admin.access-control.groups.form.members-list.button.see-all": "Procházet vše", // "admin.access-control.groups.form.members-list.headMembers": "Current Members", - "admin.access-control.groups.form.members-list.headMembers": "Aktuální členové", + "admin.access-control.groups.form.members-list.headMembers": "Současní členové", // "admin.access-control.groups.form.members-list.search.button": "Search", "admin.access-control.groups.form.members-list.search.button": "Hledat", @@ -738,13 +738,13 @@ "admin.access-control.groups.form.members-list.table.edit.buttons.add": "Přidat člena jménem \"{{name}}\"", // "admin.access-control.groups.form.members-list.notification.failure.noActiveGroup": "No current active group, submit a name first.", - "admin.access-control.groups.form.members-list.notification.failure.noActiveGroup": "Žádná aktuální aktivní skupina, nejprve zadejte jméno.", + "admin.access-control.groups.form.members-list.notification.failure.noActiveGroup": "Žádná aktuální aktivní skupina, nejprve zadejte název.", // "admin.access-control.groups.form.members-list.no-members-yet": "No members in group yet, search and add.", "admin.access-control.groups.form.members-list.no-members-yet": "Ve skupině zatím nejsou žádní členové, vyhledejte je a přidejte.", // "admin.access-control.groups.form.members-list.no-items": "No EPeople found in that search", - "admin.access-control.groups.form.members-list.no-items": "V tomto vyhledávání nebyly nalezeni žádní uživatelé", + "admin.access-control.groups.form.members-list.no-items": "V tomto vyhledávání nebyli nalezeni žádní uživatelé", // "admin.access-control.groups.form.subgroups-list.notification.failure": "Something went wrong: \"{{cause}}\"", "admin.access-control.groups.form.subgroups-list.notification.failure": "Neúspěch: Něco se pokazilo: \"{{cause}}\"", @@ -801,7 +801,7 @@ "admin.access-control.groups.form.subgroups-list.notification.failure.subgroupToAddIsActiveGroup": "Toto je aktuální skupina, nelze přidat.", // "admin.access-control.groups.form.subgroups-list.no-items": "No groups found with this in their name or this as UUID", - "admin.access-control.groups.form.subgroups-list.no-items": "Nebyly nalezeny žádné skupiny, které by měly toto ve svém názvu nebo toto UUID", + "admin.access-control.groups.form.subgroups-list.no-items": "Nebyla nalezena žádná skupina s tímto v návzvu nebo UUID", // "admin.access-control.groups.form.subgroups-list.no-subgroups-yet": "No subgroups in group yet.", "admin.access-control.groups.form.subgroups-list.no-subgroups-yet": "Ve skupině zatím nejsou žádné podskupiny.", @@ -831,7 +831,7 @@ "admin.notifications.source.breadcrumbs": "Quality Assurance", // "admin.access-control.groups.form.tooltip.editGroupPage": "On this page, you can modify the properties and members of a group. In the top section, you can edit the group name and description, unless this is an admin group for a collection or community, in which case the group name and description are auto-generated and cannot be edited. In the following sections, you can edit group membership. See [the wiki](https://wiki.lyrasis.org/display/DSDOC7x/Create+or+manage+a+user+group) for more details.", - "admin.access-control.groups.form.tooltip.editGroupPage": "Na této stránce můžete upravit vlastnosti a členy skupiny. V horní části můžete upravit název a popis skupiny, pokud se nejedá o skupinu admin pro kolekci nebo komunitu. V takovém případě jsou název a popis skupiny vygenerovány automaticky a nelze je upravovat. V následujících částech můžete upravovat členství ve skupině. Další podrobnosti naleznete v části [the wiki](https://wiki.lyrasis.org/display/DSDOC7x/Create+or+manage+a+user+group).", + "admin.access-control.groups.form.tooltip.editGroupPage": "Na této stránce můžete upravit vlastnosti a členy skupiny. V horní části můžete upravit název a popis skupiny, pokud se nejedá o skupinu admin pro kolekci nebo komunitu. V takovém případě jsou název a popis skupiny vygenerovány automaticky a nelze je upravovat. V následujících částech můžete upravovat členství ve skupině. Další podrobnosti naleznete v části [wiki](https://wiki.lyrasis.org/display/DSDOC7x/Create+or+manage+a+user+group).", // "admin.access-control.groups.form.tooltip.editGroup.addEpeople": "To add or remove an EPerson to/from this group, either click the 'Browse All' button or use the search bar below to search for users (use the dropdown to the left of the search bar to choose whether to search by metadata or by email). Then click the plus icon for each user you wish to add in the list below, or the trash can icon for each user you wish to remove. The list below may have several pages: use the page controls below the list to navigate to the next pages.", "admin.access-control.groups.form.tooltip.editGroup.addEpeople": "Pro přidání nebo odebrání uživatele z této skupiny, buď klikněte na „Prohledávat všechny“ nebo použijte vyhledávací okno pro vyhledání uživatele (použijte roletku na levé straně vyhledávacího okna a vyberte, zda chcete prohledávat pomocí metadat nebo e-mailu). Potom klikněte na tlačítko plus u každého uživatele, kterého chcete přidat, nebo na ikonu odpadkového koše u každého uživatele, kterého chcete odebrat. Seznam níže může obsahovat několik stránek: k přechodu na další stránku použijte ovládací prvky pod seznamem. Až budete hotovi, uložte změny pomocí tlačítka „Uložit“ v horní části.", @@ -1406,7 +1406,7 @@ "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.button.see-all": "Prohledávat vše", // "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.headMembers": "Current Members", - "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.headMembers": "Aktuální členové", + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.headMembers": "Současní členové", // "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.button": "Search", "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.button": "Hledat", @@ -1448,7 +1448,7 @@ "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.edit.buttons.add": "Přidat uživatele jménem \"{{name}}\"", // "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.failure.noActiveGroup": "No current active group, submit a name first.", - "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.failure.noActiveGroup": "Žádná aktivní skupina, nejdříve vložte název.", + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.failure.noActiveGroup": "Žádná aktuální aktivní skupina, nejprve zadejte název.", // "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.no-members-yet": "No members in group yet, search and add.", "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.no-members-yet": "Žádní členové ve skupině, nejdříve vyhledejte a přidejte.", @@ -1792,7 +1792,7 @@ "claimed-approved-search-result-list-element.title": "Schváleno", // "claimed-declined-search-result-list-element.title": "Rejected, sent back to submitter", - "claimed-declined-search-result-list-element.title": "Zamítnuto, posláno zpět vkladateli", + "claimed-declined-search-result-list-element.title": "Zamítnuto, posláno zpět odesílateli", // "claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow", "claimed-declined-task-search-result-list-element.title": "Zamítnuto, posláno zpět správci schvalovacího workflow", @@ -1815,7 +1815,7 @@ "collection.create.sub-head": "Vytvořit kolekci pro komunitu {{ parent }}", // "collection.curate.header": "Curate Collection: {{collection}}", - "collection.curate.header": "Kurátorovat kolekci: {{collection}}", + "collection.curate.header": "Spravovat kolekci: {{collection}}", // "collection.delete.cancel": "Cancel", "collection.delete.cancel": "Zrušit", @@ -1923,7 +1923,7 @@ "collection.edit.logo.notifications.add.success": "Nahrání loga kolekce proběhlo úspěšně.", // "collection.edit.logo.notifications.delete.success.title": "Logo deleted", - "collection.edit.logo.notifications.delete.success.title": "Logo odstraněno", + "collection.edit.logo.notifications.delete.success.title": "Logo smazáno", // "collection.edit.logo.notifications.delete.success.content": "Successfully deleted the collection's logo", "collection.edit.logo.notifications.delete.success.content": "Úspěšně odstraněno logo kolekce", @@ -1948,10 +1948,10 @@ "collection.edit.tabs.access-control.title": "Úprava kolekce - Řízení přístupu", // "collection.edit.tabs.curate.head": "Curate", - "collection.edit.tabs.curate.head": "Kurátorovat", + "collection.edit.tabs.curate.head": "Spravovat", // "collection.edit.tabs.curate.title": "Collection Edit - Curate", - "collection.edit.tabs.curate.title": "Upravit kolekci - kurátorovat", + "collection.edit.tabs.curate.title": "Upravit kolekci - správa", // "collection.edit.tabs.authorizations.head": "Authorizations", "collection.edit.tabs.authorizations.head": "Oprávnění", @@ -2014,7 +2014,7 @@ "collection.edit.tabs.source.head": "Zdroj obsahu", // "collection.edit.tabs.source.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - "collection.edit.tabs.source.notifications.discarded.content": "Vaše změny byly vyřazeny. Chcete-li své změny obnovit, klikněte na tlačítko Zpět", + "collection.edit.tabs.source.notifications.discarded.content": "Vaše změny byly zahozeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", // "collection.edit.tabs.source.notifications.discarded.title": "Changes discarded", // TODO Source message changed - Revise the translation @@ -2024,7 +2024,7 @@ "collection.edit.tabs.source.notifications.invalid.content": "Vaše změny nebyly uloženy. Před uložením se prosím ujistěte, že jsou všechna pole platná.", // "collection.edit.tabs.source.notifications.invalid.title": "Metadata invalid", - "collection.edit.tabs.source.notifications.invalid.title": "Metadata neplatná", + "collection.edit.tabs.source.notifications.invalid.title": "Neplatná metadata", // "collection.edit.tabs.source.notifications.saved.content": "Your changes to this collection's content source were saved.", "collection.edit.tabs.source.notifications.saved.content": "Vaše změny ve zdroji obsahu této kolekce byly uloženy.", @@ -2215,7 +2215,7 @@ "collection.source.controls.harvest.no-information": "Není k dispozi", // "collection.source.update.notifications.error.content": "The provided settings have been tested and didn't work.", - "collection.source.update.notifications.error.content": "Zadané nastavení bylo testováno a nefungovalo.", + "collection.source.update.notifications.error.content": "Zadané nastavení bylo otestováno a nefungovalo.", // "collection.source.update.notifications.error.title": "Server Error", "collection.source.update.notifications.error.title": "Chyba serveru", @@ -2262,7 +2262,7 @@ "community.create.sub-head": "Vytvořit dílčí komunitu pro komunitu {{ parent }}", // "community.curate.header": "Curate Community: {{community}}", - "community.curate.header": "Kurátorství komunity: {{ community }}", + "community.curate.header": "Spravovat komunitu: {{ community }}", // "community.delete.cancel": "Cancel", "community.delete.cancel": "Zrušit", @@ -2295,7 +2295,7 @@ "community.edit.breadcrumbs": "Upravit komunitu", // "community.edit.logo.delete.title": "Delete logo", - "community.edit.logo.delete.title": "Odstranit logo", + "community.edit.logo.delete.title": "Smazat logo", // "community.edit.logo.delete-undo.title": "Undo delete", "community.edit.logo.delete-undo.title": "Zrušit odstranění", @@ -2310,7 +2310,7 @@ "community.edit.logo.notifications.add.success": "Nahrání loga komunity proběhlo úspěšně.", // "community.edit.logo.notifications.delete.success.title": "Logo deleted", - "community.edit.logo.notifications.delete.success.title": "Logo odstraněno", + "community.edit.logo.notifications.delete.success.title": "Logo smazáno", // "community.edit.logo.notifications.delete.success.content": "Successfully deleted the community's logo", "community.edit.logo.notifications.delete.success.content": "Úspěšně odstraněno logo komunity", @@ -2335,10 +2335,10 @@ "community.edit.return": "Zpět", // "community.edit.tabs.curate.head": "Curate", - "community.edit.tabs.curate.head": "Kurátorovat", + "community.edit.tabs.curate.head": "Spravovat", // "community.edit.tabs.curate.title": "Community Edit - Curate", - "community.edit.tabs.curate.title": "Upravit komunitu - kurátorovat", + "community.edit.tabs.curate.title": "Upravit komunitu - správa", // "community.edit.tabs.access-control.head": "Access Control", "community.edit.tabs.access-control.head": "Řízení přístupu", @@ -2396,13 +2396,13 @@ "comcol-role.edit.collection-admin.name": "Správci", // "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.community-admin.description": "Správci komunit mohou vytvářet dílčí komunity nebo kolekce a spravovat nebo přidělovat správu těmto dílčím komunitám nebo kolekcím. Kromě toho rozhodují o tom, kdo může odesílat záznamy do všech dílčích kolekcí, upravovat metadata záznamů (po odeslání) a přidávat (mapovat) existující záznamy z jiných kolekcí (na základě oprávnění).", + "comcol-role.edit.community-admin.description": "Administrátoři komunit mohou vytvářet dílčí komunity nebo kolekce a spravovat nebo přidělovat správu těmto dílčím komunitám nebo kolekcím. Kromě toho rozhodují o tom, kdo může odesílat záznamy do všech dílčích kolekcí, upravovat metadata záznamů (po odeslání) a přidávat (mapovat) existující záznamy z jiných kolekcí (na základě oprávnění).", // "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.collection-admin.description": "Správci kolekcí rozhodují o tom, kdo může do kolekce vkládat záznamy, upravovat metadata záznamů (po vložení) a přidávat (mapovat) existující záznamy z jiných kolekcí do této kolekce (podléhá autorizaci pro danou kolekci).", // "comcol-role.edit.submitters.name": "Submitters", - "comcol-role.edit.submitters.name": "Vkladatelé", + "comcol-role.edit.submitters.name": "Odesílatelé", // "comcol-role.edit.submitters.description": "The E-People and Groups that have permission to submit new items to this collection.", "comcol-role.edit.submitters.description": "Uživatelé a skupiny, které mají oprávnění ke vkládání nových záznamů do této kolekce.", @@ -2460,7 +2460,7 @@ "community.form.errors.title.required": "Zadejte název komunity", // "community.form.rights": "Copyright text (HTML)", - "community.form.rights": "Text autorských práv (HTML)", + "community.form.rights": "Copyright (HTML)", // "community.form.tableofcontents": "News (HTML)", "community.form.tableofcontents": "Novinky (HTML)", @@ -2640,16 +2640,16 @@ "curation.form.submit": "Start", // "curation.form.submit.success.head": "The curation task has been started successfully", - "curation.form.submit.success.head": "Kurátorská úloha byla úspěšně spuštěna", + "curation.form.submit.success.head": "Úloha správy byla úspěšně spuštěna", // "curation.form.submit.success.content": "You will be redirected to the corresponding process page.", "curation.form.submit.success.content": "Budete přesměrováni na příslušnou stránku procesu.", // "curation.form.submit.error.head": "Running the curation task failed", - "curation.form.submit.error.head": "Spuštění kurátorské úlohy se nezdařilo.", + "curation.form.submit.error.head": "Spuštění úlohy správy se nezdařilo.", // "curation.form.submit.error.content": "An error occured when trying to start the curation task.", - "curation.form.submit.error.content": "Při pokusu o spuštění kurátorské úlohy došlo k chybě.", + "curation.form.submit.error.content": "Při pokusu o spuštění úlohy správy došlo k chybě.", // "curation.form.submit.error.invalid-handle": "Couldn't determine the handle for this object", "curation.form.submit.error.invalid-handle": "Nepodařilo se určit handle pro tento objekt", @@ -3044,7 +3044,7 @@ "forgot-email.form.success.head": "E-mail pro obnovení hesla odeslán", // "forgot-email.form.success.content": "An email has been sent to {{ email }} containing a special URL and further instructions.", - "forgot-email.form.success.content": "Na adresu {{ email }} byl odeslán e-mail obsahující speciální adresu URL a další pokyny.", + "forgot-email.form.success.content": "Na adresu {{ email }} byl odeslán e-mail obsahující speciální URL a další instrukce.", // "forgot-email.form.error.head": "Error when trying to reset password", // TODO Source message changed - Revise the translation @@ -3242,7 +3242,7 @@ "grant-deny-request-copy.header": "Žádost o kopii dokumentu", // "grant-deny-request-copy.home-page": "Take me to the home page", - "grant-deny-request-copy.home-page": "Přesměrujte mne na domovskou stránku", + "grant-deny-request-copy.home-page": "Návrat na domovskou stránku", // "grant-deny-request-copy.intro1": "If you are one of the authors of the document {{ name }}, then please use one of the options below to respond to the user's request.", "grant-deny-request-copy.intro1": "Pokud jste jedním z autorů dokumentu {{ name }}, použijte prosím jednu z níže uvedených možností, abyste odpověděli na žádost uživatele.", @@ -3278,13 +3278,13 @@ "health-page.info-tab": "Informace", // "health-page.status-tab": "Status", - "health-page.status-tab": "Status", + "health-page.status-tab": "Stav", // "health-page.error.msg": "The health check service is temporarily unavailable", "health-page.error.msg": "Služba kontrolující stav systému je momentálně nedostupná.", // "health-page.property.status": "Status code", - "health-page.property.status": "Kód statusu", + "health-page.property.status": "Stavový kód", // "health-page.section.db.title": "Database", "health-page.section.db.title": "Databáze", @@ -3455,7 +3455,7 @@ "item.badge.private": "Nedohledatelné", // "item.badge.withdrawn": "Withdrawn", - "item.badge.withdrawn": "Zrušeno", + "item.badge.withdrawn": "Vyřazeno", // "item.bitstreams.upload.bundle": "Bundle", "item.bitstreams.upload.bundle": "Svazek", @@ -3543,10 +3543,10 @@ "item.edit.bitstreams.headers.name": "Název", // "item.edit.bitstreams.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - "item.edit.bitstreams.notifications.discarded.content": "Vaše změny byly zahozeny. Pro obnovení změn klikněte na tlačítko 'Undo'", + "item.edit.bitstreams.notifications.discarded.content": "Vaše změny byly zahozeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", // "item.edit.bitstreams.notifications.discarded.title": "Changes discarded", - "item.edit.bitstreams.notifications.discarded.title": "Změny zahozeny", + "item.edit.bitstreams.notifications.discarded.title": "Zahozené změny", // "item.edit.bitstreams.notifications.move.failed.title": "Error moving bitstreams", "item.edit.bitstreams.notifications.move.failed.title": "Chyba při přesunu souborů", @@ -3558,7 +3558,7 @@ "item.edit.bitstreams.notifications.move.saved.title": "Změny uloženy", // "item.edit.bitstreams.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - "item.edit.bitstreams.notifications.outdated.content": "Záznam, na které právě pracujete, byl změněn jiným uživatelem. Vaše aktuální změny jsou zahozeny, aby se zabránilo konfliktům", + "item.edit.bitstreams.notifications.outdated.content": "Záznam, na kterém právě pracujete, byl změněn jiným uživatelem. Vaše aktuální změny byly zahozeny, aby se zabránilo konfliktům", // "item.edit.bitstreams.notifications.outdated.title": "Changes outdated", "item.edit.bitstreams.notifications.outdated.title": "Změny jsou zastaralé", @@ -3651,13 +3651,13 @@ "item.edit.identifiers.doi.status.MINTED": "Vydáno (nezaregistrováno)", // "item.edit.tabs.status.buttons.register-doi.label": "Register a new or pending DOI", - "item.edit.tabs.status.buttons.register-doi.label": "Registrace nového nebo čekání na nové DOI", + "item.edit.tabs.status.buttons.register-doi.label": "Registrovat nové nebo čekající DOI", // "item.edit.tabs.status.buttons.register-doi.button": "Register DOI...", "item.edit.tabs.status.buttons.register-doi.button": "Zaregistrujte DOI...", // "item.edit.register-doi.header": "Register a new or pending DOI", - "item.edit.register-doi.header": "Zaregistrujte nové DOI nebo čekající k registraci", + "item.edit.register-doi.header": "Registrovat nové nebo čekající DOI", // "item.edit.register-doi.description": "Review any pending identifiers and item metadata below and click Confirm to proceed with DOI registration, or Cancel to back out", "item.edit.register-doi.description": "Níže zkontrolujte všechny identifikátory a metadata ve frontě. Zvolte \"Potvrdit\" pro pokračování v registraci DOI nebo \"Zrušit\" pro přerušení procesu.", @@ -3687,7 +3687,7 @@ "item.edit.item-mapper.cancel": "Zrušit", // "item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", - "item.edit.item-mapper.description": "Toto je nástroj pro mapování záznamů, který umožňuje správcům mapovat tuto záznam na jiné kolekce. Můžete vyhledávat kolekce a mapovat je nebo procházet seznam kolekcí, ke kterým je záznam aktuálně mapována.", + "item.edit.item-mapper.description": "Toto je nástroj pro mapování záznamů, který umožňuje správcům mapovat tento záznam do jiné kolekce. Můžete vyhledávat kolekce a mapovat je nebo procházet seznam kolekcí, ke kterým je záznam aktuálně mapována.", // "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", "item.edit.item-mapper.head": "Mapování do jiných kolekcí - mapovat záznam do kolekcí", @@ -3768,7 +3768,7 @@ "item.edit.metadata.edit.buttons.unedit": "Zastavit úpravy", // "item.edit.metadata.edit.buttons.virtual": "This is a virtual metadata value, i.e. a value inherited from a related entity. It can’t be modified directly. Add or remove the corresponding relationship in the \"Relationships\" tab", - "item.edit.metadata.edit.buttons.virtual": "Tato hodnota metadat je virtuální, tedy děděná z příbuzné entity. Nemůže být zděděna napřímo. Přidejte či odeberte příslušný vztah na záložce \"Vztahy\".", + "item.edit.metadata.edit.buttons.virtual": "Tato hodnota metadat je virtuální, tedy děděná z provázané entity. Nemůže být změněna napřímo. Přidejte či odeberte příslušný vztah na záložce \"Vztahy\".", // "item.edit.metadata.empty": "The item currently doesn't contain any metadata. Click Add to start adding a metadata value.", "item.edit.metadata.empty": "Záznam aktuálně neobsahuje žádná metadata. Kliknutím na tlačítko Přidat začněte přidávat hodnotu metadat.", @@ -3796,11 +3796,11 @@ "item.edit.metadata.metadatafield.invalid": "Prosím, vyberte platné metadatové pole", // "item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - "item.edit.metadata.notifications.discarded.content": "Vaše změny byly vyřazeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", + "item.edit.metadata.notifications.discarded.content": "Vaše změny byly zahozeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", // "item.edit.metadata.notifications.discarded.title": "Changes discarded", // TODO Source message changed - Revise the translation - "item.edit.metadata.notifications.discarded.title": "Změny zahozeny", + "item.edit.metadata.notifications.discarded.title": "Zahozené změny", // "item.edit.metadata.notifications.error.title": "An error occurred", "item.edit.metadata.notifications.error.title": "Došlo k chybě", @@ -3809,17 +3809,17 @@ "item.edit.metadata.notifications.invalid.content": "Vaše změny nebyly uloženy. Před uložením se prosím ujistěte, že jsou všechna pole platná.", // "item.edit.metadata.notifications.invalid.title": "Metadata invalid", - "item.edit.metadata.notifications.invalid.title": "Metadata neplatná", + "item.edit.metadata.notifications.invalid.title": "Neplatná metadata", // "item.edit.metadata.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - "item.edit.metadata.notifications.outdated.content": "Záznam, na které právě pracujete, byl změněna jiným uživatelem. Vaše aktuální změny jsou zahozeny, aby se zabránilo konfliktům", + "item.edit.metadata.notifications.outdated.content": "Záznam, na kterém právě pracujete, byl změněn jiným uživatelem. Vaše aktuální změny byly zahozeny, aby se zabránilo konfliktům", // "item.edit.metadata.notifications.outdated.title": "Changes outdated", // TODO Source message changed - Revise the translation "item.edit.metadata.notifications.outdated.title": "Změny jsou zastaralé", // "item.edit.metadata.notifications.saved.content": "Your changes to this item's metadata were saved.", - "item.edit.metadata.notifications.saved.content": "Vaše změny metadat této záznamu byly uloženy.", + "item.edit.metadata.notifications.saved.content": "Vaše změny metadat tohoto záznamu byly uloženy.", // "item.edit.metadata.notifications.saved.title": "Metadata saved", "item.edit.metadata.notifications.saved.title": "Metadata uložena", @@ -3976,16 +3976,16 @@ "item.edit.relationships.no-relationships": "Žádné vztahy", // "item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - "item.edit.relationships.notifications.discarded.content": "Vaše změny byly vyřazeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", + "item.edit.relationships.notifications.discarded.content": "Vaše změny byly zahozeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", // "item.edit.relationships.notifications.discarded.title": "Changes discarded", - "item.edit.relationships.notifications.discarded.title": "Změny vyřazeny", + "item.edit.relationships.notifications.discarded.title": "Zahozené změny", // "item.edit.relationships.notifications.failed.title": "Error editing relationships", "item.edit.relationships.notifications.failed.title": "Chyba při úpravě vztahů", // "item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - "item.edit.relationships.notifications.outdated.content": "Záznam, na které právě pracujete, byl změněna jiným uživatelem. Vaše aktuální změny jsou vyřazeny, aby se zabránilo konfliktům", + "item.edit.relationships.notifications.outdated.content": "Záznam, na kterém právě pracujete, byl změněn jiným uživatelem. Vaše aktuální změny byly zahozeny, aby se zabránilo konfliktům", // "item.edit.relationships.notifications.outdated.title": "Changes outdated", "item.edit.relationships.notifications.outdated.title": "Změny jsou zastaralé", @@ -4009,25 +4009,25 @@ "item.edit.return": "Zpět", // "item.edit.tabs.bitstreams.head": "Bitstreams", - "item.edit.tabs.bitstreams.head": "Soubor", + "item.edit.tabs.bitstreams.head": "Soubory", // "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", "item.edit.tabs.bitstreams.title": "Úprava záznamu - soubor", // "item.edit.tabs.curate.head": "Curate", - "item.edit.tabs.curate.head": "Kurátorovat", + "item.edit.tabs.curate.head": "Spravovat", // "item.edit.tabs.curate.title": "Item Edit - Curate", - "item.edit.tabs.curate.title": "Úprava záznamu - Kurátorovat", + "item.edit.tabs.curate.title": "Úprava záznamu - spravovat", // "item.edit.curate.title": "Curate Item: {{item}}", - "item.edit.curate.title": "Kurátorovat záznam: {{item}}", + "item.edit.curate.title": "Spravovat záznam: {{item}}", // "item.edit.tabs.access-control.head": "Access Control", - "item.edit.tabs.access-control.head": "Kontrola přístupu", + "item.edit.tabs.access-control.head": "Řízení přístupu", // "item.edit.tabs.access-control.title": "Item Edit - Access Control", - "item.edit.tabs.access-control.title": "Úprava záznamu - Kontrola přístupu", + "item.edit.tabs.access-control.title": "Úprava záznamu - Řízení přístupu", // "item.edit.tabs.metadata.head": "Metadata", "item.edit.tabs.metadata.head": "Metadata", @@ -4275,7 +4275,7 @@ "item.page.collections": "Kolekce", // "item.page.collections.loading": "Loading...", - "item.page.collections.loading": "Načítání...", + "item.page.collections.loading": "Načítá se...", // "item.page.collections.load-more": "Load more", "item.page.collections.load-more": "Načíst další", @@ -4377,7 +4377,7 @@ "item.page.reinstate": "Request reinstatement", // "item.page.version.hasDraft": "A new version cannot be created because there is an inprogress submission in the version history", - "item.page.version.hasDraft": "Nová verze nemůže být vytvořena, protože v historii verzí právě probíhá zadání nové verze.", + "item.page.version.hasDraft": "Nová verze nemůže být vytvořena, protože v historii verzí už další rozpracovaná verze existuje.", // "item.page.claim.button": "Claim", "item.page.claim.button": "Prohlásit", @@ -4585,7 +4585,7 @@ "item.version.history.table.action.deleteVersion": "Smazat verzi", // "item.version.history.table.action.hasDraft": "A new version cannot be created because there is an inprogress submission in the version history", - "item.version.history.table.action.hasDraft": "Nová verze nemůže být vytvořena, protože v historii verzí probíhá odesílání.", + "item.version.history.table.action.hasDraft": "Nová verze nemůže být vytvořena, protože v historii verzí už další rozpracovaná verze existuje.", // "item.version.notice": "This is not the latest version of this item. The latest version can be found here.", "item.version.notice": "Toto není nejnovější verze tohoto záznamu. Nejnovější verzi naleznete zde.", @@ -4698,7 +4698,7 @@ "item.version.create.notification.failure": "Nová verze nebyla vytvořena", // "item.version.create.notification.inProgress": "A new version cannot be created because there is an inprogress submission in the version history", - "item.version.create.notification.inProgress": "Nová verze nemůže být vytvořena, protože v historii verzí probíhá odesílání.", + "item.version.create.notification.inProgress": "Nová verze nemůže být vytvořena, protože v historii verzí už další rozpracovaná verze existuje.", // "item.version.delete.modal.header": "Delete version", "item.version.delete.modal.header": "Smazat verzi", @@ -4788,7 +4788,7 @@ "itemtemplate.edit.metadata.metadatafield.invalid": "Prosím zvolte platné metadatové pole", // "itemtemplate.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - "itemtemplate.edit.metadata.notifications.discarded.content": "Vaše změny byly zahozeny. Chcete-li obnovit své změny, klikněte na tlačítko 'Zpět'.", + "itemtemplate.edit.metadata.notifications.discarded.content": "Vaše změny byly zahozeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", // "itemtemplate.edit.metadata.notifications.discarded.title": "Changes discarded", "itemtemplate.edit.metadata.notifications.discarded.title": "Změny byly zahozeny", @@ -4797,7 +4797,7 @@ "itemtemplate.edit.metadata.notifications.error.title": "Vyskytla se chyba", // "itemtemplate.edit.metadata.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", - "itemtemplate.edit.metadata.notifications.invalid.content": "Vaše změny nebyly uloženy. Před uložením se ujistěte, že všechna pole jsou platná.", + "itemtemplate.edit.metadata.notifications.invalid.content": "Vaše změny nebyly uloženy. Před uložením se prosím ujistěte, že jsou všechna pole platná.", // "itemtemplate.edit.metadata.notifications.invalid.title": "Metadata invalid", "itemtemplate.edit.metadata.notifications.invalid.title": "Neplatná metadata", @@ -4812,7 +4812,7 @@ "itemtemplate.edit.metadata.notifications.saved.content": "Vaše změny v šabloně metadat tohoto záznamu byly uloženy.", // "itemtemplate.edit.metadata.notifications.saved.title": "Metadata saved", - "itemtemplate.edit.metadata.notifications.saved.title": "Metadata uloženy", + "itemtemplate.edit.metadata.notifications.saved.title": "Metadata uložena", // "itemtemplate.edit.metadata.reinstate-button": "Undo", "itemtemplate.edit.metadata.reinstate-button": "Vrátit zpět", @@ -5043,7 +5043,7 @@ "menu.header.admin": "Admin", // "menu.header.image.logo": "Repository logo", - "menu.header.image.logo": "Logo úložiště", + "menu.header.image.logo": "Logo repozitáře", // "menu.header.admin.description": "Management menu", "menu.header.admin.description": "Management menu", @@ -5119,7 +5119,7 @@ "menu.section.control_panel": "Ovládací panel", // "menu.section.curation_task": "Curation Task", - "menu.section.curation_task": "Kurátorská úloha", + "menu.section.curation_task": "Úloha správy", // "menu.section.edit": "Edit", "menu.section.edit": "Upravit", @@ -5165,25 +5165,25 @@ "menu.section.icon.control_panel": "Sekce menu Ovládací panel", // "menu.section.icon.curation_tasks": "Curation Task menu section", - "menu.section.icon.curation_tasks": "Sekce menu Kurátorská úloha", + "menu.section.icon.curation_tasks": "Sekce menu úloha správy", // "menu.section.icon.edit": "Edit menu section", "menu.section.icon.edit": "Sekce menu Upravit", // "menu.section.icon.export": "Export menu section", - "menu.section.icon.export": "Exportovat sekci menu", + "menu.section.icon.export": "Sekce menu Exportovat", // "menu.section.icon.find": "Find menu section", - "menu.section.icon.find": "Najít sekci menu", + "menu.section.icon.find": "Sekce menu Najít", // "menu.section.icon.health": "Health check menu section", - "menu.section.icon.health": "Kontrola stavu sekce menu", + "menu.section.icon.health": "Sekce menu Kontrola stavu", // "menu.section.icon.import": "Import menu section", - "menu.section.icon.import": "Importovat sekci menu", + "menu.section.icon.import": "Sekce menu Importovat", // "menu.section.icon.new": "New menu section", - "menu.section.icon.new": "Nová sekce menu", + "menu.section.icon.new": "Sekce menu Nový", // "menu.section.icon.pin": "Pin sidebar", "menu.section.icon.pin": "Připnout postranní panel", @@ -5260,7 +5260,7 @@ "menu.section.health": "Stav systému", // "menu.section.registries": "Registries", - "menu.section.registries": "Rejstřík", + "menu.section.registries": "Registry", // "menu.section.registries_format": "Format", "menu.section.registries_format": "Formát", @@ -5285,7 +5285,7 @@ "menu.section.toggle.control_panel": "Přepnout sekci ovládacího panelu", // "menu.section.toggle.curation_task": "Toggle Curation Task section", - "menu.section.toggle.curation_task": "Přepnout sekci Kurátorská úloha", + "menu.section.toggle.curation_task": "Přepnout sekci úloha správy", // "menu.section.toggle.edit": "Toggle Edit section", "menu.section.toggle.edit": "Přepnout sekci Úpravy", @@ -5303,7 +5303,7 @@ "menu.section.toggle.new": "Přepnout sekci Nová", // "menu.section.toggle.registries": "Toggle Registries section", - "menu.section.toggle.registries": "Přepnout sekci Rejstříky", + "menu.section.toggle.registries": "Přepnout sekci Registry", // "menu.section.toggle.statistics_task": "Toggle Statistics Task section", "menu.section.toggle.statistics_task": "Přepnout sekci Statistická úloha", @@ -5327,7 +5327,7 @@ "mydspace.description": "", // "mydspace.messages.controller-help": "Select this option to send a message to item's submitter.", - "mydspace.messages.controller-help": "Zvolte tuto možnost, chcete-li odeslat zprávu vkladateli záznamu.", + "mydspace.messages.controller-help": "Zvolte tuto možnost, chcete-li odeslat zprávu odesílateli záznamu.", // "mydspace.messages.description-placeholder": "Insert your message here...", "mydspace.messages.description-placeholder": "Zde vložte svou zprávu...", @@ -5506,7 +5506,7 @@ "nav.statistics.header": "Statistika", // "nav.stop-impersonating": "Stop impersonating EPerson", - "nav.stop-impersonating": "Přestat simulovat jiného uživatele", + "nav.stop-impersonating": "Přestat vystupovat jako jiný uživatel", // "nav.subscriptions": "Subscriptions", "nav.subscriptions": "Nastavení notifikací", @@ -5810,7 +5810,7 @@ "orgunit.page.city": "Město", // "orgunit.page.country": "Country", - "orgunit.page.country": "Stát", + "orgunit.page.country": "Země", // "orgunit.page.dateestablished": "Date established", "orgunit.page.dateestablished": "Datum založení.", @@ -5892,7 +5892,7 @@ "person-relationships.search.results.head": "Výsledky vyhledávání osob", // "person.search.title": "Person Search", - "person.search.title": "Vyledatávání osob", + "person.search.title": "Vyhledávání osob", // "process.new.select-parameters": "Parameters", "process.new.select-parameters": "Parametry", @@ -6121,7 +6121,7 @@ "process.overview.delete.clear": "Zrušit výběr mazání", // "process.overview.delete.processing": "{{count}} process(es) are being deleted. Please wait for the deletion to fully complete. Note that this can take a while.", - "process.overview.delete.processing": "Smazává se {{count}} procesů. Počkejte prosím na úplné dokončení mazání. To může chvíli trvat.", + "process.overview.delete.processing": "Maže se {{count}} procesů. Počkejte prosím na úplné dokončení mazání. To může chvíli trvat.", // "process.overview.delete.body": "Are you sure you want to delete {{count}} process(es)?", "process.overview.delete.body": "Určitě chcete smazat {{count}} procesů", @@ -6512,7 +6512,7 @@ "register-page.registration.header": "Registrace nového uživatele", // "register-page.registration.info": "Register an account to subscribe to collections for email updates, and submit new items to DSpace.", - "register-page.registration.info": "Zaregistrujte si účet a přihlaste se k odběru sbírek pro e-mailové aktualizace a odesílejte nové záznamy do systému DSpace", + "register-page.registration.info": "Zaregistrujte si účet a přihlaste se k odběru kolekcí pro e-mailové aktualizace a odesílejte nové záznamy do systému DSpace", // "register-page.registration.email": "Email Address *", "register-page.registration.email": "E-mailová adresa *", @@ -6880,7 +6880,7 @@ "search.filters.applied.f.dateIssued.min": "Datum zahájení", // "search.filters.applied.f.dateSubmitted": "Date submitted", - "search.filters.applied.f.dateSubmitted": "Date vložení", + "search.filters.applied.f.dateSubmitted": "Datum vložení", // "search.filters.applied.f.discoverable": "Non-discoverable", // TODO Source message changed - Revise the translation @@ -6925,7 +6925,7 @@ "search.filters.applied.f.supervisedBy": "Zkontrolováno", // "search.filters.applied.f.withdrawn": "Withdrawn", - "search.filters.applied.f.withdrawn": "Zrušeno", + "search.filters.applied.f.withdrawn": "Vyřazeno", // "search.filters.applied.operator.equals": "", // TODO New key - Add a translation @@ -7142,10 +7142,10 @@ "search.filters.filter.objectpeople.label": "Hledat osoby", // "search.filters.filter.organizationAddressCountry.head": "Country", - "search.filters.filter.organizationAddressCountry.head": "Stát", + "search.filters.filter.organizationAddressCountry.head": "Země", // "search.filters.filter.organizationAddressCountry.placeholder": "Country", - "search.filters.filter.organizationAddressCountry.placeholder": "Stát", + "search.filters.filter.organizationAddressCountry.placeholder": "Země", // "search.filters.filter.organizationAddressCountry.label": "Search country", "search.filters.filter.organizationAddressCountry.label": "Hledat stát", @@ -7438,10 +7438,10 @@ "sorting.dc.date.issued.DESC": "Datum vydání sestupně", // "sorting.dc.date.accessioned.ASC": "Accessioned Date Ascending", - "sorting.dc.date.accessioned.ASC": "Datum přírůstku vzestupně", + "sorting.dc.date.accessioned.ASC": "Datum odeslání vzestupně", // "sorting.dc.date.accessioned.DESC": "Accessioned Date Descending", - "sorting.dc.date.accessioned.DESC": "Datum přírůstku sestupně", + "sorting.dc.date.accessioned.DESC": "Datum odeslání sestupně", // "sorting.lastModified.ASC": "Last modified Ascending", "sorting.lastModified.ASC": "Naposledy upraveno vzestupně", @@ -8284,7 +8284,7 @@ "submission.sections.identifiers.info": "Pro tento záznam budou vytvořeny následující identifikátory:", // "submission.sections.identifiers.no_handle": "No handles have been minted for this item.", - "submission.sections.identifiers.no_handle": "Tomuto záznamu nybyl přidělen žádný handle.", + "submission.sections.identifiers.no_handle": "Tomuto záznamu nebyl přidělen žádný handle.", // "submission.sections.identifiers.no_doi": "No DOIs have been minted for this item.", "submission.sections.identifiers.no_doi": "Tomuto záznamu nebylo přiděleno žádné DOI.", @@ -8446,10 +8446,10 @@ "submission.sections.upload.form.until-placeholder": "Až do", // "submission.sections.upload.header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", - "submission.sections.upload.header.policy.default.nolist": "Nahrané soubory v kolekci {{collectionName}} budou přístupné podle následující skupiny (skupin):", + "submission.sections.upload.header.policy.default.nolist": "Nahrané soubory v kolekci {{collectionName}} budou přístupné podle následujících skupin:", // "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", - "submission.sections.upload.header.policy.default.withlist": "Vezměte prosím na vědomí, že nahrané soubory v kolekci {{collectionName}} budou kromě toho, co je explicitně rozhodnuto pro jednotlivý soubor, přístupné podle následující skupiny (skupin):", + "submission.sections.upload.header.policy.default.withlist": "Vezměte prosím na vědomí, že nahrané soubory v kolekci {{collectionName}} budou kromě toho, co je explicitně rozhodnuto pro jednotlivý soubor, přístupné podle následujících skupin:", // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files by dragging & dropping them anywhere on the page.", "submission.sections.upload.info": "Zde najdete všechny soubory, které jsou aktuálně v záznamu. Můžete aktualizovat metadata souborů a podmínky přístupu nebo nahrát další soubory pouhým přetažením kdekoli na stránku.", @@ -8618,7 +8618,7 @@ "submission.sections.sherpa.record.information.uri": "URI", // "submission.sections.sherpa.error.message": "There was an error retrieving sherpa informations", - "submission.sections.sherpa.error.message": "Informace ze Sherpa Romeo se nepodařilo načíst.", + "submission.sections.sherpa.error.message": "Při načítání informací ze služby Sherpa došlo k chybě", // "submission.submit.breadcrumbs": "New submission", "submission.submit.breadcrumbs": "Nově podaný záznam", @@ -8676,7 +8676,7 @@ "submission.workflow.tasks.claimed.decline_help": "", // "submission.workflow.tasks.claimed.reject.reason.info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.", - "submission.workflow.tasks.claimed.reject.reason.info": "Do níže uvedeného pole zadejte důvod odmítnutí podání a uveďte, zda může vkladatel problém odstranit a podání znovu odeslat.", + "submission.workflow.tasks.claimed.reject.reason.info": "Do níže uvedeného pole zadejte důvod odmítnutí podání a uveďte, zda může odesílatel problém odstranit a podání znovu odeslat.", // "submission.workflow.tasks.claimed.reject.reason.placeholder": "Describe the reason of reject", "submission.workflow.tasks.claimed.reject.reason.placeholder": "Popište důvod odmítnutí", @@ -8691,7 +8691,7 @@ "submission.workflow.tasks.claimed.reject.submit": "Odmítnout", // "submission.workflow.tasks.claimed.reject_help": "If you have reviewed the item and found it is not suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.", - "submission.workflow.tasks.claimed.reject_help": "Pokud jste záznam zkontrolovali a zjistili, že není vhodný pro zařazení do kolekce, vyberte \"Odmítnout\". Poté budete vyzváni k zadání zprávy, ve které uvedete, proč je záznam nevhodný, a zda by měl vkladatel něco změnit a znovu odeslat.", + "submission.workflow.tasks.claimed.reject_help": "Pokud jste záznam zkontrolovali a zjistili, že není vhodný pro zařazení do kolekce, vyberte \"Odmítnout\". Poté budete vyzváni k zadání zprávy, ve které uvedete, proč je záznam nevhodný, a zda by měl odesílatel něco změnit a znovu odeslat.", // "submission.workflow.tasks.claimed.return": "Return to pool", "submission.workflow.tasks.claimed.return": "Vrátit do fondu", @@ -8712,7 +8712,7 @@ "submission.workflow.tasks.generic.success": "Operace proběhla úspěšně", // "submission.workflow.tasks.pool.claim": "Claim", - "submission.workflow.tasks.pool.claim": "Vyžádat si", + "submission.workflow.tasks.pool.claim": "Převzít", // "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", "submission.workflow.tasks.pool.claim_help": "Přiřaďte si tento úkol.", @@ -8909,7 +8909,7 @@ "uploader.or": ", nebo ", // "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)", - "uploader.processing": "(nyní je bezpečné tuto stránku zavřít)", + "uploader.processing": "Zpracovávám nahrané soubory... (nyní je bezpečné tuto stránku zavřít)", // "uploader.queue-length": "Queue length", "uploader.queue-length": "Délka fronty", @@ -8933,7 +8933,7 @@ "workflowAdmin.search.results.head": "Spravovat workflow", // "workflow.search.results.head": "Workflow tasks", - "workflow.search.results.head": "Kroky workflow", + "workflow.search.results.head": "Úlohy workflow", // "supervision.search.results.head": "Workflow and Workspace tasks", "supervision.search.results.head": "Úlohy Workflow a osobního pracovního prostoru", @@ -8973,22 +8973,22 @@ "workflow-item.delete.button.confirm": "Smazat", // "workflow-item.send-back.notification.success.title": "Sent back to submitter", - "workflow-item.send-back.notification.success.title": "Odesláno zpět vkladateli", + "workflow-item.send-back.notification.success.title": "Vráceno zpět odesílateli", // "workflow-item.send-back.notification.success.content": "This workflow item was successfully sent back to the submitter", - "workflow-item.send-back.notification.success.content": "Tento záznam ve workflow byl úspěšně odeslán zpět vkladateli", + "workflow-item.send-back.notification.success.content": "Tento záznam ve workflow byl úspěšně vrácen zpět odesílateli", // "workflow-item.send-back.notification.error.title": "Something went wrong", "workflow-item.send-back.notification.error.title": "Něco se pokazilo", // "workflow-item.send-back.notification.error.content": "The workflow item could not be sent back to the submitter", - "workflow-item.send-back.notification.error.content": "Záznam ve workflow se nepodařilo odeslat zpět vkladateli", + "workflow-item.send-back.notification.error.content": "Záznam ve workflow se nepodařilo vrátit zpět odesílateli", // "workflow-item.send-back.title": "Send workflow item back to submitter", - "workflow-item.send-back.title": "Odeslat záznam ve workflow zpět vladateli", + "workflow-item.send-back.title": "Vrátit záznam ve workflow zpět odesílateli", // "workflow-item.send-back.header": "Send workflow item back to submitter", - "workflow-item.send-back.header": "Odeslat záznam ve workflow zpět vladateli", + "workflow-item.send-back.header": "Vrátit záznam ve workflow zpět odesílateli", // "workflow-item.send-back.button.cancel": "Cancel", "workflow-item.send-back.button.cancel": "Zrušit", @@ -9004,7 +9004,7 @@ "workspace-item.view.breadcrumbs": "Zobrazení pracovního prostoru", // "workspace-item.view.title": "Workspace View", - "workspace-item.view.title": "Zobrazení pracovní prostoru", + "workspace-item.view.title": "Zobrazení pracovního prostoru", // "workspace-item.delete.breadcrumbs": "Workspace Delete", "workspace-item.delete.breadcrumbs": "Vymazat obsah pracovního prostoru", @@ -9337,7 +9337,7 @@ "person.page.orcid.sync-queue.send.bad-request-error": "Odeslání do ORCID se nezdařilo, protože zdroj odeslaný do registru ORCID není platný", // "person.page.orcid.sync-queue.send.error": "The submission to ORCID failed", - "person.page.orcid.sync-queue.send.error": "Podání do ORCID se nezdařilo", + "person.page.orcid.sync-queue.send.error": "Odeslání do ORCID se nezdařilo", // "person.page.orcid.sync-queue.send.conflict-error": "The submission to ORCID failed because the resource is already present on the ORCID registry", "person.page.orcid.sync-queue.send.conflict-error": "Odeslání do ORCID se nezdařilo, protože zdroj se již nachází v registru ORCID", @@ -9490,7 +9490,7 @@ "system-wide-alert-form.retrieval.error": "Něco se pokazilo při načítání systémových upozornění", // "system-wide-alert.form.cancel": "Cancel", - "system-wide-alert.form.cancel": "Zrušir", + "system-wide-alert.form.cancel": "Zrušit", // "system-wide-alert.form.save": "Save", "system-wide-alert.form.save": "Uložit", @@ -9508,7 +9508,7 @@ "system-wide-alert.form.label.message": "Obsah upozornění", // "system-wide-alert.form.label.countdownTo.enable": "Enable a countdown timer", - "system-wide-alert.form.label.countdownTo.enable": "Povolit odpočítávací měřič", + "system-wide-alert.form.label.countdownTo.enable": "Povolit odpočet", // "system-wide-alert.form.label.countdownTo.hint": "Hint: Set a countdown timer. When enabled, a date can be set in the future and the system-wide alert banner will perform a countdown to the set date. When this timer ends, it will disappear from the alert. The server will NOT be automatically stopped.", "system-wide-alert.form.label.countdownTo.hint": "Nápověda: Nastavte odpočítávací měřič. Je-li to povoleno, lze datum nastavit i v budoucnosti a systémový upozornění bude uvádět odpočítávání do nastaveného data. Ve chvíli, kdy skončí odpočítávání, zmizí i odpočítávač z upozornění. Server NEBUDE automaticky zastaven.", @@ -9643,7 +9643,7 @@ // "vocabulary-treeview.search.form.add": "Add", // TODO New key - Add a translation - "vocabulary-treeview.search.form.add": "Add", + "vocabulary-treeview.search.form.add": "Přidat", // "admin.notifications.publicationclaim.breadcrumbs": "Publication Claim", // TODO New key - Add a translation From d586a8450d5d0207035d4703b7545d8caf1b5dfa Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Wed, 9 Oct 2024 16:14:04 +0200 Subject: [PATCH 060/123] Updated cs localization for subcommunities and subcollections (cherry picked from commit 9badd4a4b6261a2abf5ed838f16bb000b1a127f0) --- src/assets/i18n/cs.json5 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 2cb95f10d3..09dd023e8d 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -2259,7 +2259,7 @@ "community.create.notifications.success": "Úspěšně vytvořena komunita", // "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", - "community.create.sub-head": "Vytvořit dílčí komunitu pro komunitu {{ parent }}", + "community.create.sub-head": "Vytvořit podkomunitu v komunitě {{ parent }}", // "community.curate.header": "Curate Community: {{community}}", "community.curate.header": "Spravovat komunitu: {{ community }}", @@ -2396,7 +2396,7 @@ "comcol-role.edit.collection-admin.name": "Správci", // "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.community-admin.description": "Administrátoři komunit mohou vytvářet dílčí komunity nebo kolekce a spravovat nebo přidělovat správu těmto dílčím komunitám nebo kolekcím. Kromě toho rozhodují o tom, kdo může odesílat záznamy do všech dílčích kolekcí, upravovat metadata záznamů (po odeslání) a přidávat (mapovat) existující záznamy z jiných kolekcí (na základě oprávnění).", + "comcol-role.edit.community-admin.description": "Administrátoři komunit mohou vytvářet podkomunity nebo kolekce a spravovat nebo přidělovat správu těmto podkomunitám nebo kolekcím. Kromě toho rozhodují o tom, kdo může odesílat záznamy do všech podkolekcí, upravovat metadata záznamů (po odeslání) a přidávat (mapovat) existující záznamy z jiných kolekcí (na základě oprávnění).", // "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.collection-admin.description": "Správci kolekcí rozhodují o tom, kdo může do kolekce vkládat záznamy, upravovat metadata záznamů (po vložení) a přidávat (mapovat) existující záznamy z jiných kolekcí do této kolekce (podléhá autorizaci pro danou kolekci).", @@ -2421,7 +2421,7 @@ // "comcol-role.edit.bitstream_read.description": "E-People and Groups that can read new bitstreams submitted to this collection. Changes to this role are not retroactive. Existing bitstreams in the system will still be viewable by those who had read access at the time of their addition.", // TODO Source message changed - Revise the translation - "comcol-role.edit.bitstream_read.description": "Správci komunit mohou vytvářet dílčí komunity nebo kolekce a spravovat nebo přidělovat správu těmto dílčím komunitám nebo kolekcím. Kromě toho rozhodují o tom, kdo může odesílat záznamy do libovolných dílčích kolekcí, upravovat metadata záznamů (po odeslání) a přidávat (mapovat) existující záznamy z jiných kolekcí (na základě oprávnění).", + "comcol-role.edit.bitstream_read.description": "Správci komunit mohou vytvářet podkomunity nebo kolekce a spravovat nebo přidělovat správu těmto podkomunitám nebo kolekcím. Kromě toho rozhodují o tom, kdo může odesílat záznamy do libovolných podkolekcí, upravovat metadata záznamů (po odeslání) a přidávat (mapovat) existující záznamy z jiných kolekcí (na základě oprávnění).", // "comcol-role.edit.bitstream_read.anonymous-group": "Default read for incoming bitstreams is currently set to Anonymous.", "comcol-role.edit.bitstream_read.anonymous-group": "Výchozí čtení pro příchozí soubory je v současné době nastaveno na hodnotu Anonymní.", @@ -2481,7 +2481,7 @@ "community.page.news": "Novinky", // "community.all-lists.head": "Subcommunities and Collections", - "community.all-lists.head": "Dílčí komunity a kolekce", + "community.all-lists.head": "Podkomunity a kolekce", // "community.search.results.head": "Search Results", // TODO New key - Add a translation @@ -2927,10 +2927,10 @@ "error.invalid-search-query": "Vyhledávací dotaz není platný. Další informace o této chybě naleznete v osvědčených postupech Solr query syntax.", // "error.sub-collections": "Error fetching sub-collections", - "error.sub-collections": "Chyba při načítání dílčích kolekcí", + "error.sub-collections": "Chyba při načítání podkolekcí", // "error.sub-communities": "Error fetching sub-communities", - "error.sub-communities": "Chyba při načítání dílčích komunit", + "error.sub-communities": "Chyba při načítání podkomunit", // "error.submission.sections.init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below :

", "error.submission.sections.init-form-error": "Při inicializaci sekce došlo k chybě, zkontrolujte prosím konfiguraci vstupního formuláře. Podrobnosti jsou uvedeny níže :

", @@ -4985,10 +4985,10 @@ "loading.search-results": "Načítají se výsledky vyhledávání...", // "loading.sub-collections": "Loading sub-collections...", - "loading.sub-collections": "Načítají se dílčí kolekce...", + "loading.sub-collections": "Načítají se podkolekce...", // "loading.sub-communities": "Loading sub-communities...", - "loading.sub-communities": "Načítají se dílčí komunity...", + "loading.sub-communities": "Načítají se podkomunity...", // "loading.top-level-communities": "Loading top-level communities...", "loading.top-level-communities": "Načítají se komunity nejvyšší úrovně...", From 68b6cc9bd45e01a306990c05b464e94cacb1a4f2 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 11 Oct 2024 13:37:24 +0200 Subject: [PATCH 061/123] Fixed small cs localization mistakes (cherry picked from commit 680d6c94166266d6195f13b92e3be916917ae8c0) --- src/assets/i18n/cs.json5 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 09dd023e8d..466e3475bf 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -2939,7 +2939,7 @@ "error.top-level-communities": "Chyba při načítání komunit nejvyšší úrovně", // "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", - "error.validation.license.notgranted": "Bez udělení licence nelze záznam dokončit. Pokud v tuto chvíli nemůžete licenci udělit, uložte svou práci a vraťte se k příspěveku později nebo jej smažte.", + "error.validation.license.notgranted": "Bez udělení licence nelze záznam dokončit. Pokud v tuto chvíli nemůžete licenci udělit, uložte svou práci a vraťte se k příspěvku později nebo jej smažte.", // "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", "error.validation.pattern": "Toto zadání je omezeno aktuálním vzorcem: {{ pattern }}.", @@ -4934,7 +4934,7 @@ "iiif.page.doi": "Trvalý odkaz: ", // "iiif.page.issue": "Issue: ", - "iiif.page.issue": "Číslo:", + "iiif.page.issue": "Číslo: ", // "iiif.page.description": "Description: ", "iiif.page.description": "Popis: ", From b11efbbf0cde5cde73f3873a41539bec7588cf9c Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Mon, 4 Nov 2024 14:37:04 +0100 Subject: [PATCH 062/123] Updated messages for the 'supervised' and 'claim' sentenses (cherry picked from commit 1aef6ce1d623ac8f0a52dcf8086847996a2d0180) --- src/assets/i18n/cs.json5 | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 466e3475bf..f06db668a3 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -4221,10 +4221,10 @@ "workflow-item.search.result.notification.deleted.failure": "Při mazání příkazu supervize \"{{name}}\" došlo k chybě", // "workflow-item.search.result.list.element.supervised-by": "Supervised by:", - "workflow-item.search.result.list.element.supervised-by": "Supervizován:", + "workflow-item.search.result.list.element.supervised-by": "Pod dohledem:", // "workflow-item.search.result.list.element.supervised.remove-tooltip": "Remove supervision group", - "workflow-item.search.result.list.element.supervised.remove-tooltip": "Odebrat skupinu supervize", + "workflow-item.search.result.list.element.supervised.remove-tooltip": "Odebrat skupinu dohledu", // "confidence.indicator.help-text.accepted": "This authority value has been confirmed as accurate by an interactive user", // TODO New key - Add a translation @@ -4380,10 +4380,10 @@ "item.page.version.hasDraft": "Nová verze nemůže být vytvořena, protože v historii verzí už další rozpracovaná verze existuje.", // "item.page.claim.button": "Claim", - "item.page.claim.button": "Prohlásit", + "item.page.claim.button": "Převzít", // "item.page.claim.tooltip": "Claim this item as profile", - "item.page.claim.tooltip": "Prohlásit tento záznam za profilovou", + "item.page.claim.tooltip": "Prohlásit tento záznam za profil", // "item.page.image.alt.ROR": "ROR logo", // TODO New key - Add a translation @@ -5414,7 +5414,7 @@ "mydspace.show.workspace": "Vaše záznamy", // "mydspace.show.supervisedWorkspace": "Supervised items", - "mydspace.show.supervisedWorkspace": "Zkontrolované záznamy", + "mydspace.show.supervisedWorkspace": "Záznamy pod dohledem", // "mydspace.status.mydspaceArchived": "Archived", "mydspace.status.mydspaceArchived": "Archivováno", @@ -6922,7 +6922,7 @@ "search.filters.applied.f.birthDate.min": "Datum narození od", // "search.filters.applied.f.supervisedBy": "Supervised by", - "search.filters.applied.f.supervisedBy": "Zkontrolováno", + "search.filters.applied.f.supervisedBy": "Pod dohledem", // "search.filters.applied.f.withdrawn": "Withdrawn", "search.filters.applied.f.withdrawn": "Vyřazeno", @@ -7221,8 +7221,7 @@ "search.filters.filter.supervisedBy.placeholder": "Supervised By", // "search.filters.filter.supervisedBy.label": "Search Supervised By", - // TODO New key - Add a translation - "search.filters.filter.supervisedBy.label": "Search Supervised By", + "search.filters.filter.supervisedBy.label": "Hledat pod dohledem", // "search.filters.entityType.JournalIssue": "Journal Issue", "search.filters.entityType.JournalIssue": "Číslo časopisu", @@ -8924,7 +8923,7 @@ "virtual-metadata.delete-relationship.modal-head": "Vyberte záznamy, pro které chcete uložit virtuální metadata jako skutečná metadata", // "supervisedWorkspace.search.results.head": "Supervised Items", - "supervisedWorkspace.search.results.head": "Zkontrolované záznamy", + "supervisedWorkspace.search.results.head": "Záznamy pod dohledem", // "workspace.search.results.head": "Your submissions", "workspace.search.results.head": "Moje záznamy", From bd638f03560015a5bdc31108227810ca1fdba540 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 8 Nov 2024 15:24:06 +0100 Subject: [PATCH 063/123] Updated supervised by messages following NTK suggestions (cherry picked from commit d819cf43968665c20870ee16a1620e744dfbd821) --- src/assets/i18n/cs.json5 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index f06db668a3..866660f513 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -4221,10 +4221,10 @@ "workflow-item.search.result.notification.deleted.failure": "Při mazání příkazu supervize \"{{name}}\" došlo k chybě", // "workflow-item.search.result.list.element.supervised-by": "Supervised by:", - "workflow-item.search.result.list.element.supervised-by": "Pod dohledem:", + "workflow-item.search.result.list.element.supervised-by": "Dohlížející autorita:", // "workflow-item.search.result.list.element.supervised.remove-tooltip": "Remove supervision group", - "workflow-item.search.result.list.element.supervised.remove-tooltip": "Odebrat skupinu dohledu", + "workflow-item.search.result.list.element.supervised.remove-tooltip": "Odebrat dohlížející autoritu", // "confidence.indicator.help-text.accepted": "This authority value has been confirmed as accurate by an interactive user", // TODO New key - Add a translation @@ -7221,7 +7221,7 @@ "search.filters.filter.supervisedBy.placeholder": "Supervised By", // "search.filters.filter.supervisedBy.label": "Search Supervised By", - "search.filters.filter.supervisedBy.label": "Hledat pod dohledem", + "search.filters.filter.supervisedBy.label": "Hledat dohlížející autoritu", // "search.filters.entityType.JournalIssue": "Journal Issue", "search.filters.entityType.JournalIssue": "Číslo časopisu", From 66dedc5b2e08de7c3691fb21a7238d1ac08be15c Mon Sep 17 00:00:00 2001 From: Alisa Ismailati Date: Fri, 19 Jul 2024 14:54:39 +0200 Subject: [PATCH 064/123] [CST-15591] Fixed headings by their rank --- .../create-collection-page.component.html | 2 +- .../create-community-page.component.html | 4 ++-- .../workspaceitem/workspaceitem-actions.component.html | 2 +- .../item-list-preview/item-list-preview.component.html | 2 +- .../scope-selector-modal.component.html | 6 +++--- .../search/search-results/search-results.component.html | 2 +- .../sections/upload/section-upload.component.html | 2 +- .../workspaceitems-delete-page.component.html | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/collection-page/create-collection-page/create-collection-page.component.html b/src/app/collection-page/create-collection-page/create-collection-page.component.html index f3f9785692..4a09d25aed 100644 --- a/src/app/collection-page/create-collection-page/create-collection-page.component.html +++ b/src/app/collection-page/create-collection-page/create-collection-page.component.html @@ -1,7 +1,7 @@
-

{{'collection.create.sub-head' | translate:{ parent: dsoNameService.getName((parentRD$| async)?.payload) } }}

+

{{'collection.create.sub-head' | translate:{ parent: dsoNameService.getName((parentRD$| async)?.payload) } }}

- -

{{ 'community.create.sub-head' | translate:{ parent: dsoNameService.getName(parent) } }}

+

{{ 'community.create.head' | translate }}

+

{{ 'community.create.sub-head' | translate:{ parent: dsoNameService.getName(parent) } }}

diff --git a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html index 6e958c7a8b..26066df4b5 100644 --- a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html +++ b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html @@ -30,7 +30,7 @@ -

+

diff --git a/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html b/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html index 013274320b..74c71b1d12 100644 --- a/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html +++ b/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html @@ -6,14 +6,14 @@
-
{{'dso-selector.' + action + '.' + objectType.toString().toLowerCase() + '.input-header' | translate}}
+

{{'dso-selector.' + action + '.' + objectType.toString().toLowerCase() + '.input-header' | translate}}

diff --git a/src/app/shared/search/search-results/search-results.component.html b/src/app/shared/search/search-results/search-results.component.html index 11c589254a..1f1e58ea10 100644 --- a/src/app/shared/search/search-results/search-results.component.html +++ b/src/app/shared/search/search-results/search-results.component.html @@ -1,5 +1,5 @@
-

{{ (configuration ? configuration + '.search.results.head' : 'search.results.head') | translate }}

+

{{ (configuration ? configuration + '.search.results.head' : 'search.results.head') | translate }}

diff --git a/src/app/submission/sections/upload/section-upload.component.html b/src/app/submission/sections/upload/section-upload.component.html index b57b454288..3c4fbe49f6 100644 --- a/src/app/submission/sections/upload/section-upload.component.html +++ b/src/app/submission/sections/upload/section-upload.component.html @@ -5,7 +5,7 @@
-

{{'submission.sections.upload.no-file-uploaded' | translate}}

+
{{'submission.sections.upload.no-file-uploaded' | translate}}
diff --git a/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html b/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html index a0f0a1711e..9d9bece197 100644 --- a/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html +++ b/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html @@ -1,5 +1,5 @@
-

{{ 'workspace-item.delete.header' | translate }}

+

{{ 'workspace-item.delete.header' | translate }}

@@ -7,7 +7,7 @@ diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts index 2d0a2a25de..d21714a5ef 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts @@ -8,6 +8,9 @@ import { Community } from '../../../../core/shared/community.model'; import { CreateCommunityParentSelectorComponent } from './create-community-parent-selector.component'; import { MetadataValue } from '../../../../core/shared/metadata.models'; import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; +import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; +import { By } from '@angular/platform-browser'; +import { of as observableOf } from 'rxjs'; describe('CreateCommunityParentSelectorComponent', () => { let component: CreateCommunityParentSelectorComponent; @@ -26,7 +29,9 @@ describe('CreateCommunityParentSelectorComponent', () => { const communityRD = createSuccessfulRemoteDataObject(community); const modalStub = jasmine.createSpyObj('modalStub', ['close']); const createPath = '/communities/create'; - + const mockAuthorizationDataService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true), + }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], @@ -47,7 +52,8 @@ describe('CreateCommunityParentSelectorComponent', () => { }, { provide: Router, useValue: router - } + }, + { provide: AuthorizationDataService, useValue: mockAuthorizationDataService }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -70,4 +76,20 @@ describe('CreateCommunityParentSelectorComponent', () => { expect(router.navigate).toHaveBeenCalledWith([createPath], { queryParams: { parent: community.uuid } }); }); + it('should show the div when user is an admin', (waitForAsync(() => { + component.isAdmin$ = observableOf(true); + fixture.detectChanges(); + + const divElement = fixture.debugElement.query(By.css('div[data-test="admin-div"]')); + expect(divElement).toBeTruthy(); + }))); + + it('should hide the div when user is not an admin', (waitForAsync(() => { + component.isAdmin$ = observableOf(false); + fixture.detectChanges(); + + const divElement = fixture.debugElement.query(By.css('div[data-test="admin-div"]')); + expect(divElement).toBeFalsy(); + }))); + }); diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts index 77458d9802..e44a7450e6 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts @@ -14,6 +14,9 @@ import { } from '../../../../community-page/community-page-routing-paths'; import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; import { environment } from '../../../../../environments/environment'; +import { FeatureID } from '../../../../core/data/feature-authorization/feature-id'; +import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; +import { Observable } from 'rxjs'; /** * Component to wrap a button - for top communities - @@ -32,11 +35,16 @@ export class CreateCommunityParentSelectorComponent extends DSOSelectorModalWrap selectorTypes = [DSpaceObjectType.COMMUNITY]; action = SelectorActionType.CREATE; defaultSort = new SortOptions(environment.comcolSelectionSort.sortField, environment.comcolSelectionSort.sortDirection as SortDirection); + isAdmin$: Observable; - constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { + constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router, protected authorizationService: AuthorizationDataService) { super(activeModal, route); } + ngOnInit() { + this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf); + } + /** * Navigate to the community create page */ From 8849f140fbf47e8a15dda672ecec7770d49308b7 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Thu, 28 Nov 2024 22:39:08 +0100 Subject: [PATCH 071/123] 117287: Removed method calls returning observables from ItemDetailPreviewComponent --- .../item-detail-preview.component.html | 2 +- .../item-detail-preview.component.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html index bd00c9411f..c2f5299d4b 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html @@ -8,7 +8,7 @@ - +
+ diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index bf316daaed..e6c3776a42 100644 --- a/src/app/shared/form/form.service.ts +++ b/src/app/shared/form/form.service.ts @@ -130,6 +130,9 @@ export class FormService { } public addControlErrors(field: AbstractControl, formId: string, fieldId: string, fieldIndex: number) { + if (field.errors === null) { + return; + } const errors: string[] = Object.keys(field.errors) .filter((errorKey) => field.errors[errorKey] === true) .map((errorKey) => `error.validation.${errorKey}`); From 987ea5104adda0f7887a4fef710a11b6fa165989 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Tue, 19 Nov 2024 16:35:45 +0100 Subject: [PATCH 076/123] 120109: Fixed BaseDataService not emitting when the request is too fast and the ResponsePending are not emitted (cherry picked from commit 0f4d71eb58d63c9c73af2e098126437da0f266c4) --- .../core/data/base/base-data.service.spec.ts | 69 +++++++++++++++---- src/app/core/data/base/base-data.service.ts | 6 +- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/app/core/data/base/base-data.service.spec.ts b/src/app/core/data/base/base-data.service.spec.ts index 098f075c10..9ec31b376e 100644 --- a/src/app/core/data/base/base-data.service.spec.ts +++ b/src/app/core/data/base/base-data.service.spec.ts @@ -50,6 +50,7 @@ describe('BaseDataService', () => { let selfLink; let linksToFollow; let testScheduler; + let remoteDataTimestamp: number; let remoteDataMocks; function initTestService(): TestService { @@ -86,19 +87,21 @@ describe('BaseDataService', () => { expect(actual).toEqual(expected); }); - const timeStamp = new Date().getTime(); + // The response's lastUpdated equals the time of 60 seconds after the test started, ensuring they are not perceived + // as cached values. + remoteDataTimestamp = new Date().getTime() + 60 * 1000; const msToLive = 15 * 60 * 1000; const payload = { foo: 'bar' }; const statusCodeSuccess = 200; const statusCodeError = 404; const errorMessage = 'not found'; remoteDataMocks = { - RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined), - ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), - Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess), - SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess), - Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), - ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError), + RequestPending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.RequestPending, undefined, undefined, undefined), + ResponsePending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), + Success: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess), + SuccessStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess), + Error: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), + ErrorStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError), }; return new TestService( @@ -330,11 +333,15 @@ describe('BaseDataService', () => { spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source); }); - - it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => { + it('should not emit a cached completed RemoteData', () => { + // Old cached value from 1 minute before the test started + const oldCachedSucceededData: RemoteData = Object.assign({}, remoteDataMocks.Success, { + timeCompleted: remoteDataTimestamp - 2 * 60 * 1000, + lastUpdated: remoteDataTimestamp - 2 * 60 * 1000, + } as RemoteData); testScheduler.run(({ cold, expectObservable }) => { spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.Success, + a: oldCachedSucceededData, b: remoteDataMocks.RequestPending, c: remoteDataMocks.ResponsePending, d: remoteDataMocks.Success, @@ -352,6 +359,22 @@ describe('BaseDataService', () => { }); }); + it('should emit the first completed RemoteData since the request was made', () => { + testScheduler.run(({ cold, expectObservable }) => { + spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b', { + a: remoteDataMocks.Success, + b: remoteDataMocks.SuccessStale, + })); + const expected = 'a-b'; + const values = { + a: remoteDataMocks.Success, + b: remoteDataMocks.SuccessStale, + }; + + expectObservable(service.findByHref(selfLink, false, true, ...linksToFollow)).toBe(expected, values); + }); + }); + it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { testScheduler.run(({ cold, expectObservable }) => { spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', { @@ -514,11 +537,15 @@ describe('BaseDataService', () => { spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source); }); - - it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => { + it('should not emit a cached completed RemoteData', () => { testScheduler.run(({ cold, expectObservable }) => { + // Old cached value from 1 minute before the test started + const oldCachedSucceededData: RemoteData = Object.assign({}, remoteDataMocks.Success, { + timeCompleted: remoteDataTimestamp - 2 * 60 * 1000, + lastUpdated: remoteDataTimestamp - 2 * 60 * 1000, + } as RemoteData); spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.Success, + a: oldCachedSucceededData, b: remoteDataMocks.RequestPending, c: remoteDataMocks.ResponsePending, d: remoteDataMocks.Success, @@ -536,6 +563,22 @@ describe('BaseDataService', () => { }); }); + it('should emit the first completed RemoteData since the request was made', () => { + testScheduler.run(({ cold, expectObservable }) => { + spyOn(rdbService, 'buildList').and.returnValue(cold('a-b', { + a: remoteDataMocks.Success, + b: remoteDataMocks.SuccessStale, + })); + const expected = 'a-b'; + const values = { + a: remoteDataMocks.Success, + b: remoteDataMocks.SuccessStale, + }; + + expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values); + }); + }); + it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { testScheduler.run(({ cold, expectObservable }) => { spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { diff --git a/src/app/core/data/base/base-data.service.ts b/src/app/core/data/base/base-data.service.ts index edd6d9e2a4..18131dea63 100644 --- a/src/app/core/data/base/base-data.service.ts +++ b/src/app/core/data/base/base-data.service.ts @@ -266,6 +266,7 @@ export class BaseDataService implements HALDataServic map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow)), ); + const startTime: number = new Date().getTime(); this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); return this.rdbService.buildSingle(requestHref$, ...linksToFollow).pipe( @@ -273,7 +274,7 @@ export class BaseDataService implements HALDataServic // call it isn't immediately returned, but we wait until the remote data for the new request // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a // cached completed object - skipWhile((rd: RemoteData) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted), + skipWhile((rd: RemoteData) => rd.isStale || (!useCachedVersionIfAvailable && rd.lastUpdated < startTime)), this.reRequestStaleRemoteData(reRequestOnStale, () => this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), ); @@ -300,6 +301,7 @@ export class BaseDataService implements HALDataServic map((href: string) => this.buildHrefFromFindOptions(href, options, [], ...linksToFollow)), ); + const startTime: number = new Date().getTime(); this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); return this.rdbService.buildList(requestHref$, ...linksToFollow).pipe( @@ -307,7 +309,7 @@ export class BaseDataService implements HALDataServic // call it isn't immediately returned, but we wait until the remote data for the new request // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a // cached completed object - skipWhile((rd: RemoteData>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted), + skipWhile((rd: RemoteData>) => rd.isStale || (!useCachedVersionIfAvailable && rd.lastUpdated < startTime)), this.reRequestStaleRemoteData(reRequestOnStale, () => this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), ); From 91acc957b13675ab8c1fdab141579b8794afd2d2 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Tue, 19 Nov 2024 18:14:12 +0100 Subject: [PATCH 077/123] 120109: Fixed "no elements in sequence" sometimes being thrown on the item bitstream & relationship tabs (cherry picked from commit decacec40437e891c672d1842598dfabd55f204a) --- .../abstract-item-update/abstract-item-update.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index 80002f614b..3745b972d6 100644 --- a/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -6,7 +6,7 @@ import { ObjectUpdatesService } from '../../../core/data/object-updates/object-u import { ActivatedRoute, Router, Data } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; -import { first, map, switchMap, tap } from 'rxjs/operators'; +import { take, map, switchMap, tap } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; import { environment } from '../../../../environments/environment'; @@ -85,7 +85,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl if (this.url.indexOf('?') > 0) { this.url = this.url.substr(0, this.url.indexOf('?')); } - this.hasChanges().pipe(first()).subscribe((hasChanges) => { + this.hasChanges().pipe(take(1)).subscribe((hasChanges) => { if (!hasChanges) { this.initializeOriginalFields(); } else { @@ -170,7 +170,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl */ private checkLastModified() { const currentVersion = this.item.lastModified; - this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe( + this.objectUpdatesService.getLastModified(this.url).pipe(take(1)).subscribe( (updateVersion: Date) => { if (updateVersion.getDate() !== currentVersion.getDate()) { this.notificationsService.warning(this.getNotificationTitle('outdated'), this.getNotificationContent('outdated')); From ce17d23c08c2e5eef9f318c4095e95abbe626108 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Tue, 19 Nov 2024 18:45:16 +0100 Subject: [PATCH 078/123] 120109: Updated the route configuration to only resolve the dsoEditMenuResolver on pages who use the DsoEditMenuComponent (cherry picked from commit 5c9f494f7692ae7d664cf89d970c330528878abe) --- src/app/collection-page/collection-page-routing.module.ts | 4 +++- src/app/community-page/community-page-routing.module.ts | 4 +++- src/app/item-page/item-page-routing.module.ts | 7 ++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/app/collection-page/collection-page-routing.module.ts b/src/app/collection-page/collection-page-routing.module.ts index 9dc25b778e..6d07c467e6 100644 --- a/src/app/collection-page/collection-page-routing.module.ts +++ b/src/app/collection-page/collection-page-routing.module.ts @@ -36,7 +36,6 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; resolve: { dso: CollectionPageResolver, breadcrumb: CollectionBreadcrumbResolver, - menu: DSOEditMenuResolver }, runGuardsAndResolvers: 'always', children: [ @@ -66,6 +65,9 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; path: '', component: ThemedCollectionPageComponent, pathMatch: 'full', + resolve: { + menu: DSOEditMenuResolver, + }, } ], data: { diff --git a/src/app/community-page/community-page-routing.module.ts b/src/app/community-page/community-page-routing.module.ts index c37f8832f8..6412eb9f8e 100644 --- a/src/app/community-page/community-page-routing.module.ts +++ b/src/app/community-page/community-page-routing.module.ts @@ -29,7 +29,6 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; resolve: { dso: CommunityPageResolver, breadcrumb: CommunityBreadcrumbResolver, - menu: DSOEditMenuResolver }, runGuardsAndResolvers: 'always', children: [ @@ -49,6 +48,9 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; path: '', component: ThemedCommunityPageComponent, pathMatch: 'full', + resolve: { + menu: DSOEditMenuResolver, + }, } ], data: { diff --git a/src/app/item-page/item-page-routing.module.ts b/src/app/item-page/item-page-routing.module.ts index 0c855ab34d..5fb11f7056 100644 --- a/src/app/item-page/item-page-routing.module.ts +++ b/src/app/item-page/item-page-routing.module.ts @@ -28,7 +28,6 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; resolve: { dso: ItemPageResolver, breadcrumb: ItemBreadcrumbResolver, - menu: DSOEditMenuResolver }, runGuardsAndResolvers: 'always', children: [ @@ -36,10 +35,16 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; path: '', component: ThemedItemPageComponent, pathMatch: 'full', + resolve: { + menu: DSOEditMenuResolver, + }, }, { path: 'full', component: ThemedFullItemPageComponent, + resolve: { + menu: DSOEditMenuResolver, + }, }, { path: ITEM_EDIT_PATH, From b5a8b564730a68b382eaa4951c8fea89eb479c7a Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Tue, 29 Oct 2024 18:33:01 +0100 Subject: [PATCH 079/123] 117287: Prevent /api/eperson/epersons/undefined from being fired on the create ePerson page (cherry picked from commit 0cb5b76159ce41b8b15192010ff7c89ab9911d21) --- .../eperson-form/eperson-form.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 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 97a0a13727..9526725200 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 @@ -223,9 +223,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * This method will initialise the page */ initialisePage() { - this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData) => { - this.epersonService.editEPerson(ePersonRD.payload); - })); + if (this.route.snapshot.params.id) { + this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData) => { + this.epersonService.editEPerson(ePersonRD.payload); + })); + } this.firstName = new DynamicInputModel({ id: 'firstName', label: this.translateService.instant(`${this.messagePrefix}.firstName`), From 830ada37f5ce502cea50a7772779d657cd4d0b3d Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Wed, 6 Nov 2024 14:07:58 +0100 Subject: [PATCH 080/123] 119915: Restored functionality to hide the research profile section on the profile page (cherry picked from commit ded0079f24e0f6670f357e545e9ff14219aa4913) --- src/app/profile-page/profile-page.component.html | 2 +- src/app/profile-page/profile-page.component.spec.ts | 6 +++--- src/app/profile-page/profile-page.component.ts | 7 ------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/app/profile-page/profile-page.component.html b/src/app/profile-page/profile-page.component.html index 331c360054..e2769b38ec 100644 --- a/src/app/profile-page/profile-page.component.html +++ b/src/app/profile-page/profile-page.component.html @@ -1,7 +1,7 @@

{{'profile.title' | translate}}

- +
{{'profile.card.researcher' | translate}}
diff --git a/src/app/profile-page/profile-page.component.spec.ts b/src/app/profile-page/profile-page.component.spec.ts index 709ed56790..ec07cddd9d 100644 --- a/src/app/profile-page/profile-page.component.spec.ts +++ b/src/app/profile-page/profile-page.component.spec.ts @@ -320,7 +320,7 @@ describe('ProfilePageComponent', () => { }); it('should return true', () => { - const result = component.isResearcherProfileEnabled(); + const result = component.isResearcherProfileEnabled$; const expected = cold('a', { a: true }); @@ -336,7 +336,7 @@ describe('ProfilePageComponent', () => { }); it('should return false', () => { - const result = component.isResearcherProfileEnabled(); + const result = component.isResearcherProfileEnabled$; const expected = cold('a', { a: false }); @@ -352,7 +352,7 @@ describe('ProfilePageComponent', () => { }); it('should return false', () => { - const result = component.isResearcherProfileEnabled(); + const result = component.isResearcherProfileEnabled$; const expected = cold('a', { a: false }); diff --git a/src/app/profile-page/profile-page.component.ts b/src/app/profile-page/profile-page.component.ts index d49bdedb83..6e693461d2 100644 --- a/src/app/profile-page/profile-page.component.ts +++ b/src/app/profile-page/profile-page.component.ts @@ -192,13 +192,6 @@ export class ProfilePageComponent implements OnInit { this.updateProfile(); } - /** - * Returns true if the researcher profile feature is enabled, false otherwise. - */ - isResearcherProfileEnabled(): Observable { - return this.isResearcherProfileEnabled$.asObservable(); - } - /** * Returns an error message from a password validation request with a specific reason or * a default message without specific reason. From 5ee721f2c4b11c1d25c974d2e7c5880e7b9c45de Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Fri, 6 Dec 2024 18:08:57 +0100 Subject: [PATCH 081/123] 117287: Embed the communities/collections on the EPerson groups --- .../epeople-registry/eperson-form/eperson-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 296038ca2b..2e13671178 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 @@ -279,7 +279,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, { currentPage: 1, elementsPerPage: this.config.pageSize - }); + }, undefined, undefined, followLink('object')); } this.formGroup.patchValue({ firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '', From 297fc018922b213cc41e645e444562b562284719 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 02:29:01 +0000 Subject: [PATCH 082/123] Bump axios from 1.7.7 to 1.7.9 Bumps [axios](https://github.com/axios/axios) from 1.7.7 to 1.7.9. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.7.7...v1.7.9) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f8f725fafc..fe1adc036e 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@nicky-lenaers/ngx-scroll-to": "^14.0.0", "angular-idle-preload": "3.0.0", "angulartics2": "^12.2.1", - "axios": "^1.7.7", + "axios": "^1.7.9", "bootstrap": "^4.6.1", "cerialize": "0.1.18", "cli-progress": "^3.12.0", diff --git a/yarn.lock b/yarn.lock index 0106427952..8064831647 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3416,10 +3416,10 @@ axios@0.21.4: dependencies: follow-redirects "^1.14.0" -axios@^1.7.7: - version "1.7.7" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" - integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== +axios@^1.7.9: + version "1.7.9" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" + integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" From 5a88cedc22e2f8a3264d03795ccf3cb718d52962 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 11 Dec 2024 11:17:01 +0100 Subject: [PATCH 083/123] 118223: Remove console.log --- .../edit-item-page/item-bitstreams/item-bitstreams.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts index 9bbf380487..bc771971d3 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -318,7 +318,6 @@ export class ItemBitstreamsService { switchMap(() => this.requestService.setStaleByHrefSubstring(bundle.self)), take(1), ).subscribe(() => { - console.log('got here!'); this.isPerformingMoveRequest.next(false); finish?.(); }); From c9d6c9556337c4875cfa5d83ede37f0e720d910a Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 16 Dec 2024 14:06:50 -0600 Subject: [PATCH 084/123] Use fully qualified image names in Dockerfiles. Minor syntax fixes to ENV variables --- Dockerfile | 4 ++-- Dockerfile.dist | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8fac7495e1..e395e4b90e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # This image will be published as dspace/dspace-angular # See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details -FROM node:18-alpine +FROM docker.io/node:18-alpine # Ensure Python and other build tools are available # These are needed to install some node modules, especially on linux/arm64 @@ -24,5 +24,5 @@ ENV NODE_OPTIONS="--max_old_space_size=4096" # Listen / accept connections from all IP addresses. # NOTE: At this time it is only possible to run Docker container in Production mode # if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485 -ENV NODE_ENV development +ENV NODE_ENV=development CMD yarn serve --host 0.0.0.0 diff --git a/Dockerfile.dist b/Dockerfile.dist index 641c765cf2..c3ea539e04 100644 --- a/Dockerfile.dist +++ b/Dockerfile.dist @@ -4,7 +4,7 @@ # Test build: # docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist . -FROM node:18-alpine AS build +FROM docker.io/node:18-alpine AS build # Ensure Python and other build tools are available # These are needed to install some node modules, especially on linux/arm64 @@ -26,6 +26,6 @@ COPY --chown=node:node docker/dspace-ui.json /app/dspace-ui.json WORKDIR /app USER node -ENV NODE_ENV production +ENV NODE_ENV=production EXPOSE 4000 CMD pm2-runtime start dspace-ui.json --json From 996c877412a15483313bd7ae9f3c7f125a36f570 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 16 Dec 2024 14:12:01 -0600 Subject: [PATCH 085/123] Allow for other Docker registries to be used with all Docker compose scripts --- docker/cli.yml | 2 +- docker/db.entities.yml | 2 +- docker/docker-compose-ci.yml | 6 +++--- docker/docker-compose-dist.yml | 2 +- docker/docker-compose-rest.yml | 6 +++--- docker/docker-compose.yml | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docker/cli.yml b/docker/cli.yml index 890fbde956..11fe2ee662 100644 --- a/docker/cli.yml +++ b/docker/cli.yml @@ -21,7 +21,7 @@ networks: external: true services: dspace-cli: - image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}" + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}" container_name: dspace-cli environment: # Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. diff --git a/docker/db.entities.yml b/docker/db.entities.yml index 5c0a15c8f6..26d848e022 100644 --- a/docker/db.entities.yml +++ b/docker/db.entities.yml @@ -14,7 +14,7 @@ # # Therefore, it should be kept in sync with that file services: dspacedb: - image: dspace/dspace-postgres-pgcrypto:loadsql + image: ${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}-loadsql environment: # This LOADSQL should be kept in sync with the URL in DSpace/DSpace # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index 9431a02a87..e09d88b472 100644 --- a/docker/docker-compose-ci.yml +++ b/docker/docker-compose-ci.yml @@ -33,7 +33,7 @@ services: # This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly. solr__D__statistics__P__autoCommit: 'false' LOGGING_CONFIG: /dspace/config/log4j2-container.xml - image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}" + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}" depends_on: - dspacedb networks: @@ -60,7 +60,7 @@ services: # NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data dspacedb: container_name: dspacedb - image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x-loadsql}" + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}-loadsql" environment: # This LOADSQL should be kept in sync with the LOADSQL in # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml @@ -81,7 +81,7 @@ services: # DSpace Solr container dspacesolr: container_name: dspacesolr - image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}" + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}" networks: - dspacenet ports: diff --git a/docker/docker-compose-dist.yml b/docker/docker-compose-dist.yml index 1f4d2d7f5e..88e5be16a5 100644 --- a/docker/docker-compose-dist.yml +++ b/docker/docker-compose-dist.yml @@ -26,7 +26,7 @@ services: DSPACE_REST_HOST: demo.dspace.org DSPACE_REST_PORT: 443 DSPACE_REST_NAMESPACE: /server - image: dspace/dspace-angular:dspace-7_x-dist + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-7_x}-dist" build: context: .. dockerfile: Dockerfile.dist diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index 37bccee364..19d4d3c604 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -40,7 +40,7 @@ services: # from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above. proxies__P__trusted__P__ipranges: '172.23.0' LOGGING_CONFIG: /dspace/config/log4j2-container.xml - image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}" + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}" depends_on: - dspacedb networks: @@ -67,7 +67,7 @@ services: dspacedb: container_name: dspacedb # Uses a custom Postgres image with pgcrypto installed - image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}" + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}" environment: PGDATA: /pgdata POSTGRES_PASSWORD: dspace @@ -84,7 +84,7 @@ services: # DSpace Solr container dspacesolr: container_name: dspacesolr - image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}" + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}" networks: - dspacenet ports: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8bce9548ab..90a1d0c21c 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -23,7 +23,7 @@ services: DSPACE_REST_HOST: localhost DSPACE_REST_PORT: 8080 DSPACE_REST_NAMESPACE: /server - image: dspace/dspace-angular:dspace-7_x + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-7_x}" build: context: .. dockerfile: Dockerfile From 686b61915a39c53880af0c75c6e2fae72c90d094 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 16 Dec 2024 14:15:52 -0600 Subject: [PATCH 086/123] Update GitHub Actions for Docker & normal build to use GitHub Container Registry --- .github/workflows/build.yml | 3 +++ .github/workflows/docker.yml | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6ffa5e004..d0a03cf413 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,6 +35,9 @@ jobs: NODE_OPTIONS: '--max-old-space-size=4096' # Project name to use when running "docker compose" prior to e2e tests COMPOSE_PROJECT_NAME: 'ci' + # Docker Registry to use for Docker compose scripts below. + # We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub. + DOCKER_REGISTRY: ghcr.io strategy: # Create a matrix of Node versions to test against (in parallel) matrix: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index db42a36d3b..6b299ba6f1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -17,6 +17,7 @@ on: permissions: contents: read # to fetch code (actions/checkout) + packages: write # to write images to GitHub Container Registry (GHCR) jobs: ############################################################# From cd500007b66b239322918beaedcea028491ea13f Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 16 Dec 2024 15:09:13 -0600 Subject: [PATCH 087/123] Login to GHCR in order to have access to private Docker images for e2e tests. --- .github/workflows/build.yml | 11 ++++++++++- .github/workflows/docker.yml | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d0a03cf413..bb641bea1e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,8 @@ name: Build on: [push, pull_request] permissions: - contents: read # to fetch code (actions/checkout) + contents: read # to fetch code (actions/checkout) + packages: read # to fetch private images from GitHub Container Registry (GHCR) jobs: tests: @@ -111,6 +112,14 @@ jobs: path: 'coverage/dspace-angular/lcov.info' retention-days: 14 + # Login to our Docker registry, so that we can access private Docker images using "docker compose" below. + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + # Using "docker compose" start backend using CI configuration # and load assetstore from a cached copy - name: Start DSpace REST Backend via Docker (for e2e tests) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6b299ba6f1..c9671bcac0 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -16,7 +16,7 @@ on: pull_request: permissions: - contents: read # to fetch code (actions/checkout) + contents: read # to fetch code (actions/checkout) packages: write # to write images to GitHub Container Registry (GHCR) jobs: From 4bf4e18389abb26cecbef78760081b47cc4f95f4 Mon Sep 17 00:00:00 2001 From: Andrea-Guevara <101608067+Andrea-Guevara@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:18:24 -0300 Subject: [PATCH 088/123] More accessible file download link for users who use a screen reader (#3264) * More accessible file download link for users who use a screen reader * Refactoring implementation - More accessible file download link for users who use a screen reader * Fixing import error * Solving the spaces error * Solving the spaces error * Solving the spaces error in file pt-BR.json5 --------- Co-authored-by: andreaNeki --- .../file-download-link/file-download-link.component.html | 6 +++++- .../file-download-link/file-download-link.component.ts | 2 ++ src/assets/i18n/en.json5 | 2 ++ src/assets/i18n/es.json5 | 5 +++-- src/assets/i18n/pt-BR.json5 | 3 +++ 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/app/shared/file-download-link/file-download-link.component.html b/src/app/shared/file-download-link/file-download-link.component.html index 7a5729cc2d..59f255a652 100644 --- a/src/app/shared/file-download-link/file-download-link.component.html +++ b/src/app/shared/file-download-link/file-download-link.component.html @@ -1,4 +1,8 @@ - + diff --git a/src/app/shared/file-download-link/file-download-link.component.ts b/src/app/shared/file-download-link/file-download-link.component.ts index a79a71b634..1488401ab1 100644 --- a/src/app/shared/file-download-link/file-download-link.component.ts +++ b/src/app/shared/file-download-link/file-download-link.component.ts @@ -1,6 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { Bitstream } from '../../core/shared/bitstream.model'; import { getBitstreamDownloadRoute, getBitstreamRequestACopyRoute } from '../../app-routing-paths'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { hasValue, isNotEmpty } from '../empty.util'; @@ -48,6 +49,7 @@ export class FileDownloadLinkComponent implements OnInit { constructor( private authorizationService: AuthorizationDataService, + public dsoNameService: DSONameService, ) { } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index a3c98f7346..a56f1cd4f2 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -5386,6 +5386,8 @@ "browse.search-form.placeholder": "Search the repository", + "file-download-link.download": "Download ", + "register-page.registration.aria.label": "Enter your e-mail address", "forgot-email.form.aria.label": "Enter your e-mail address", diff --git a/src/assets/i18n/es.json5 b/src/assets/i18n/es.json5 index dfdf4ca628..51bd9c833c 100644 --- a/src/assets/i18n/es.json5 +++ b/src/assets/i18n/es.json5 @@ -7831,11 +7831,12 @@ //"browse.search-form.placeholder": "Search the repository", "browse.search-form.placeholder": "Buscar en el repositorio", + // "file-download-link.download": "Download ", + "file-download-link.download": "Descargar ", + // "register-page.registration.aria.label": "Enter your e-mail address", "register-page.registration.aria.label": "Introduzca su dirección de correo electrónico", // "forgot-email.form.aria.label": "Enter your e-mail address", "forgot-email.form.aria.label": "Introduzca su dirección de correo electrónico", - - } diff --git a/src/assets/i18n/pt-BR.json5 b/src/assets/i18n/pt-BR.json5 index eebf896889..13a6b652a6 100644 --- a/src/assets/i18n/pt-BR.json5 +++ b/src/assets/i18n/pt-BR.json5 @@ -7858,6 +7858,9 @@ //"browse.search-form.placeholder": "Search the repository", "browse.search-form.placeholder": "Buscar no repositório", + // "file-download-link.download": "Download ", + "file-download-link.download": "Baixar ", + // "register-page.registration.aria.label": "Enter your e-mail address", "register-page.registration.aria.label": "Digite seu e-mail", From 25ab69ff213f80e1cc48704317abeee92a6071dc Mon Sep 17 00:00:00 2001 From: igorbaptist4 Date: Wed, 11 Sep 2024 14:48:59 -0300 Subject: [PATCH 089/123] Configuring the URI link target --- .../metadata-values.component.html | 5 ++++- .../metadata-values.component.ts | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.html b/src/app/item-page/field-components/metadata-values/metadata-values.component.html index a598815d4c..a9576da26a 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.html +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.html @@ -18,7 +18,10 @@ - diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts index cbbae9006d..20df92099f 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts @@ -4,6 +4,7 @@ import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface'; import { BrowseDefinition } from '../../../core/shared/browse-definition.model'; import { hasValue } from '../../../shared/empty.util'; import { VALUE_LIST_BROWSE_DEFINITION } from '../../../core/shared/value-list-browse-definition.resource-type'; +import { environment } from '../../../../environments/environment'; /** * This component renders the configured 'values' into the ds-metadata-field-wrapper component. @@ -90,4 +91,25 @@ export class MetadataValuesComponent implements OnChanges { } return queryParams; } + + /** + * Checks if the given link value is an internal link. + * @param linkValue - The link value to check. + * @returns True if the link value starts with the base URL defined in the environment configuration, false otherwise. + */ + hasInternalLink(linkValue: string): boolean { + return linkValue.startsWith(environment.ui.baseUrl); + } + + /** + * This method performs a validation and determines the target of the url. + * @returns - Returns the target url. + */ + getLinkAttributes(urlValue: string): { target: string, rel: string } { + if (this.hasInternalLink(urlValue)) { + return { target: '_self', rel: '' }; + } else { + return { target: '_blank', rel: 'noopener noreferrer' }; + } + } } From 2a1ef02d7583eacb2e59d58c33486102ba6e2f17 Mon Sep 17 00:00:00 2001 From: igorbaptist4 Date: Wed, 11 Sep 2024 15:15:26 -0300 Subject: [PATCH 090/123] fix identation --- .../metadata-values/metadata-values.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts index 20df92099f..a902e48ed1 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts @@ -105,7 +105,7 @@ export class MetadataValuesComponent implements OnChanges { * This method performs a validation and determines the target of the url. * @returns - Returns the target url. */ - getLinkAttributes(urlValue: string): { target: string, rel: string } { + getLinkAttributes(urlValue: string): { target: string, rel: string } { if (this.hasInternalLink(urlValue)) { return { target: '_self', rel: '' }; } else { From c6ef2f1bd095e5e1bd417600cc817198cd8fd494 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 11 Dec 2024 13:07:48 -0300 Subject: [PATCH 091/123] Addition of unit tests for the getLinkAttributes() method --- .../metadata-values.component.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts index 23f8098207..de46cd9e4c 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts @@ -73,4 +73,20 @@ describe('MetadataValuesComponent', () => { expect(comp.hasLink(mdValue)).toBe(true); }); + it('should return correct target and rel for internal links', () => { + spyOn(comp, 'hasInternalLink').and.returnValue(true); + const urlValue = '/internal-link'; + const result = comp.getLinkAttributes(urlValue); + expect(result.target).toBe('_self'); + expect(result.rel).toBe(''); + }); + + it('should return correct target and rel for external links', () => { + spyOn(comp, 'hasInternalLink').and.returnValue(false); + const urlValue = 'https://www.dspace.org'; + const result = comp.getLinkAttributes(urlValue); + expect(result.target).toBe('_blank'); + expect(result.rel).toBe('noopener noreferrer'); + }); + }); From b601405e564bf6c668a3cd6086c6c9d9a56eb2f8 Mon Sep 17 00:00:00 2001 From: andreaNeki Date: Fri, 27 Sep 2024 13:44:31 -0300 Subject: [PATCH 092/123] Adding the aria-label attribute to buttons --- .../vocabulary-treeview.component.html | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html index a0d320a854..a37b26e348 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html @@ -6,13 +6,16 @@ [attr.aria-label]="'vocabulary-treeview.search.form.search-placeholder' | translate" [placeholder]="'vocabulary-treeview.search.form.search-placeholder' | translate">
- - -
@@ -88,13 +91,15 @@ - - From 14680b013e5c7ced5d4243806a7d7a027111620d Mon Sep 17 00:00:00 2001 From: andreaNeki Date: Fri, 27 Sep 2024 14:01:44 -0300 Subject: [PATCH 093/123] Adding focus to the input after the reset button is clicked --- .../vocabulary-treeview.component.html | 2 +- .../vocabulary-treeview/vocabulary-treeview.component.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html index a37b26e348..a216384c9c 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html @@ -2,7 +2,7 @@
-
diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts index b1edda461e..2602cfd75e 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts @@ -1,5 +1,5 @@ import { FlatTreeControl } from '@angular/cdk/tree'; -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; import { Store } from '@ngrx/store'; @@ -29,6 +29,11 @@ import { AlertType } from '../../alert/alert-type'; }) export class VocabularyTreeviewComponent implements OnDestroy, OnInit, OnChanges { + /** + * Implemented to manage focus on input + */ + @ViewChild('searchInput') searchInput: ElementRef; + /** * The {@link VocabularyOptions} object */ @@ -294,6 +299,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit, OnChanges this.storedNodeMap = new Map(); this.vocabularyTreeviewService.restoreNodes(); } + this.searchInput.nativeElement.focus(); } add() { From 75260b00d95d6bf3024a9387127ebab65e64b368 Mon Sep 17 00:00:00 2001 From: andreaNeki Date: Fri, 27 Sep 2024 15:08:53 -0300 Subject: [PATCH 094/123] =?UTF-8?q?Ensuring=20that=20the=20message=20?= =?UTF-8?q?=E2=80=9CThere=20were=20no=20items=20to=20show=E2=80=9D=20is=20?= =?UTF-8?q?announced=20to=20the=20screen=20reader=20when=20necessary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vocabulary-treeview.component.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html index a216384c9c..ad894328a6 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html @@ -24,9 +24,11 @@
-

- {{'vocabulary-treeview.search.no-result' | translate}} -

+
+

+ {{'vocabulary-treeview.search.no-result' | translate}} +

+
From a8ff6a41e051cb68c6083da2df588888d37e2903 Mon Sep 17 00:00:00 2001 From: andreaNeki Date: Fri, 27 Sep 2024 16:18:56 -0300 Subject: [PATCH 095/123] Trying to correct an error in the focus implementation --- .../vocabulary-treeview/vocabulary-treeview.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts index 2602cfd75e..5695644040 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts @@ -32,7 +32,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit, OnChanges /** * Implemented to manage focus on input */ - @ViewChild('searchInput') searchInput: ElementRef; + @ViewChild('searchInput') searchInput!: ElementRef; /** * The {@link VocabularyOptions} object @@ -299,7 +299,9 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit, OnChanges this.storedNodeMap = new Map(); this.vocabularyTreeviewService.restoreNodes(); } - this.searchInput.nativeElement.focus(); + if (this.searchInput) { + this.searchInput.nativeElement.focus(); + } } add() { From abc3c41c635e5d8df49c75cddab40aa2951bcbb2 Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Mon, 18 Nov 2024 14:07:34 +0100 Subject: [PATCH 096/123] [DURACOM-297] fix changing values in submission form after ordering (cherry picked from commit 1c9272107c427e3c1210c9df2c3f859da2463278) --- .../array-group/dynamic-form-array.component.html | 6 +++--- .../array-group/dynamic-form-array.component.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html index dd19e6158d..a7a6b103e4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html @@ -7,9 +7,9 @@
-
Date: Mon, 23 Dec 2024 02:28:39 +0000 Subject: [PATCH 097/123] Bump mirador from 3.3.0 to 3.4.2 Bumps [mirador](https://github.com/ProjectMirador/mirador) from 3.3.0 to 3.4.2. - [Release notes](https://github.com/ProjectMirador/mirador/releases) - [Commits](https://github.com/ProjectMirador/mirador/compare/v3.3.0...v3.4.2) --- updated-dependencies: - dependency-name: mirador dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 93c54cc7b7..613def9edf 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "lru-cache": "^7.14.1", "markdown-it": "^13.0.2", "markdown-it-mathjax3": "^4.3.2", - "mirador": "^3.3.0", + "mirador": "^3.4.2", "mirador-dl-plugin": "^0.13.0", "mirador-share-plugin": "^0.16.0", "morgan": "^1.10.0", diff --git a/yarn.lock b/yarn.lock index f1fcbe2cc2..fc1475cd45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1864,7 +1864,7 @@ resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== -"@material-ui/core@^4.11.0", "@material-ui/core@^4.12.4": +"@material-ui/core@^4.12.3", "@material-ui/core@^4.12.4": version "4.12.4" resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.4.tgz" integrity sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ== @@ -8212,12 +8212,12 @@ mirador-share-plugin@^0.16.0: dependencies: notistack "^3.0.1" -mirador@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/mirador/-/mirador-3.3.0.tgz" - integrity sha512-BmGfRnWJ45B+vtiAwcFT7n9nKialfejE9UvuUK0NorO37ShArpsKr3yVSD4jQASwSR4DRRpPEG21jOk4WN7H3w== +mirador@^3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/mirador/-/mirador-3.4.2.tgz#51ec7ca1b9854792bbe2fa5e13b3692c92e97102" + integrity sha512-Gd7G4NkXq6/qD/De5soYspSo9VykAzrGFunKqUI3x9WShoZP23pYIEPoC/96tvfk3KMv+UbAUxDp99Xeo7vnVQ== dependencies: - "@material-ui/core" "^4.11.0" + "@material-ui/core" "^4.12.3" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "^4.0.0-alpha.53" "@researchgate/react-intersection-observer" "^1.0.0" @@ -8234,7 +8234,7 @@ mirador@^3.3.0: lodash "^4.17.11" manifesto.js "^4.2.0" normalize-url "^4.5.0" - openseadragon "^2.4.2" + openseadragon "^2.4.2 || ^3.0.0 || 4.0.x || ^4.1.1 || ^5.0.0" prop-types "^15.6.2" re-reselect "^4.0.0" react-aria-live "^2.0.5" @@ -8764,10 +8764,10 @@ open@8.4.2, open@^8.0.9: is-docker "^2.1.1" is-wsl "^2.2.0" -openseadragon@^2.4.2: - version "2.4.2" - resolved "https://registry.npmjs.org/openseadragon/-/openseadragon-2.4.2.tgz" - integrity sha512-398KbZwRtOYA6OmeWRY4Q0737NTacQ9Q6whmr9Lp1MNQO3p0eBz5LIASRne+4gwequcSM1vcHcjfy3dIndQziw== +"openseadragon@^2.4.2 || ^3.0.0 || 4.0.x || ^4.1.1 || ^5.0.0": + version "5.0.1" + resolved "https://registry.yarnpkg.com/openseadragon/-/openseadragon-5.0.1.tgz#ad3aaccc6c0f733c3153131e9af05bc2e17a1952" + integrity sha512-a/hjouW9i3UfWxRADVYN2MyRhXMGnE7x9VVL7/4jXCcDLFyO4UM5o4RStYtqa5BfaHw/wMNAaD2WbxQF8f1pJg== openurl@1.1.1: version "1.1.1" From 5c8eabddab7c4432942bdf1c52b5175481c1f233 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 02:28:53 +0000 Subject: [PATCH 098/123] Bump @fortawesome/fontawesome-free from 6.6.0 to 6.7.2 Bumps [@fortawesome/fontawesome-free](https://github.com/FortAwesome/Font-Awesome) from 6.6.0 to 6.7.2. - [Release notes](https://github.com/FortAwesome/Font-Awesome/releases) - [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md) - [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.6.0...6.7.2) --- updated-dependencies: - dependency-name: "@fortawesome/fontawesome-free" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 93c54cc7b7..dc984272f9 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "@angular/compiler-cli": "^15.2.10", "@angular/language-service": "^15.2.10", "@cypress/schematic": "^1.5.0", - "@fortawesome/fontawesome-free": "^6.6.0", + "@fortawesome/fontawesome-free": "^6.7.2", "@material-ui/core": "^4.12.4", "@material-ui/icons": "^4.11.3", "@ngrx/store-devtools": "^15.4.0", diff --git a/yarn.lock b/yarn.lock index f1fcbe2cc2..ce0490ec10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1749,10 +1749,10 @@ resolved "https://registry.yarnpkg.com/@fastify/deepmerge/-/deepmerge-1.3.0.tgz#8116858108f0c7d9fd460d05a7d637a13fe3239a" integrity sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A== -"@fortawesome/fontawesome-free@^6.6.0": - version "6.6.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz#0e984f0f2344ee513c185d87d77defac4c0c8224" - integrity sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow== +"@fortawesome/fontawesome-free@^6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz#8249de9b7e22fcb3ceb5e66090c30a1d5492b81a" + integrity sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA== "@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3": version "1.1.3" From 33fdee554ec4921cc5b11e00cebbc41b6fddcd2a Mon Sep 17 00:00:00 2001 From: jensvannerum <78496674+jensvannerum@users.noreply.github.com> Date: Wed, 1 Jan 2025 10:50:30 +0100 Subject: [PATCH 099/123] [Port dspace-7_x] Get rid of unnecessary and failing REST requests when navigating between different browse indexes (#3788) Co-authored-by: Koen Pauwels Co-authored-by: Jens Vannerum --- .../browse-by-date-page.component.ts | 18 +++++++++--------- .../browse-by-metadata-page.component.ts | 8 ++++++-- .../browse-by-title-page.component.ts | 8 +++----- src/app/menu.resolver.spec.ts | 11 +++++++---- src/app/menu.resolver.ts | 17 ++++++++++++++--- 5 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts index d3c832d3e2..7e0b6f0f88 100644 --- a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts +++ b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts @@ -1,8 +1,7 @@ import { ChangeDetectorRef, Component, Inject } from '@angular/core'; import { BrowseByMetadataPageComponent, - browseParamsToOptions, - getBrowseSearchOptions + browseParamsToOptions } from '../browse-by-metadata-page/browse-by-metadata-page.component'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; @@ -11,7 +10,7 @@ import { BrowseService } from '../../core/browse/browse.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { StartsWithType } from '../../shared/starts-with/starts-with-decorator'; import { PaginationService } from '../../core/pagination/pagination.service'; -import { map } from 'rxjs/operators'; +import { map, take } from 'rxjs/operators'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { isValidDate } from '../../shared/date.util'; @@ -52,15 +51,16 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { ngOnInit(): void { const sortConfig = new SortOptions('default', SortDirection.ASC); this.startsWithType = StartsWithType.date; - // include the thumbnail configuration in browse search options - this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails)); this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.subs.push( - observableCombineLatest([this.route.params, this.route.queryParams, this.route.data, - this.currentPagination$, this.currentSort$]).pipe( - map(([routeParams, queryParams, data, currentPage, currentSort]) => { - return [Object.assign({}, routeParams, queryParams, data), currentPage, currentSort]; + observableCombineLatest( + [ this.route.params.pipe(take(1)), + this.route.queryParams, + this.currentPagination$, + this.currentSort$]).pipe( + map(([routeParams, queryParams, currentPage, currentSort]) => { + return [Object.assign({}, routeParams, queryParams), currentPage, currentSort]; }) ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys; diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts index c03cf03429..fe407a2fb0 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts +++ b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts @@ -15,7 +15,7 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { StartsWithType } from '../../shared/starts-with/starts-with-decorator'; import { PaginationService } from '../../core/pagination/pagination.service'; -import { filter, map, mergeMap } from 'rxjs/operators'; +import { filter, map, mergeMap, take } from 'rxjs/operators'; import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { Bitstream } from '../../core/shared/bitstream.model'; import { Collection } from '../../core/shared/collection.model'; @@ -152,7 +152,11 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.subs.push( - observableCombineLatest([this.route.params, this.route.queryParams, this.currentPagination$, this.currentSort$]).pipe( + observableCombineLatest( + [ this.route.params.pipe(take(1)), + this.route.queryParams, + this.currentPagination$, + this.currentSort$]).pipe( map(([routeParams, queryParams, currentPage, currentSort]) => { return [Object.assign({}, routeParams, queryParams),currentPage,currentSort]; }) diff --git a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts index 58df79ebe8..11dc2a2a6a 100644 --- a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts +++ b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts @@ -4,13 +4,13 @@ import { ActivatedRoute, Params, Router } from '@angular/router'; import { hasValue } from '../../shared/empty.util'; import { BrowseByMetadataPageComponent, - browseParamsToOptions, getBrowseSearchOptions + browseParamsToOptions } from '../browse-by-metadata-page/browse-by-metadata-page.component'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { BrowseService } from '../../core/browse/browse.service'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { PaginationService } from '../../core/pagination/pagination.service'; -import { map } from 'rxjs/operators'; +import { map, take } from 'rxjs/operators'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @@ -38,12 +38,10 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent { ngOnInit(): void { const sortConfig = new SortOptions('dc.title', SortDirection.ASC); - // include the thumbnail configuration in browse search options - this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails)); this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.subs.push( - observableCombineLatest([this.route.params, this.route.queryParams, this.currentPagination$, this.currentSort$]).pipe( + observableCombineLatest([this.route.params.pipe(take(1)), this.route.queryParams, this.currentPagination$, this.currentSort$]).pipe( map(([routeParams, queryParams, currentPage, currentSort]) => { return [Object.assign({}, routeParams, queryParams),currentPage,currentSort]; }) diff --git a/src/app/menu.resolver.spec.ts b/src/app/menu.resolver.spec.ts index 838d5a53c5..12d5efa24e 100644 --- a/src/app/menu.resolver.spec.ts +++ b/src/app/menu.resolver.spec.ts @@ -16,9 +16,11 @@ import { MenuServiceStub } from './shared/testing/menu-service.stub'; import { MenuID } from './shared/menu/menu-id.model'; import { BrowseService } from './core/browse/browse.service'; import { cold } from 'jasmine-marbles'; -import createSpy = jasmine.createSpy; import { createSuccessfulRemoteDataObject$ } from './shared/remote-data.utils'; import { createPaginatedList } from './shared/testing/utils.test'; +import createSpy = jasmine.createSpy; +import { AuthService } from './core/auth/auth.service'; +import { AuthServiceStub } from './shared/testing/auth-service.stub'; const BOOLEAN = { t: true, f: false }; const MENU_STATE = { @@ -61,6 +63,7 @@ describe('MenuResolver', () => { { provide: BrowseService, useValue: browseService }, { provide: AuthorizationDataService, useValue: authorizationService }, { provide: ScriptDataService, useValue: scriptService }, + { provide: AuthService, useValue: AuthServiceStub }, { provide: NgbModal, useValue: { open: () => {/*comment*/ @@ -80,19 +83,19 @@ describe('MenuResolver', () => { describe('resolve', () => { it('should create all menus', (done) => { spyOn(resolver, 'createPublicMenu$').and.returnValue(observableOf(true)); - spyOn(resolver, 'createAdminMenu$').and.returnValue(observableOf(true)); + spyOn(resolver, 'createAdminMenuIfLoggedIn$').and.returnValue(observableOf(true)); resolver.resolve(null, null).subscribe(resolved => { expect(resolved).toBeTrue(); expect(resolver.createPublicMenu$).toHaveBeenCalled(); - expect(resolver.createAdminMenu$).toHaveBeenCalled(); + expect(resolver.createAdminMenuIfLoggedIn$).toHaveBeenCalled(); done(); }); }); it('should return an Observable that emits true as soon as all menus are created', () => { spyOn(resolver, 'createPublicMenu$').and.returnValue(cold('--(t|)', BOOLEAN)); - spyOn(resolver, 'createAdminMenu$').and.returnValue(cold('----(t|)', BOOLEAN)); + spyOn(resolver, 'createAdminMenuIfLoggedIn$').and.returnValue(cold('----(t|)', BOOLEAN)); expect(resolver.resolve(null, null)).toBeObservable(cold('----(t|)', BOOLEAN)); }); diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts index cad6a6ec57..83c9351c7f 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu.resolver.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; -import { combineLatest as observableCombineLatest, combineLatest, Observable } from 'rxjs'; +import { combineLatest as observableCombineLatest, combineLatest, Observable, of as observableOf } from 'rxjs'; import { MenuID } from './shared/menu/menu-id.model'; import { MenuState } from './shared/menu/menu-state.model'; import { MenuItemType } from './shared/menu/menu-item-type.model'; @@ -12,7 +12,7 @@ import { RemoteData } from './core/data/remote-data'; import { TextMenuItemModel } from './shared/menu/menu-item/models/text.model'; import { BrowseService } from './core/browse/browse.service'; import { MenuService } from './shared/menu/menu.service'; -import { filter, find, map, take } from 'rxjs/operators'; +import { filter, find, map, mergeMap, take } from 'rxjs/operators'; import { hasValue } from './shared/empty.util'; import { FeatureID } from './core/data/feature-authorization/feature-id'; import { @@ -47,6 +47,7 @@ import { import { ExportBatchSelectorComponent } from './shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component'; +import { AuthService } from './core/auth/auth.service'; /** * Creates all of the app's menus @@ -61,6 +62,7 @@ export class MenuResolver implements Resolve { protected authorizationService: AuthorizationDataService, protected modalService: NgbModal, protected scriptDataService: ScriptDataService, + protected authService: AuthService, ) { } @@ -70,7 +72,7 @@ export class MenuResolver implements Resolve { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { return combineLatest([ this.createPublicMenu$(), - this.createAdminMenu$(), + this.createAdminMenuIfLoggedIn$(), ]).pipe( map((menusDone: boolean[]) => menusDone.every(Boolean)), ); @@ -146,6 +148,15 @@ export class MenuResolver implements Resolve { return this.waitForMenu$(MenuID.PUBLIC); } + /** + * Initialize all menu sections and items for {@link MenuID.ADMIN}, only if the user is logged in. + */ + createAdminMenuIfLoggedIn$() { + return this.authService.isAuthenticated().pipe( + mergeMap((isAuthenticated) => isAuthenticated ? this.createAdminMenu$() : observableOf(true)), + ); + } + /** * Initialize all menu sections and items for {@link MenuID.ADMIN} */ From ecf3298345373aa51cb7c6523761b0f9d4f0a6f7 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Fri, 6 Dec 2024 17:34:37 +0100 Subject: [PATCH 100/123] fix value of selector in component annotation (cherry picked from commit 71de4b600b8ed268cbc88a1191f6c2f931e23705) --- .../org-unit-search-result-list-submission-element.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts index ef58007bae..76a5618292 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts @@ -22,7 +22,7 @@ import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.inter @listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModal) @listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModalWithNameVariants) @Component({ - selector: 'ds-person-search-result-list-submission-element', + selector: 'ds-org-unit-search-result-list-submission-element', styleUrls: ['./org-unit-search-result-list-submission-element.component.scss'], templateUrl: './org-unit-search-result-list-submission-element.component.html' }) From 894448e3dc6e161b947c6affb301d4047d9c6607 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 03:04:09 +0000 Subject: [PATCH 101/123] Bump postcss in the postcss group across 1 directory Bumps the postcss group with 1 update in the / directory: [postcss](https://github.com/postcss/postcss). Updates `postcss` from 8.4.47 to 8.4.49 - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.47...8.4.49) --- updated-dependencies: - dependency-name: postcss dependency-type: direct:development update-type: version-update:semver-patch dependency-group: postcss ... Signed-off-by: dependabot[bot] --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index f1fcbe2cc2..42cdc77d95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9051,7 +9051,7 @@ performance-now@^2.1.0: resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== -picocolors@^1.0.0, picocolors@^1.1.0: +picocolors@^1.0.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -9425,12 +9425,12 @@ postcss@8.4.31: source-map-js "^1.0.2" postcss@^8.2.14, postcss@^8.3.11, postcss@^8.3.7, postcss@^8.4, postcss@^8.4.19: - version "8.4.47" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" - integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== + version "8.4.49" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" + integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== dependencies: nanoid "^3.3.7" - picocolors "^1.1.0" + picocolors "^1.1.1" source-map-js "^1.2.1" prelude-ls@^1.2.1: From d3d86f4f2d5da2c184eea4c522d0f6b163855a2e Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Sat, 7 Dec 2024 20:58:52 +0100 Subject: [PATCH 102/123] 119799: Prevent the lookup/lookup-name fields from resetting when hitting the enter key in another input field (cherry picked from commit c8694e1a87ab6ad70ef0f984a77f3e12dbd5605d) --- .../models/lookup/dynamic-lookup.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html index 0ecddf9278..1920209368 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html @@ -94,11 +94,11 @@ (scrolled)="onScroll()" [scrollWindow]="false"> - -
From 60586f6c33b37c1b3c874839d98f4a31e759c94b Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Fri, 6 Dec 2024 18:05:07 +0100 Subject: [PATCH 109/123] fix nested span element (cherry picked from commit 97b5e0cc92420366355c00f5013550ce7715801d) --- ...-unit-search-result-list-submission-element.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html index e264958738..1f020d127f 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html @@ -11,11 +11,11 @@ - , + , - +
From 7538f49a36c4b8971387de4ea884597ec5fcaeb8 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Fri, 6 Dec 2024 18:09:02 +0100 Subject: [PATCH 110/123] remove nested span element (cherry picked from commit acb7c9bd33bb678766ab219851999f217f7b81d5) --- .../journal-issue-search-result-list-element.component.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html index 2dd721b8f3..2de765d712 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html @@ -31,7 +31,6 @@ - - - ; From 04123f92aae4b654c98ebc7eed5c9b68d7342a24 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Fri, 6 Dec 2024 18:12:44 +0100 Subject: [PATCH 111/123] fix indentation (cherry picked from commit dbd67f056ebff2b5dd62264f8724c64cc11b5d4a) --- ...-search-result-list-element.component.html | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html index 2de765d712..3d1d7c3e09 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html @@ -22,21 +22,21 @@ class="lead item-list-title dont-break-out" [innerHTML]="dsoTitle"> - - - - ; - - - - - ; - - + + + + ; + + + - ; + + + - +
From 76539dd8a18953f897148f484b8fdd45dc6f4dd9 Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Tue, 7 Jan 2025 22:23:48 +0100 Subject: [PATCH 112/123] Reduce browse definition requests on simple item page (#3701) * 121561 Reduce the number of browse definition requests on Item pages by reusing the navbar request for all browse indexes * Fix test issues. --------- Co-authored-by: Koen Pauwels --- .../core/shared/browse-definition.model.ts | 8 +- .../hierarchical-browse-definition.model.ts | 5 +- .../non-hierarchical-browse-definition.ts | 3 - .../journal/journal.component.spec.ts | 5 +- ...item-page-abstract-field.component.spec.ts | 5 +- .../item-page-author-field.component.spec.ts | 5 +- .../item-page-date-field.component.spec.ts | 5 +- .../generic-item-page-field.component.spec.ts | 5 +- .../item-page-field.component.spec.ts | 5 +- .../item-page-field.component.ts | 38 +++++++- .../uri/item-page-uri-field.component.spec.ts | 5 +- .../publication/publication.component.spec.ts | 3 + .../item-types/shared/item.component.spec.ts | 3 + .../untyped-item.component.spec.ts | 3 + src/app/shared/testing/browse-service.stub.ts | 91 +++++++++++++++++++ 15 files changed, 169 insertions(+), 20 deletions(-) create mode 100644 src/app/shared/testing/browse-service.stub.ts diff --git a/src/app/core/shared/browse-definition.model.ts b/src/app/core/shared/browse-definition.model.ts index a5bed53c9f..bd7e4862ce 100644 --- a/src/app/core/shared/browse-definition.model.ts +++ b/src/app/core/shared/browse-definition.model.ts @@ -1,4 +1,7 @@ -import { autoserialize } from 'cerialize'; +import { + autoserialize, + autoserializeAs, +} from 'cerialize'; import { CacheableObject } from '../cache/cacheable-object.model'; /** @@ -9,6 +12,9 @@ export abstract class BrowseDefinition extends CacheableObject { @autoserialize id: string; + @autoserializeAs('metadata') + metadataKeys: string[]; + /** * Get the render type of the BrowseDefinition model */ diff --git a/src/app/core/shared/hierarchical-browse-definition.model.ts b/src/app/core/shared/hierarchical-browse-definition.model.ts index d561fff643..b64ecabdf2 100644 --- a/src/app/core/shared/hierarchical-browse-definition.model.ts +++ b/src/app/core/shared/hierarchical-browse-definition.model.ts @@ -1,4 +1,4 @@ -import { autoserialize, autoserializeAs, deserialize, inheritSerialization } from 'cerialize'; +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { typedObject } from '../cache/builders/build-decorators'; import { excludeFromEquals } from '../utilities/equals.decorators'; import { HIERARCHICAL_BROWSE_DEFINITION } from './hierarchical-browse-definition.resource-type'; @@ -26,9 +26,6 @@ export class HierarchicalBrowseDefinition extends BrowseDefinition { @autoserialize vocabulary: string; - @autoserializeAs('metadata') - metadataKeys: string[]; - get self(): string { return this._links.self.href; } diff --git a/src/app/core/shared/non-hierarchical-browse-definition.ts b/src/app/core/shared/non-hierarchical-browse-definition.ts index d5481fcc8d..cba6f3ecd6 100644 --- a/src/app/core/shared/non-hierarchical-browse-definition.ts +++ b/src/app/core/shared/non-hierarchical-browse-definition.ts @@ -16,9 +16,6 @@ export abstract class NonHierarchicalBrowseDefinition extends BrowseDefinition { @autoserializeAs('order') defaultSortOrder: string; - @autoserializeAs('metadata') - metadataKeys: string[]; - @autoserialize dataType: BrowseByDataType; } diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts index 6e2ded334b..ace7a69f9a 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts @@ -39,6 +39,8 @@ import { BrowseDefinitionDataServiceStub } from '../../../../shared/testing/browse-definition-data-service.stub'; import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service'; +import { BrowseService } from '../../../../core/browse/browse.service'; +import { BrowseServiceStub } from '../../../../shared/testing/browse-service.stub'; let comp: JournalComponent; let fixture: ComponentFixture; @@ -105,7 +107,8 @@ describe('JournalComponent', () => { { provide: WorkspaceitemDataService, useValue: {} }, { provide: SearchService, useValue: {} }, { provide: RouteService, useValue: mockRouteService }, - { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: BrowseService, useValue: BrowseServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.spec.ts index 4fb8889440..6bbee90945 100644 --- a/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.spec.ts @@ -6,6 +6,8 @@ import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loa import { SharedModule } from '../../../../../shared/shared.module'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; import { environment } from '../../../../../../environments/environment'; +import { BrowseService } from '../../../../../core/browse/browse.service'; +import { BrowseServiceStub } from '../../../../../shared/testing/browse-service.stub'; import { By } from '@angular/platform-browser'; import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service'; import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub'; @@ -27,7 +29,8 @@ describe('ItemPageAbstractFieldComponent', () => { ], providers: [ { provide: APP_CONFIG, useValue: environment }, - { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: BrowseService, useValue: BrowseServiceStub }, ], declarations: [ItemPageAbstractFieldComponent], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/field-components/specific-field/author/item-page-author-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/author/item-page-author-field.component.spec.ts index 855a995142..d45261b85b 100644 --- a/src/app/item-page/simple/field-components/specific-field/author/item-page-author-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/author/item-page-author-field.component.spec.ts @@ -7,6 +7,8 @@ import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component import { ItemPageAuthorFieldComponent } from './item-page-author-field.component'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; import { environment } from '../../../../../../environments/environment'; +import { BrowseService } from '../../../../../core/browse/browse.service'; +import { BrowseServiceStub } from '../../../../../shared/testing/browse-service.stub'; import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service'; import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub'; @@ -27,7 +29,8 @@ describe('ItemPageAuthorFieldComponent', () => { })], providers: [ { provide: APP_CONFIG, useValue: environment }, - { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: BrowseService, useValue: BrowseServiceStub }, ], declarations: [ItemPageAuthorFieldComponent, MetadataValuesComponent], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/field-components/specific-field/date/item-page-date-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/date/item-page-date-field.component.spec.ts index be124dab82..a88e9f74a7 100644 --- a/src/app/item-page/simple/field-components/specific-field/date/item-page-date-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/date/item-page-date-field.component.spec.ts @@ -7,6 +7,8 @@ import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component import { ItemPageDateFieldComponent } from './item-page-date-field.component'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; import { environment } from '../../../../../../environments/environment'; +import { BrowseService } from '../../../../../core/browse/browse.service'; +import { BrowseServiceStub } from '../../../../../shared/testing/browse-service.stub'; import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service'; import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub'; @@ -27,7 +29,8 @@ describe('ItemPageDateFieldComponent', () => { })], providers: [ { provide: APP_CONFIG, useValue: environment }, - { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: BrowseService, useValue: BrowseServiceStub }, ], declarations: [ItemPageDateFieldComponent, MetadataValuesComponent], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.spec.ts index fdf5ac1bb5..606f43ed9a 100644 --- a/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.spec.ts @@ -7,6 +7,8 @@ import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component import { GenericItemPageFieldComponent } from './generic-item-page-field.component'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; import { environment } from '../../../../../../environments/environment'; +import { BrowseService } from '../../../../../core/browse/browse.service'; +import { BrowseServiceStub } from '../../../../../shared/testing/browse-service.stub'; import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service'; import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub'; @@ -29,7 +31,8 @@ describe('GenericItemPageFieldComponent', () => { })], providers: [ { provide: APP_CONFIG, useValue: environment }, - { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: BrowseService, useValue: BrowseServiceStub }, ], declarations: [GenericItemPageFieldComponent, MetadataValuesComponent], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts index 15b7a9df21..f0bddbc695 100644 --- a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts @@ -13,6 +13,8 @@ import { MarkdownPipe } from '../../../../shared/utils/markdown.pipe'; import { SharedModule } from '../../../../shared/shared.module'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; import { By } from '@angular/platform-browser'; +import { BrowseService } from '../../../../core/browse/browse.service'; +import { BrowseServiceStub } from '../../../../shared/testing/browse-service.stub'; import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service'; import { BrowseDefinitionDataServiceStub } from '../../../../shared/testing/browse-definition-data-service.stub'; import { RouterTestingModule } from '@angular/router/testing'; @@ -51,7 +53,8 @@ describe('ItemPageFieldComponent', () => { ], providers: [ { provide: APP_CONFIG, useValue: appConfig }, - { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: BrowseService, useValue: BrowseServiceStub }, ], declarations: [ItemPageFieldComponent, MetadataValuesComponent], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts index 3a18666a81..aa1e78f229 100644 --- a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts @@ -1,10 +1,20 @@ import { Component, Input } from '@angular/core'; import { Item } from '../../../../core/shared/item.model'; -import { map } from 'rxjs/operators'; +import intersectionWith from 'lodash/intersectionWith'; +import { + filter, + mergeAll, + take, +} from 'rxjs/operators'; import { Observable } from 'rxjs'; +import { BrowseService } from '../../../../core/browse/browse.service'; import { BrowseDefinition } from '../../../../core/shared/browse-definition.model'; import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service'; -import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; +import { + getFirstCompletedRemoteData, + getPaginatedListPayload, + getRemoteDataPayload, +} from '../../../../core/shared/operators'; /** * This component can be used to represent metadata on a simple item page. @@ -17,7 +27,8 @@ import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; }) export class ItemPageFieldComponent { - constructor(protected browseDefinitionDataService: BrowseDefinitionDataService) { + constructor(protected browseDefinitionDataService: BrowseDefinitionDataService, + protected browseService: BrowseService) { } /** @@ -56,9 +67,26 @@ export class ItemPageFieldComponent { * link in dspace.cfg (webui.browse.link.) */ get browseDefinition(): Observable { - return this.browseDefinitionDataService.findByFields(this.fields).pipe( + return this.browseService.getBrowseDefinitions().pipe( getFirstCompletedRemoteData(), - map((def) => def.payload) + getRemoteDataPayload(), + getPaginatedListPayload(), + mergeAll(), + filter((def: BrowseDefinition) => + intersectionWith(def.metadataKeys, this.fields, ItemPageFieldComponent.fieldMatch).length > 0, + ), + take(1), ); } + + /** + * Returns true iff the spec and field match. + * @param spec Specification of a metadata field name: either a metadata field, or a prefix ending in ".*". + * @param field A metadata field name. + * @private + */ + private static fieldMatch(spec: string, field: string): boolean { + return field === spec + || (spec.endsWith('.*') && field.substring(0, spec.length - 1) === spec.substring(0, spec.length - 1)); + } } diff --git a/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.spec.ts index cc55b76e3e..7b969737db 100644 --- a/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.spec.ts @@ -7,6 +7,8 @@ import { ItemPageUriFieldComponent } from './item-page-uri-field.component'; import { MetadataUriValuesComponent } from '../../../../field-components/metadata-uri-values/metadata-uri-values.component'; import { environment } from '../../../../../../environments/environment'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; +import { BrowseService } from '../../../../../core/browse/browse.service'; +import { BrowseServiceStub } from '../../../../../shared/testing/browse-service.stub'; import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service'; import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub'; @@ -28,7 +30,8 @@ describe('ItemPageUriFieldComponent', () => { })], providers: [ { provide: APP_CONFIG, useValue: environment }, - { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: BrowseService, useValue: BrowseServiceStub }, ], declarations: [ItemPageUriFieldComponent, MetadataUriValuesComponent], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts index 211ec102bc..98db2da7f0 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts +++ b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts @@ -40,6 +40,8 @@ import { BrowseDefinitionDataService } from '../../../../core/browse/browse-defi import { BrowseDefinitionDataServiceStub } from '../../../../shared/testing/browse-definition-data-service.stub'; +import { BrowseService } from '../../../../core/browse/browse.service'; +import { BrowseServiceStub } from '../../../../shared/testing/browse-service.stub'; const noMetadata = new MetadataMap(); @@ -93,6 +95,7 @@ describe('PublicationComponent', () => { { provide: SearchService, useValue: {} }, { provide: RouteService, useValue: mockRouteService }, { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: BrowseService, useValue: BrowseServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/item-types/shared/item.component.spec.ts b/src/app/item-page/simple/item-types/shared/item.component.spec.ts index 5bf08fc004..558cd6d379 100644 --- a/src/app/item-page/simple/item-types/shared/item.component.spec.ts +++ b/src/app/item-page/simple/item-types/shared/item.component.spec.ts @@ -44,6 +44,8 @@ import { BrowseDefinitionDataService } from '../../../../core/browse/browse-defi import { BrowseDefinitionDataServiceStub } from '../../../../shared/testing/browse-definition-data-service.stub'; +import { BrowseService } from '../../../../core/browse/browse.service'; +import { BrowseServiceStub } from '../../../../shared/testing/browse-service.stub'; import { buildPaginatedList } from '../../../../core/data/paginated-list.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; @@ -133,6 +135,7 @@ export function getItemPageFieldsTest(mockItem: Item, component) { { provide: AuthorizationDataService, useValue: authorizationService }, { provide: ResearcherProfileDataService, useValue: {} }, { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: BrowseService, useValue: BrowseServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts index 4b7da40abe..3d72ec2360 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts @@ -41,6 +41,8 @@ import { BrowseDefinitionDataService } from '../../../../core/browse/browse-defi import { BrowseDefinitionDataServiceStub } from '../../../../shared/testing/browse-definition-data-service.stub'; +import { BrowseService } from '../../../../core/browse/browse.service'; +import { BrowseServiceStub } from '../../../../shared/testing/browse-service.stub'; const noMetadata = new MetadataMap(); @@ -96,6 +98,7 @@ describe('UntypedItemComponent', () => { { provide: ItemVersionsSharedService, useValue: {} }, { provide: RouteService, useValue: mockRouteService }, { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: BrowseService, useValue: BrowseServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(UntypedItemComponent, { diff --git a/src/app/shared/testing/browse-service.stub.ts b/src/app/shared/testing/browse-service.stub.ts new file mode 100644 index 0000000000..85ca717cb0 --- /dev/null +++ b/src/app/shared/testing/browse-service.stub.ts @@ -0,0 +1,91 @@ +import { + EMPTY, + Observable, +} from 'rxjs'; + +import { + buildPaginatedList, + PaginatedList, +} from '../../core/data/paginated-list.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { BrowseDefinition } from '../../core/shared/browse-definition.model'; +import { FlatBrowseDefinition } from '../../core/shared/flat-browse-definition.model'; +import { HierarchicalBrowseDefinition } from '../../core/shared/hierarchical-browse-definition.model'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { ValueListBrowseDefinition } from '../../core/shared/value-list-browse-definition.model'; +import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; + +const mockData = [ + Object.assign(new FlatBrowseDefinition(), { + 'id': 'dateissued', + 'browseType': 'flatBrowse', + 'dataType': 'date', + 'sortOptions': EMPTY, + 'order': 'ASC', + 'type': 'browse', + 'metadataKeys': [ + 'dc.date.issued', + ], + '_links': EMPTY, + }), + + Object.assign(new ValueListBrowseDefinition(), { + 'id': 'author', + 'browseType': 'valueList', + 'dataType': 'text', + 'sortOptions': EMPTY, + 'order': 'ASC', + 'type': 'browse', + 'metadataKeys': [ + 'dc.contributor.*', + 'dc.creator', + ], + '_links': EMPTY, + }), + + Object.assign(new FlatBrowseDefinition(), { + 'id': 'title', + 'browseType': 'flatBrowse', + 'dataType': 'title', + 'sortOptions': EMPTY, + 'order': 'ASC', + 'type': 'browse', + 'metadataKeys': [ + 'dc.title', + ], + '_links': EMPTY, + }), + + Object.assign(new ValueListBrowseDefinition(), { + 'id': 'subject', + 'browseType': 'valueList', + 'dataType': 'text', + 'sortOptions': EMPTY, + 'order': 'ASC', + 'type': 'browse', + 'metadataKeys': [ + 'dc.subject.*', + ], + '_links': EMPTY, + }), + + Object.assign(new HierarchicalBrowseDefinition(), { + 'id': 'srsc', + 'browseType': 'hierarchicalBrowse', + 'facetType': 'subject', + 'vocabulary': 'srsc', + 'type': 'browse', + 'metadataKeys': [ + 'dc.subject', + ], + '_links': EMPTY, + }), +]; +export const BrowseServiceStub: any = { + /** + * Get all browse definitions. + */ + getBrowseDefinitions(): Observable>> { + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockData)); + }, +}; From 05d5a0816d121c31c73edb606edb4f8c5aed9b83 Mon Sep 17 00:00:00 2001 From: VictorHugoDuranS Date: Fri, 10 Jan 2025 15:17:08 -0600 Subject: [PATCH 113/123] Change - Metadata field selector, add infinite scroll for data paginated (#3096) * Change - Metadata field selector add infinite scroll for data paginated * Update change on new BRANCH * Fix - LINT ERRORS * Fix - LINT ERRORS --- .../metadata-field-selector.component.html | 29 +++- .../metadata-field-selector.component.scss | 5 + .../metadata-field-selector.component.spec.ts | 9 +- .../metadata-field-selector.component.ts | 127 +++++++++++++----- 4 files changed, 128 insertions(+), 42 deletions(-) diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html index 3f5af76051..a82f31b953 100644 --- a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html @@ -6,13 +6,30 @@ [formControl]="input" (focusin)="query$.next(mdField)" (dsClickOutside)="query$.next(null)" - (click)="$event.stopPropagation();" /> + (click)="$event.stopPropagation();" + (keyup)="this.selectedValueLoading = false" + />
{{ dsoType + '.edit.metadata.metadatafield.invalid' | translate }}
- - -
-

{{'profile.groups.head' | translate}}

-
    -
  • {{ dsoNameService.getName(group) }}
  • -
-
+ + + + + +
+

{{ 'profile.groups.head' | translate }}

+
    +
  • {{ dsoNameService.getName(group) }}
  • +
+
+
+
+
+ + + +
diff --git a/src/app/profile-page/profile-page.component.ts b/src/app/profile-page/profile-page.component.ts index 6e693461d2..f8cb30a447 100644 --- a/src/app/profile-page/profile-page.component.ts +++ b/src/app/profile-page/profile-page.component.ts @@ -9,7 +9,12 @@ import { RemoteData } from '../core/data/remote-data'; import { PaginatedList } from '../core/data/paginated-list.model'; import { filter, switchMap, tap } from 'rxjs/operators'; import { EPersonDataService } from '../core/eperson/eperson-data.service'; -import { getAllSucceededRemoteData, getFirstCompletedRemoteData, getRemoteDataPayload } from '../core/shared/operators'; +import { + getAllCompletedRemoteData, + getAllSucceededRemoteData, + getFirstCompletedRemoteData, + getRemoteDataPayload +} from '../core/shared/operators'; import { hasValue, isNotEmpty } from '../shared/empty.util'; import { followLink } from '../shared/utils/follow-link-config.model'; import { AuthService } from '../core/auth/auth.service'; @@ -19,6 +24,8 @@ import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { ConfigurationProperty } from '../core/shared/configuration-property.model'; import { DSONameService } from '../core/breadcrumbs/dso-name.service'; +import { PaginationService } from '../core/pagination/pagination.service'; +import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; @Component({ selector: 'ds-profile-page', @@ -79,6 +86,15 @@ export class ProfilePageComponent implements OnInit { private currentUser: EPerson; canChangePassword$: Observable; + /** + * Default configuration for group pagination + **/ + optionsGroupsPagination = Object.assign(new PaginationComponentOptions(),{ + id: 'page_groups', + currentPage: 1, + pageSize: 20, + }); + isResearcherProfileEnabled$: BehaviorSubject = new BehaviorSubject(false); constructor(private authService: AuthService, @@ -88,6 +104,7 @@ export class ProfilePageComponent implements OnInit { private authorizationService: AuthorizationDataService, private configurationService: ConfigurationDataService, public dsoNameService: DSONameService, + private paginationService: PaginationService, ) { } @@ -99,7 +116,18 @@ export class ProfilePageComponent implements OnInit { getRemoteDataPayload(), tap((user: EPerson) => this.currentUser = user) ); - this.groupsRD$ = this.user$.pipe(switchMap((user: EPerson) => user.groups)); + this.groupsRD$ = this.paginationService.getCurrentPagination(this.optionsGroupsPagination.id, this.optionsGroupsPagination).pipe( + switchMap((pageOptions: PaginationComponentOptions) => { + return this.epersonService.findById(this.currentUser.id, true, true, followLink('groups',{ + findListOptions: { + elementsPerPage: pageOptions.pageSize, + currentPage: pageOptions.currentPage, + } })); + }), + getAllCompletedRemoteData(), + getRemoteDataPayload(), + switchMap((user: EPerson) => user?.groups), + ); this.canChangePassword$ = this.user$.pipe(switchMap((user: EPerson) => this.authorizationService.isAuthorized(FeatureID.CanChangePassword, user._links.self.href))); this.specialGroupsRD$ = this.authService.getSpecialGroupsFromAuthStatus(); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index a56f1cd4f2..f53670c4e3 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1622,6 +1622,8 @@ "error.recent-submissions": "Error fetching recent submissions", + "error.profile-groups": "Error retrieving profile groups", + "error.search-results": "Error fetching search results", "error.invalid-search-query": "Search query is not valid. Please check Solr query syntax best practices for further information about this error.", From a57fd1a069a32375b4e5177969c076e93537657a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 02:39:03 +0000 Subject: [PATCH 115/123] Bump sass from 1.80.6 to 1.83.1 in the sass group across 1 directory Bumps the sass group with 1 update in the / directory: [sass](https://github.com/sass/dart-sass). Updates `sass` from 1.80.6 to 1.83.1 - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.80.6...1.83.1) --- updated-dependencies: - dependency-name: sass dependency-type: direct:development update-type: version-update:semver-minor dependency-group: sass ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 15b2bb9779..8929f9ddb1 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "react-copy-to-clipboard": "^5.1.0", "react-dom": "^16.14.0", "rimraf": "^3.0.2", - "sass": "~1.80.6", + "sass": "~1.83.1", "sass-loader": "^12.6.0", "sass-resources-loader": "^2.2.5", "ts-node": "^8.10.2", diff --git a/yarn.lock b/yarn.lock index 63a2fbbba5..89561983f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6620,6 +6620,11 @@ immutable@^4.0.0: resolved "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz" integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== +immutable@^5.0.2: + version "5.0.3" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1" + integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" @@ -10235,13 +10240,13 @@ sass@1.58.1: immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" -sass@^1.25.0, sass@~1.80.6: - version "1.80.6" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.80.6.tgz#5d0aa55763984effe41e40019c9571ab73e6851f" - integrity sha512-ccZgdHNiBF1NHBsWvacvT5rju3y1d/Eu+8Ex6c21nHp2lZGLBEtuwc415QfiI1PJa1TpCo3iXwwSRjRpn2Ckjg== +sass@^1.25.0, sass@~1.83.1: + version "1.83.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.83.1.tgz#dee1ab94b47a6f9993d3195d36f556bcbda64846" + integrity sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA== dependencies: chokidar "^4.0.0" - immutable "^4.0.0" + immutable "^5.0.2" source-map-js ">=0.6.2 <2.0.0" optionalDependencies: "@parcel/watcher" "^2.4.1" From 3c16537fe46a0d9eeeec5eb7fe8438aeb3399442 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 02:39:41 +0000 Subject: [PATCH 116/123] Bump express from 4.21.1 to 4.21.2 Bumps [express](https://github.com/expressjs/express) from 4.21.1 to 4.21.2. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md) - [Commits](https://github.com/expressjs/express/compare/4.21.1...4.21.2) --- updated-dependencies: - dependency-name: express dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 34 +++++++++++++++++----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 15b2bb9779..483358b029 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "date-fns-tz": "^1.3.7", "deepmerge": "^4.3.1", "ejs": "^3.1.10", - "express": "^4.21.1", + "express": "^4.21.2", "express-rate-limit": "^5.1.3", "fast-json-patch": "^3.1.1", "filesize": "^6.1.0", diff --git a/yarn.lock b/yarn.lock index 63a2fbbba5..e9c821cb60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5617,10 +5617,10 @@ express-static-gzip@^2.2.0: parseurl "^1.3.3" serve-static "^1.16.2" -express@^4.17.3, express@^4.18.2, express@^4.21.1: - version "4.21.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" - integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== +express@^4.17.3, express@^4.18.2, express@^4.21.2: + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== dependencies: accepts "~1.3.8" array-flatten "1.1.1" @@ -5641,7 +5641,7 @@ express@^4.17.3, express@^4.18.2, express@^4.21.1: methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.10" + path-to-regexp "0.1.12" proxy-addr "~2.0.7" qs "6.13.0" range-parser "~1.2.1" @@ -9022,10 +9022,10 @@ path-scurry@^1.6.1: lru-cache "^9.0.0" minipass "^5.0.0" -path-to-regexp@0.1.10: - version "0.1.10" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" - integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== path-type@^4.0.0: version "4.0.0" @@ -10807,20 +10807,20 @@ statuses@2.0.1: resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -"statuses@>= 1.4.0 < 2", statuses@~1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz" - integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== +"statuses@>= 1.4.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== statuses@~1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz" integrity sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg== -statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +statuses@~1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz" + integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== stop-iteration-iterator@^1.0.0: version "1.0.0" From ecceaf9ee1bac2b22a9d50c3a4daf2a9b893cddc Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 15 Jan 2025 10:30:46 -0600 Subject: [PATCH 117/123] Ensure Admin sidebar link is visible before clicking links --- cypress/e2e/admin-add-new-modals.cy.ts | 3 +++ cypress/e2e/admin-edit-modals.cy.ts | 3 +++ cypress/e2e/admin-export-modals.cy.ts | 2 ++ 3 files changed, 8 insertions(+) diff --git a/cypress/e2e/admin-add-new-modals.cy.ts b/cypress/e2e/admin-add-new-modals.cy.ts index 565ae154f1..0179ca7a7c 100644 --- a/cypress/e2e/admin-add-new-modals.cy.ts +++ b/cypress/e2e/admin-add-new-modals.cy.ts @@ -12,6 +12,7 @@ describe('Admin Add New Modals', () => { cy.get('#sidebar-collapse-toggle').click(); // Click on entry of menu + cy.get('#admin-menu-section-new-title').should('be.visible'); cy.get('#admin-menu-section-new-title').click(); cy.get('a[data-test="menu.section.new_community"]').click(); @@ -25,6 +26,7 @@ describe('Admin Add New Modals', () => { cy.get('#sidebar-collapse-toggle').click(); // Click on entry of menu + cy.get('#admin-menu-section-new-title').should('be.visible'); cy.get('#admin-menu-section-new-title').click(); cy.get('a[data-test="menu.section.new_collection"]').click(); @@ -38,6 +40,7 @@ describe('Admin Add New Modals', () => { cy.get('#sidebar-collapse-toggle').click(); // Click on entry of menu + cy.get('#admin-menu-section-new-title').should('be.visible'); cy.get('#admin-menu-section-new-title').click(); cy.get('a[data-test="menu.section.new_item"]').click(); diff --git a/cypress/e2e/admin-edit-modals.cy.ts b/cypress/e2e/admin-edit-modals.cy.ts index e96d6ce898..79190bfce9 100644 --- a/cypress/e2e/admin-edit-modals.cy.ts +++ b/cypress/e2e/admin-edit-modals.cy.ts @@ -12,6 +12,7 @@ describe('Admin Edit Modals', () => { cy.get('#sidebar-collapse-toggle').click(); // Click on entry of menu + cy.get('#admin-menu-section-edit-title').should('be.visible'); cy.get('#admin-menu-section-edit-title').click(); cy.get('a[data-test="menu.section.edit_community"]').click(); @@ -25,6 +26,7 @@ describe('Admin Edit Modals', () => { cy.get('#sidebar-collapse-toggle').click(); // Click on entry of menu + cy.get('#admin-menu-section-edit-title').should('be.visible'); cy.get('#admin-menu-section-edit-title').click(); cy.get('a[data-test="menu.section.edit_collection"]').click(); @@ -38,6 +40,7 @@ describe('Admin Edit Modals', () => { cy.get('#sidebar-collapse-toggle').click(); // Click on entry of menu + cy.get('#admin-menu-section-edit-title').should('be.visible'); cy.get('#admin-menu-section-edit-title').click(); cy.get('a[data-test="menu.section.edit_item"]').click(); diff --git a/cypress/e2e/admin-export-modals.cy.ts b/cypress/e2e/admin-export-modals.cy.ts index c5b0b0908f..55d952cad8 100644 --- a/cypress/e2e/admin-export-modals.cy.ts +++ b/cypress/e2e/admin-export-modals.cy.ts @@ -12,6 +12,7 @@ describe('Admin Export Modals', () => { cy.get('#sidebar-collapse-toggle').click(); // Click on entry of menu + cy.get('#admin-menu-section-export-title').should('be.visible'); cy.get('#admin-menu-section-export-title').click(); cy.get('a[data-test="menu.section.export_metadata"]').click(); @@ -25,6 +26,7 @@ describe('Admin Export Modals', () => { cy.get('#sidebar-collapse-toggle').click(); // Click on entry of menu + cy.get('#admin-menu-section-export-title').should('be.visible'); cy.get('#admin-menu-section-export-title').click(); cy.get('a[data-test="menu.section.export_batch"]').click(); From 9e1ce916f655cccc82c51fdf71487cafa615697c Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 15 Jan 2025 10:31:10 -0600 Subject: [PATCH 118/123] Ensure Item Edit page tab is visible before & after clicking it. --- cypress/e2e/item-edit.cy.ts | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/cypress/e2e/item-edit.cy.ts b/cypress/e2e/item-edit.cy.ts index 604aab9d04..ad5d8ea093 100644 --- a/cypress/e2e/item-edit.cy.ts +++ b/cypress/e2e/item-edit.cy.ts @@ -13,9 +13,11 @@ beforeEach(() => { describe('Edit Item > Edit Metadata tab', () => { it('should pass accessibility tests', () => { + cy.get('a[data-test="metadata"]').should('be.visible'); cy.get('a[data-test="metadata"]').click(); - // Our selected tab should be active + // Our selected tab should be both visible & active + cy.get('a[data-test="metadata"]').should('be.visible'); cy.get('a[data-test="metadata"]').should('have.class', 'active'); // tag must be loaded @@ -34,9 +36,11 @@ describe('Edit Item > Edit Metadata tab', () => { describe('Edit Item > Status tab', () => { it('should pass accessibility tests', () => { + cy.get('a[data-test="status"]').should('be.visible'); cy.get('a[data-test="status"]').click(); - // Our selected tab should be active + // Our selected tab should be both visible & active + cy.get('a[data-test="status"]').should('be.visible'); cy.get('a[data-test="status"]').should('have.class', 'active'); // tag must be loaded @@ -50,9 +54,11 @@ describe('Edit Item > Status tab', () => { describe('Edit Item > Bitstreams tab', () => { it('should pass accessibility tests', () => { + cy.get('a[data-test="bitstreams"]').should('be.visible'); cy.get('a[data-test="bitstreams"]').click(); - // Our selected tab should be active + // Our selected tab should be both visible & active + cy.get('a[data-test="bitstreams"]').should('be.visible'); cy.get('a[data-test="bitstreams"]').should('have.class', 'active'); // tag must be loaded @@ -77,9 +83,11 @@ describe('Edit Item > Bitstreams tab', () => { describe('Edit Item > Curate tab', () => { it('should pass accessibility tests', () => { + cy.get('a[data-test="curate"]').should('be.visible'); cy.get('a[data-test="curate"]').click(); - // Our selected tab should be active + // Our selected tab should be both visible & active + cy.get('a[data-test="curate"]').should('be.visible'); cy.get('a[data-test="curate"]').should('have.class', 'active'); // tag must be loaded @@ -93,9 +101,11 @@ describe('Edit Item > Curate tab', () => { describe('Edit Item > Relationships tab', () => { it('should pass accessibility tests', () => { + cy.get('a[data-test="relationships"]').should('be.visible'); cy.get('a[data-test="relationships"]').click(); - // Our selected tab should be active + // Our selected tab should be both visible & active + cy.get('a[data-test="relationships"]').should('be.visible'); cy.get('a[data-test="relationships"]').should('have.class', 'active'); // tag must be loaded @@ -109,9 +119,11 @@ describe('Edit Item > Relationships tab', () => { describe('Edit Item > Version History tab', () => { it('should pass accessibility tests', () => { + cy.get('a[data-test="versionhistory"]').should('be.visible'); cy.get('a[data-test="versionhistory"]').click(); - // Our selected tab should be active + // Our selected tab should be both visible & active + cy.get('a[data-test="versionhistory"]').should('be.visible'); cy.get('a[data-test="versionhistory"]').should('have.class', 'active'); // tag must be loaded @@ -125,9 +137,11 @@ describe('Edit Item > Version History tab', () => { describe('Edit Item > Access Control tab', () => { it('should pass accessibility tests', () => { + cy.get('a[data-test="access-control"]').should('be.visible'); cy.get('a[data-test="access-control"]').click(); - // Our selected tab should be active + // Our selected tab should be both visible & active + cy.get('a[data-test="access-control"]').should('be.visible'); cy.get('a[data-test="access-control"]').should('have.class', 'active'); // tag must be loaded @@ -141,9 +155,11 @@ describe('Edit Item > Access Control tab', () => { describe('Edit Item > Collection Mapper tab', () => { it('should pass accessibility tests', () => { + cy.get('a[data-test="mapper"]').should('be.visible'); cy.get('a[data-test="mapper"]').click(); - // Our selected tab should be active + // Our selected tab should be both visible & active + cy.get('a[data-test="mapper"]').should('be.visible'); cy.get('a[data-test="mapper"]').should('have.class', 'active'); // tag must be loaded From fd166fe79a24ddf15d04961b0cb1c4150a3faeec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 19:51:07 +0000 Subject: [PATCH 119/123] Bump eslint-plugin-jsonc in the eslint group across 1 directory Bumps the eslint group with 1 update in the / directory: [eslint-plugin-jsonc](https://github.com/ota-meshi/eslint-plugin-jsonc). Updates `eslint-plugin-jsonc` from 2.16.0 to 2.18.2 - [Release notes](https://github.com/ota-meshi/eslint-plugin-jsonc/releases) - [Changelog](https://github.com/ota-meshi/eslint-plugin-jsonc/blob/master/CHANGELOG.md) - [Commits](https://github.com/ota-meshi/eslint-plugin-jsonc/compare/v2.16.0...v2.18.2) --- updated-dependencies: - dependency-name: eslint-plugin-jsonc dependency-type: direct:development update-type: version-update:semver-minor dependency-group: eslint ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 33 ++++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 3128aeb917..1b326f1221 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ "eslint-plugin-deprecation": "^1.5.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsdoc": "^45.0.0", - "eslint-plugin-jsonc": "^2.16.0", + "eslint-plugin-jsonc": "^2.18.2", "eslint-plugin-lodash": "^7.4.0", "eslint-plugin-unused-imports": "^2.0.0", "express-static-gzip": "^2.2.0", diff --git a/yarn.lock b/yarn.lock index 1ad8fc2f35..4b6faaa9ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5299,10 +5299,10 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-compat-utils@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz#7fc92b776d185a70c4070d03fd26fde3d59652e4" - integrity sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q== +eslint-compat-utils@^0.6.0: + version "0.6.4" + resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.6.4.tgz#173d305132da755ac3612cccab03e1b2e14235ed" + integrity sha512-/u+GQt8NMfXO8w17QendT4gvO5acfxQsAKirAt0LVxDnr2N8YLCVbregaNc/Yhp7NM128DwCaRvr8PLDfeNkQw== dependencies: semver "^7.5.4" @@ -5315,6 +5315,13 @@ eslint-import-resolver-node@^0.3.9: is-core-module "^2.13.0" resolve "^1.22.4" +eslint-json-compat-utils@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/eslint-json-compat-utils/-/eslint-json-compat-utils-0.2.1.tgz#32931d42c723da383712f25177a2c57b9ef5f079" + integrity sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg== + dependencies: + esquery "^1.6.0" + eslint-module-utils@^2.12.0: version "2.12.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" @@ -5370,13 +5377,14 @@ eslint-plugin-jsdoc@^45.0.0: semver "^7.5.1" spdx-expression-parse "^3.0.1" -eslint-plugin-jsonc@^2.16.0: - version "2.16.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.16.0.tgz#e90eca15aa2e172f5aca52a77fc8c819f52862d7" - integrity sha512-Af/ZL5mgfb8FFNleH6KlO4/VdmDuTqmM+SPnWcdoWywTetv7kq+vQe99UyQb9XO3b0OWLVuTH7H0d/PXYCMdSg== +eslint-plugin-jsonc@^2.18.2: + version "2.18.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.18.2.tgz#893520feb35d343e7438e2fb57fad179ef3ff6aa" + integrity sha512-SDhJiSsWt3nItl/UuIv+ti4g3m4gpGkmnUJS9UWR3TrpyNsIcnJoBRD7Kof6cM4Rk3L0wrmY5Tm3z7ZPjR2uGg== dependencies: "@eslint-community/eslint-utils" "^4.2.0" - eslint-compat-utils "^0.5.0" + eslint-compat-utils "^0.6.0" + eslint-json-compat-utils "^0.2.1" espree "^9.6.1" graphemer "^1.4.0" jsonc-eslint-parser "^2.0.4" @@ -5520,6 +5528,13 @@ esquery@^1.4.2, esquery@^1.5.0: dependencies: estraverse "^5.1.0" +esquery@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" From 31c9b854c628863af65a9a8b7ef46d4e59c87af1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 22:54:06 +0000 Subject: [PATCH 120/123] Bump the testing group across 1 directory with 2 updates Bumps the testing group with 2 updates in the / directory: [cypress](https://github.com/cypress-io/cypress) and [ng-mocks](https://github.com/help-me-mom/ng-mocks). Updates `cypress` from 13.15.1 to 13.17.0 - [Release notes](https://github.com/cypress-io/cypress/releases) - [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md) - [Commits](https://github.com/cypress-io/cypress/compare/v13.15.1...v13.17.0) Updates `ng-mocks` from 14.13.1 to 14.13.2 - [Release notes](https://github.com/help-me-mom/ng-mocks/releases) - [Changelog](https://github.com/help-me-mom/ng-mocks/blob/master/CHANGELOG.md) - [Commits](https://github.com/help-me-mom/ng-mocks/compare/v14.13.1...v14.13.2) --- updated-dependencies: - dependency-name: cypress dependency-type: direct:development update-type: version-update:semver-minor dependency-group: testing - dependency-name: ng-mocks dependency-type: direct:development update-type: version-update:semver-patch dependency-group: testing ... Signed-off-by: dependabot[bot] --- package.json | 4 +-- yarn.lock | 75 ++++++++++++++++++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index f7c2431009..aa09e1bb81 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "copy-webpack-plugin": "^6.4.1", "cross-env": "^7.0.3", "csstype": "^3.1.3", - "cypress": "^13.15.1", + "cypress": "^13.17.0", "cypress-axe": "^1.5.0", "deep-freeze": "0.0.1", "eslint": "^8.39.0", @@ -174,7 +174,7 @@ "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", "karma-mocha-reporter": "2.2.5", - "ng-mocks": "^14.13.1", + "ng-mocks": "^14.13.2", "ngx-mask": "^13.1.7", "nodemon": "^2.0.22", "postcss": "^8.4", diff --git a/yarn.lock b/yarn.lock index 3548e550bd..d742d70c13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1534,10 +1534,10 @@ resolved "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz" integrity sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw== -"@cypress/request@^3.0.4": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.5.tgz#d893a6e68ce2636c085fcd8d7283c3186499ba63" - integrity sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA== +"@cypress/request@^3.0.6": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.7.tgz#6a74a4da98d9e5ae9121d6e2d9c14780c9b5cf1a" + integrity sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -1552,9 +1552,9 @@ json-stringify-safe "~5.0.1" mime-types "~2.1.19" performance-now "^2.1.0" - qs "6.13.0" + qs "6.13.1" safe-buffer "^5.1.2" - tough-cookie "^4.1.3" + tough-cookie "^5.0.0" tunnel-agent "^0.6.0" uuid "^8.3.2" @@ -3940,10 +3940,10 @@ chrome-trace-event@^1.0.2: resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== -ci-info@^3.2.0: - version "3.8.0" - resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz" - integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== +ci-info@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.1.0.tgz#92319d2fa29d2620180ea5afed31f589bc98cf83" + integrity sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A== classnames@^2.2.6: version "2.3.2" @@ -4465,12 +4465,12 @@ cypress-axe@^1.5.0: resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-1.5.0.tgz#95082734583da77b51ce9b7784e14a442016c7a1" integrity sha512-Hy/owCjfj+25KMsecvDgo4fC/781ccL+e8p+UUYoadGVM2ogZF9XIKbiM6KI8Y3cEaSreymdD6ZzccbI2bY0lQ== -cypress@^13.15.1: - version "13.15.1" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.15.1.tgz#d85074e07cc576eb30068617d529719ef6093b69" - integrity sha512-DwUFiKXo4lef9kA0M4iEhixFqoqp2hw8igr0lTqafRb9qtU3X0XGxKbkSYsUFdkrAkphc7MPDxoNPhk5pj9PVg== +cypress@^13.17.0: + version "13.17.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.17.0.tgz#34c3d68080c4497eace0f353bd1629587a5f600d" + integrity sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA== dependencies: - "@cypress/request" "^3.0.4" + "@cypress/request" "^3.0.6" "@cypress/xvfb" "^1.2.4" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" @@ -4481,6 +4481,7 @@ cypress@^13.15.1: cachedir "^2.3.0" chalk "^4.1.0" check-more-types "^2.24.0" + ci-info "^4.0.0" cli-cursor "^3.1.0" cli-table3 "~0.6.1" commander "^6.2.1" @@ -4495,7 +4496,6 @@ cypress@^13.15.1: figures "^3.2.0" fs-extra "^9.1.0" getos "^3.2.1" - is-ci "^3.0.1" is-installed-globally "~0.4.0" lazy-ass "^1.6.0" listr2 "^3.8.3" @@ -6832,13 +6832,6 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-ci@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" - integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== - dependencies: - ci-info "^3.2.0" - is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.8.1: version "2.15.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" @@ -8382,10 +8375,10 @@ neo-async@^2.6.2: resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -ng-mocks@^14.13.1: - version "14.13.1" - resolved "https://registry.yarnpkg.com/ng-mocks/-/ng-mocks-14.13.1.tgz#9fef8cc902a0d9d3250550d83514f9c65d4cd366" - integrity sha512-eyfnjXeC108SqVD09i/cBwCpKkK0JjBoAg8jp7oQS2HS081K3WJTttFpgLGeLDYKmZsZ6nYpI+HHNQ3OksaJ7A== +ng-mocks@^14.13.2: + version "14.13.2" + resolved "https://registry.yarnpkg.com/ng-mocks/-/ng-mocks-14.13.2.tgz#ddd675d675eb16dfa85834e28dd42343853a6622" + integrity sha512-ItAB72Pc0uznL1j4TPsFp1wehhitVp7DARkc67aafeIk1FDgwnAZvzJwntMnIp/IWMSbzrEQ6kl3cc5euX1NRA== ng2-file-upload@1.4.0: version "1.4.0" @@ -9564,6 +9557,13 @@ qs@6.13.0: dependencies: side-channel "^1.0.6" +qs@6.13.1: + version "6.13.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.1.tgz#3ce5fc72bd3a8171b85c99b93c65dd20b7d1b16e" + integrity sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg== + dependencies: + side-channel "^1.0.6" + querystringify@^2.1.1: version "2.2.0" resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" @@ -11081,6 +11081,18 @@ tiny-warning@^1.0.2: resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tldts-core@^6.1.72: + version "6.1.72" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.72.tgz#32b38e1843f4adab57d2414a9ec4af9a81826bc0" + integrity sha512-FW3H9aCaGTJ8l8RVCR3EX8GxsxDbQXuwetwwgXA2chYdsX+NY1ytCBl61narjjehWmCw92tc1AxlcY3668CU8g== + +tldts@^6.1.32: + version "6.1.72" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.72.tgz#9b85f47e451e2ff079fab5801b4fa156ecda69f4" + integrity sha512-QNtgIqSUb9o2CoUjX9T5TwaIvUUJFU1+12PJkgt42DFV2yf9J6549yTF2uGloQsJ/JOC8X+gIB81ind97hRiIQ== + dependencies: + tldts-core "^6.1.72" + tmp@0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz" @@ -11129,7 +11141,7 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@^4.0.0, tough-cookie@^4.1.2, tough-cookie@^4.1.3: +tough-cookie@^4.0.0, tough-cookie@^4.1.2: version "4.1.4" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== @@ -11139,6 +11151,13 @@ tough-cookie@^4.0.0, tough-cookie@^4.1.2, tough-cookie@^4.1.3: universalify "^0.2.0" url-parse "^1.5.3" +tough-cookie@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.1.0.tgz#0667b0f2fbb5901fe6f226c3e0b710a9a4292f87" + integrity sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg== + dependencies: + tldts "^6.1.32" + tr46@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz" From 2abdf122b49dcbc2b895641a8a6da672b4227372 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 23:46:54 +0000 Subject: [PATCH 121/123] Bump sanitize-html from 2.13.1 to 2.14.0 Bumps [sanitize-html](https://github.com/apostrophecms/sanitize-html) from 2.13.1 to 2.14.0. - [Changelog](https://github.com/apostrophecms/sanitize-html/blob/main/CHANGELOG.md) - [Commits](https://github.com/apostrophecms/sanitize-html/compare/2.13.1...2.14.0) --- updated-dependencies: - dependency-name: sanitize-html dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f7c2431009..13385f027f 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "pem": "1.14.8", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.0", - "sanitize-html": "^2.13.1", + "sanitize-html": "^2.14.0", "sortablejs": "1.15.6", "uuid": "^8.3.2", "zone.js": "~0.13.3" diff --git a/yarn.lock b/yarn.lock index 3548e550bd..3854d68bbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10208,10 +10208,10 @@ safe-stable-stringify@^2.4.3: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sanitize-html@^2.13.1: - version "2.13.1" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.13.1.tgz#b4639b0a09574ab62b1b353cb99b1b87af742834" - integrity sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg== +sanitize-html@^2.14.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.14.0.tgz#bd2a7b97ee1d86a7f0e0babf3a4468f639c3a429" + integrity sha512-CafX+IUPxZshXqqRaG9ZClSlfPVjSxI0td7n07hk8QO2oO+9JDnlcL8iM8TWeOXOIBFgIOx6zioTzM53AOMn3g== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" From ce5ab70fd42868de51742ec5d88856199dd3cb56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 23:48:52 +0000 Subject: [PATCH 122/123] Bump isbot from 5.1.17 to 5.1.21 Bumps [isbot](https://github.com/omrilotan/isbot) from 5.1.17 to 5.1.21. - [Changelog](https://github.com/omrilotan/isbot/blob/main/CHANGELOG.md) - [Commits](https://github.com/omrilotan/isbot/compare/v5.1.17...v5.1.21) --- updated-dependencies: - dependency-name: isbot dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f7c2431009..fffa4a5f5b 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "filesize": "^6.1.0", "http-proxy-middleware": "^2.0.7", "http-terminator": "^3.2.0", - "isbot": "^5.1.17", + "isbot": "^5.1.21", "js-cookie": "2.2.1", "js-yaml": "^4.1.0", "json5": "^2.2.3", diff --git a/yarn.lock b/yarn.lock index 3548e550bd..6f752b2d39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7087,10 +7087,10 @@ isbinaryfile@^4.0.8: resolved "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz" integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== -isbot@^5.1.17: - version "5.1.17" - resolved "https://registry.yarnpkg.com/isbot/-/isbot-5.1.17.tgz#ad7da5690a61bbb19056a069975c9a73182682a0" - integrity sha512-/wch8pRKZE+aoVhRX/hYPY1C7dMCeeMyhkQLNLNlYAbGQn9bkvMB8fOUXNnk5I0m4vDYbBJ9ciVtkr9zfBJ7qA== +isbot@^5.1.21: + version "5.1.21" + resolved "https://registry.yarnpkg.com/isbot/-/isbot-5.1.21.tgz#4e27526a71c8b9c243b1c7a6445ad4267fa83728" + integrity sha512-0q3naRVpENL0ReKHeNcwn/G7BDynp0DqZUckKyFtM9+hmpnPqgm8+8wbjiVZ0XNhq1wPQV28/Pb8Snh5adeUHA== isexe@^2.0.0: version "2.0.0" From 6035d9e925b66589a49fb2074b5e3d3f9b852586 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Tue, 21 Jan 2025 18:29:10 +0100 Subject: [PATCH 123/123] remove duplicated line (cherry picked from commit 5d04e44ff31593ec7c17635d0053d3b8b615fb07) --- src/styles/_bootstrap_variables_mapping.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/styles/_bootstrap_variables_mapping.scss b/src/styles/_bootstrap_variables_mapping.scss index 5a64be7e2a..944166399e 100644 --- a/src/styles/_bootstrap_variables_mapping.scss +++ b/src/styles/_bootstrap_variables_mapping.scss @@ -220,7 +220,6 @@ --bs-table-dark-hover-color: #{$table-dark-hover-color}; --bs-table-dark-hover-bg: #{$table-dark-hover-bg}; --bs-table-dark-border-color: #{$table-dark-border-color}; - --bs-table-dark-color: #{$table-dark-color}; --bs-table-striped-order: #{$table-striped-order}; --bs-table-caption-color: #{$table-caption-color}; --bs-table-bg-level: #{$table-bg-level};