From a8775f3d8812eac23481e1543556417d860f8944 Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Mon, 27 Apr 2026 15:33:12 +0200 Subject: [PATCH 1/3] Disable delete for in-use licenses, add tooltip Prevent deleting licenses that are attached to bitstreams by disabling the Delete button and showing a tooltip explaining why. Adds UI markup (ngbTooltip wrapper, aria-label, dsBtnDisabled and guarded click), a new isSelectedLicenseInUse() helper and a check in deleteLicense(). Adds unit tests covering disabled/enabled states and click behavior, and provides i18n strings for English and Czech for the disabled-tooltip. --- .../clarin-license-table.component.html | 18 +++++-- .../clarin-license-table.component.spec.ts | 52 ++++++++++++++++++- .../clarin-license-table.component.ts | 9 +++- src/assets/i18n/cs.json5 | 3 ++ src/assets/i18n/en.json5 | 2 + 5 files changed, 79 insertions(+), 5 deletions(-) diff --git a/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.html b/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.html index 729b0f781d5..a74f314a7cc 100644 --- a/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.html +++ b/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.html @@ -76,9 +76,21 @@
- + + +
diff --git a/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.spec.ts b/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.spec.ts index 0cc824bdd8b..3a37d8c3367 100644 --- a/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.spec.ts +++ b/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { ClarinLicenseTableComponent } from './clarin-license-table.component'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { ClarinLicenseDataService } from '../../core/data/clarin/clarin-license-data.service'; @@ -25,7 +26,7 @@ import { mockNonExtendedLicenseLabel, successfulResponse } from '../../shared/testing/clarin-license-mock'; import {GroupDataService} from '../../core/eperson/group-data.service'; -import {createSuccessfulRemoteDataObject$} from '../../shared/remote-data.utils'; +import { createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import {createPaginatedList} from '../../shared/testing/utils.test'; import {LinkHeadService} from '../../core/services/link-head.service'; import {ConfigurationDataService} from '../../core/data/configuration-data.service'; @@ -51,6 +52,7 @@ describe('ClarinLicenseTableComponent', () => { findAll: mockLicenseRD$, create: createdLicenseRD$, put: createdLicenseRD$, + delete: createNoContentRemoteDataObject$(), searchBy: mockLicenseRD$, getLinkPath: observableOf('') }); @@ -181,4 +183,52 @@ describe('ClarinLicenseTableComponent', () => { expect((component as any).clarinLicenseService.searchBy).toHaveBeenCalled(); expect((component as ClarinLicenseTableComponent).licensesRD$).not.toBeNull(); }); + + describe('license delete button', () => { + const getDeleteControls = () => { + const actionsRow = fixture.debugElement.query(By.css('.mt-2')); + const deleteWrapper = actionsRow.query(By.css('.btn-group.pr-1:last-child span')); + const deleteButton = deleteWrapper.query(By.css('button.btn-danger')); + return { deleteWrapper, deleteButton }; + }; + + beforeEach(() => { + (clarinLicenseDataService.delete as jasmine.Spy).calls.reset(); + }); + + it('should disable delete button and expose tooltip when selected license has bitstreams', () => { + component.selectedLicense = Object.assign({}, mockLicense, { bitstreams: 2 }); + fixture.detectChanges(); + + const { deleteWrapper, deleteButton } = getDeleteControls(); + + expect(deleteButton.attributes['aria-disabled']).toBe('true'); + expect(deleteButton.nativeElement.classList.contains('disabled')).toBeTrue(); + expect((deleteWrapper.nativeElement as HTMLElement).getAttribute('tabindex')).toBe('0'); + expect((deleteWrapper.nativeElement as HTMLElement).getAttribute('ng-reflect-ngb-tooltip')).toContain('clarin-license.button.delete-l'); + }); + + it('should not call delete when clicking disabled delete button', () => { + component.selectedLicense = Object.assign({}, mockLicense, { bitstreams: 1 }); + fixture.detectChanges(); + + const { deleteButton } = getDeleteControls(); + deleteButton.nativeElement.click(); + + expect((clarinLicenseDataService.delete as jasmine.Spy)).not.toHaveBeenCalled(); + }); + + it('should enable delete button and call delete when selected license has no bitstreams', () => { + component.selectedLicense = Object.assign({}, mockLicense, { bitstreams: 0 }); + fixture.detectChanges(); + + const { deleteWrapper, deleteButton } = getDeleteControls(); + deleteButton.nativeElement.click(); + + expect(deleteButton.attributes['aria-disabled']).toBe('false'); + expect(deleteButton.nativeElement.classList.contains('disabled')).toBeFalse(); + expect((deleteWrapper.nativeElement as HTMLElement).getAttribute('tabindex')).toBeNull(); + expect((clarinLicenseDataService.delete as jasmine.Spy)).toHaveBeenCalledWith(String(mockLicense.id)); + }); + }); }); diff --git a/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.ts b/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.ts index 143f991aab1..1b43b02498c 100644 --- a/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.ts +++ b/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.ts @@ -274,7 +274,7 @@ export class ClarinLicenseTableComponent implements OnInit { * Delete selected license. If none license is selected do nothing. */ deleteLicense() { - if (isNull(this.selectedLicense?.id)) { + if (isNull(this.selectedLicense?.id) || this.isSelectedLicenseInUse()) { return; } this.clarinLicenseService.delete(String(this.selectedLicense.id)) @@ -287,6 +287,13 @@ export class ClarinLicenseTableComponent implements OnInit { }); } + /** + * Returns whether selected license has attached bitstreams. + */ + isSelectedLicenseInUse(): boolean { + return this.selectedLicense?.bitstreams > 0; + } + /** * Pop up the notification about the request success. Messages are loaded from the `en.json5`. * @param operationResponse current response diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index ac5476fd0db..b287088939a 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -9423,6 +9423,9 @@ // "clarin-license.button.delete-license": "Delete License", "clarin-license.button.delete-license": "Odstranit licenci", + // "clarin-license.button.delete-license.disabled-tooltip": "License \"{{name}}\" cannot be deleted because it is attached to one or more bitstreams.", + "clarin-license.button.delete-license.disabled-tooltip": "Licenci \"{{name}}\" nelze smazat, protože jsou k ní připojeny jeden nebo více bitstreamů.", + // "clarin-license.button.search": "Search", "clarin-license.button.search": "Hledat", diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 472f95c978e..e0ded51faf7 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -6245,6 +6245,8 @@ "clarin-license.button.delete-license": "Delete License", + "clarin-license.button.delete-license.disabled-tooltip": "License \"{{name}}\" cannot be deleted because it is attached to one or more bitstreams.", + "clarin-license.button.search": "Search", "clarin-license.button.search.placeholder": "Search by the license name ...", From ec706748ab736a20d793df97354475b7e4d69264 Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Tue, 28 Apr 2026 14:04:03 +0200 Subject: [PATCH 2/3] Use hasNoValue for license id check Replace isNull with hasNoValue when validating selectedLicense.id in deleteLicense to correctly handle undefined/null values. Also add hasNoValue to the imports from shared/empty.util. --- .../clarin-license-table/clarin-license-table.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.ts b/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.ts index 1b43b02498c..d60411c7ae3 100644 --- a/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.ts +++ b/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.ts @@ -15,7 +15,7 @@ import { DefineLicenseLabelFormComponent } from './modal/define-license-label-fo import { ClarinLicenseConfirmationSerializer } from '../../core/shared/clarin/clarin-license-confirmation-serializer'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; -import { isNull } from '../../shared/empty.util'; +import { hasNoValue, isNull } from '../../shared/empty.util'; import { ClarinLicenseLabel } from '../../core/shared/clarin/clarin-license-label.model'; import { ClarinLicenseLabelDataService } from '../../core/data/clarin/clarin-license-label-data.service'; import { ClarinLicenseLabelExtendedSerializer } from '../../core/shared/clarin/clarin-license-label-extended-serializer'; @@ -274,7 +274,7 @@ export class ClarinLicenseTableComponent implements OnInit { * Delete selected license. If none license is selected do nothing. */ deleteLicense() { - if (isNull(this.selectedLicense?.id) || this.isSelectedLicenseInUse()) { + if (hasNoValue(this.selectedLicense?.id) || this.isSelectedLicenseInUse()) { return; } this.clarinLicenseService.delete(String(this.selectedLicense.id)) From 30718dec6b125d2af414d3f76052c53ad515dfe9 Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Tue, 28 Apr 2026 15:11:32 +0200 Subject: [PATCH 3/3] Use NgbTooltip instance in delete button spec Update clarin-license-table component spec to import NgbTooltip and retrieve the tooltip instance from the delete wrapper's injector. Replace the previous assertion that inspected the DOM's ng-reflect-ngb-tooltip attribute with an assertion on deleteTooltip.ngbTooltip to verify the expected translation key. This makes the test more robust by avoiding reliance on Angular's rendered reflection attribute. --- .../clarin-license-table.component.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.spec.ts b/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.spec.ts index 3a37d8c3367..1da35adf950 100644 --- a/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.spec.ts +++ b/src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.spec.ts @@ -15,7 +15,7 @@ import { PaginationServiceStub } from '../../shared/testing/pagination-service.s import { NotificationsService } from '../../shared/notifications/notifications.service'; import { defaultPagination } from '../clarin-license-table-pagination'; import { ClarinLicenseLabelDataService } from '../../core/data/clarin/clarin-license-label-data.service'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { NgbActiveModal, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { @@ -201,11 +201,12 @@ describe('ClarinLicenseTableComponent', () => { fixture.detectChanges(); const { deleteWrapper, deleteButton } = getDeleteControls(); + const deleteTooltip = deleteWrapper.injector.get(NgbTooltip); expect(deleteButton.attributes['aria-disabled']).toBe('true'); expect(deleteButton.nativeElement.classList.contains('disabled')).toBeTrue(); expect((deleteWrapper.nativeElement as HTMLElement).getAttribute('tabindex')).toBe('0'); - expect((deleteWrapper.nativeElement as HTMLElement).getAttribute('ng-reflect-ngb-tooltip')).toContain('clarin-license.button.delete-l'); + expect(deleteTooltip.ngbTooltip as string).toContain('clarin-license.button.delete-l'); }); it('should not call delete when clicking disabled delete button', () => {