From 73ef1ccbf93941d77c7a5417d51b7fe6ff38dfd8 Mon Sep 17 00:00:00 2001 From: Amad Ul Hassan Date: Mon, 27 Apr 2026 11:44:30 +0200 Subject: [PATCH 1/3] add New version action for archived submission items (ufal/dspace-angular#130) * add New version action for archived submission items add New version action for archived submission items * Use shareReplay to cache version draft observable Import shareReplay and apply .pipe(shareReplay(1)) to the version draft observable in ItemActionsComponent. This shares the subscription and caches the latest emission so multiple subscribers won't trigger duplicate work or requests. * Derive new-version tooltip from disable state Use the disableNewVersion$ observable to compute newVersionTooltip$ (with map) and apply shareReplay to disableNewVersion$, instead of calling getVersioningTooltipMessage unconditionally. This avoids invoking the tooltip service when the button is disabled. Added map import and updated unit test to assert the tooltip is derived from the disabled state and that getVersioningTooltipMessage is not called. (cherry picked from commit 3fb57f17d9e8c05ae632aa302ea374dab115148e) --- .../item/item-actions.component.html | 9 ++ .../item/item-actions.component.spec.ts | 97 ++++++++++++++++++- .../item/item-actions.component.ts | 56 ++++++++++- 3 files changed, 160 insertions(+), 2 deletions(-) diff --git a/src/app/shared/mydspace-actions/item/item-actions.component.html b/src/app/shared/mydspace-actions/item/item-actions.component.html index 0f085c453e1..ff38c3463a7 100644 --- a/src/app/shared/mydspace-actions/item/item-actions.component.html +++ b/src/app/shared/mydspace-actions/item/item-actions.component.html @@ -3,3 +3,12 @@ [routerLink]="[itemPageRoute]"> {{"submission.workflow.generic.view" | translate}} + + diff --git a/src/app/shared/mydspace-actions/item/item-actions.component.spec.ts b/src/app/shared/mydspace-actions/item/item-actions.component.spec.ts index e6abfa76a1c..cab9f36c326 100644 --- a/src/app/shared/mydspace-actions/item/item-actions.component.spec.ts +++ b/src/app/shared/mydspace-actions/item/item-actions.component.spec.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { of as observableOf } from 'rxjs'; @@ -16,6 +17,8 @@ import { RequestService } from '../../../core/data/request.service'; import { getMockSearchService } from '../../mocks/search-service.mock'; import { getMockRequestService } from '../../mocks/request.service.mock'; import { SearchService } from '../../../core/shared/search/search.service'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { DsoVersioningModalService } from '../../dso-page/dso-versioning-modal-service/dso-versioning-modal.service'; let component: ItemActionsComponent; let fixture: ComponentFixture; @@ -24,7 +27,25 @@ let mockObject: Item; const mockDataService = {}; +const authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true), +}); + +const dsoVersioningModalService = jasmine.createSpyObj('dsoVersioningModalService', { + isNewVersionButtonDisabled: observableOf(false), + getVersioningTooltipMessage: observableOf('item.page.version.create'), + openCreateVersionModal: undefined, +}); + mockObject = Object.assign(new Item(), { + _links: { + self: { + href: 'https://rest.test/server/api/core/items/item-id' + }, + version: { + href: 'https://rest.test/server/api/core/versions/1' + } + }, bundles: observableOf({}), metadata: { 'dc.title': [ @@ -76,7 +97,9 @@ describe('ItemActionsComponent', () => { { provide: ItemDataService, useValue: mockDataService }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: SearchService, useValue: searchService }, - { provide: RequestService, useValue: requestServce } + { provide: RequestService, useValue: requestServce }, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: DsoVersioningModalService, useValue: dsoVersioningModalService } ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ItemActionsComponent, { @@ -85,6 +108,14 @@ describe('ItemActionsComponent', () => { })); beforeEach(() => { + authorizationService.isAuthorized.calls.reset(); + authorizationService.isAuthorized.and.returnValue(observableOf(true)); + dsoVersioningModalService.isNewVersionButtonDisabled.calls.reset(); + dsoVersioningModalService.isNewVersionButtonDisabled.and.returnValue(observableOf(false)); + dsoVersioningModalService.getVersioningTooltipMessage.calls.reset(); + dsoVersioningModalService.getVersioningTooltipMessage.and.returnValue(observableOf('item.page.version.create')); + dsoVersioningModalService.openCreateVersionModal.calls.reset(); + fixture = TestBed.createComponent(ItemActionsComponent); component = fixture.componentInstance; component.object = mockObject; @@ -103,4 +134,68 @@ describe('ItemActionsComponent', () => { expect(component.object).toEqual(mockObject); }); + it('should show the New version button when version creation is authorized', () => { + fixture.detectChanges(); + + const newVersionButton = fixture.debugElement.query(By.css('button.btn-outline-primary')); + + expect(newVersionButton).toBeTruthy(); + }); + + it('should hide the New version button when version creation is not authorized', () => { + authorizationService.isAuthorized.and.returnValue(observableOf(false)); + + fixture = TestBed.createComponent(ItemActionsComponent); + component = fixture.componentInstance; + component.object = mockObject; + fixture.detectChanges(); + + const newVersionButton = fixture.debugElement.query(By.css('button.btn-outline-primary')); + + expect(newVersionButton).toBeNull(); + }); + + it('should mark the New version button as disabled when version creation is disabled', () => { + dsoVersioningModalService.isNewVersionButtonDisabled.and.returnValue(observableOf(true)); + + fixture = TestBed.createComponent(ItemActionsComponent); + component = fixture.componentInstance; + component.object = mockObject; + fixture.detectChanges(); + + let isDisabled: boolean; + component.disableNewVersion$.subscribe((value) => { + isDisabled = value; + }); + + expect(isDisabled).toBeTrue(); + }); + + it('should derive tooltip from disable state without calling getVersioningTooltipMessage', () => { + dsoVersioningModalService.isNewVersionButtonDisabled.and.returnValue(observableOf(true)); + + fixture = TestBed.createComponent(ItemActionsComponent); + component = fixture.componentInstance; + component.object = mockObject; + fixture.detectChanges(); + + let tooltipKey: string; + component.newVersionTooltip$.subscribe((value) => { + tooltipKey = value; + }); + + expect(tooltipKey).toBe('item.page.version.hasDraft'); + expect(dsoVersioningModalService.getVersioningTooltipMessage).not.toHaveBeenCalled(); + }); + + it('should open the create version modal when the New version button is clicked', () => { + fixture.detectChanges(); + + const newVersionButton = fixture.debugElement.query(By.css('button.btn-outline-primary')); + + newVersionButton.triggerEventHandler('click'); + + expect(dsoVersioningModalService.openCreateVersionModal).toHaveBeenCalledWith(mockObject); + }); + }); diff --git a/src/app/shared/mydspace-actions/item/item-actions.component.ts b/src/app/shared/mydspace-actions/item/item-actions.component.ts index 4df11d65d91..4f41329bddc 100644 --- a/src/app/shared/mydspace-actions/item/item-actions.component.ts +++ b/src/app/shared/mydspace-actions/item/item-actions.component.ts @@ -1,6 +1,8 @@ import { Component, Injector, Input, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; +import { Observable, of as observableOf } from 'rxjs'; +import { map, shareReplay } from 'rxjs/operators'; import { MyDSpaceActionsComponent } from '../mydspace-actions'; import { ItemDataService } from '../../../core/data/item-data.service'; import { Item } from '../../../core/shared/item.model'; @@ -8,6 +10,10 @@ import { NotificationsService } from '../../notifications/notifications.service' import { RequestService } from '../../../core/data/request.service'; import { SearchService } from '../../../core/shared/search/search.service'; import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { DsoVersioningModalService } from '../../dso-page/dso-versioning-modal-service/dso-versioning-modal.service'; +import { hasValue } from '../../empty.util'; /** * This component represents mydspace actions related to Item object. @@ -30,6 +36,21 @@ export class ItemActionsComponent extends MyDSpaceActionsComponent; + + /** + * Whether the New version button should be disabled. + */ + disableNewVersion$: Observable; + + /** + * Tooltip key for the New version button. + */ + newVersionTooltip$: Observable; + /** * Initialize instance variables * @@ -45,12 +66,15 @@ export class ItemActionsComponent extends MyDSpaceActionsComponent (isDisabled ? 'item.page.version.hasDraft' : 'item.page.version.create')), + ); + } + + /** + * Open the existing Create version modal for the current item. + */ + openCreateVersionModal(): void { + this.dsoVersioningModalService.openCreateVersionModal(this.object); + } + } From b98dfc813e3a56bf960dd87f0a510f676631817d Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Thu, 7 May 2026 10:13:22 +0200 Subject: [PATCH 2/3] Use service to get versioning tooltip Replace the inline mapping of disableNewVersion$ to compute the tooltip with dsoVersioningModalService.getVersioningTooltipMessage(this.object, 'item.page.version.hasDraft', 'item.page.version.create'), centralizing tooltip logic in the service. Also remove the now-unused rxjs 'map' import; shareReplay usage for disableNewVersion$ is preserved. --- .../mydspace-actions/item/item-actions.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/shared/mydspace-actions/item/item-actions.component.ts b/src/app/shared/mydspace-actions/item/item-actions.component.ts index 4f41329bddc..4c232e54c01 100644 --- a/src/app/shared/mydspace-actions/item/item-actions.component.ts +++ b/src/app/shared/mydspace-actions/item/item-actions.component.ts @@ -2,7 +2,7 @@ import { Component, Injector, Input, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Observable, of as observableOf } from 'rxjs'; -import { map, shareReplay } from 'rxjs/operators'; +import { shareReplay } from 'rxjs/operators'; import { MyDSpaceActionsComponent } from '../mydspace-actions'; import { ItemDataService } from '../../../core/data/item-data.service'; import { Item } from '../../../core/shared/item.model'; @@ -112,8 +112,10 @@ export class ItemActionsComponent extends MyDSpaceActionsComponent (isDisabled ? 'item.page.version.hasDraft' : 'item.page.version.create')), + this.newVersionTooltip$ = this.dsoVersioningModalService.getVersioningTooltipMessage( + this.object, + 'item.page.version.hasDraft', + 'item.page.version.create', ); } From c1ccabc06ab22b6adaeacbea8300483a1ebb4fc4 Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Tue, 2 Jun 2026 15:40:10 +0200 Subject: [PATCH 3/3] test: update tooltip key expectation for versioning draft state --- .../mydspace-actions/item/item-actions.component.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/shared/mydspace-actions/item/item-actions.component.spec.ts b/src/app/shared/mydspace-actions/item/item-actions.component.spec.ts index cab9f36c326..ce219907d91 100644 --- a/src/app/shared/mydspace-actions/item/item-actions.component.spec.ts +++ b/src/app/shared/mydspace-actions/item/item-actions.component.spec.ts @@ -171,8 +171,9 @@ describe('ItemActionsComponent', () => { expect(isDisabled).toBeTrue(); }); - it('should derive tooltip from disable state without calling getVersioningTooltipMessage', () => { + it('should use getVersioningTooltipMessage to derive tooltip key', () => { dsoVersioningModalService.isNewVersionButtonDisabled.and.returnValue(observableOf(true)); + dsoVersioningModalService.getVersioningTooltipMessage.and.returnValue(observableOf('item.page.version.hasDraft')); fixture = TestBed.createComponent(ItemActionsComponent); component = fixture.componentInstance; @@ -185,7 +186,8 @@ describe('ItemActionsComponent', () => { }); expect(tooltipKey).toBe('item.page.version.hasDraft'); - expect(dsoVersioningModalService.getVersioningTooltipMessage).not.toHaveBeenCalled(); + expect(dsoVersioningModalService.getVersioningTooltipMessage) + .toHaveBeenCalledWith(mockObject, 'item.page.version.hasDraft', 'item.page.version.create'); }); it('should open the create version modal when the New version button is clicked', () => {