Skip to content

Commit 42c4fcc

Browse files
Disable delete for in-use licenses, add tooltip (#137)
* 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. * 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. * 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.
1 parent cd1c617 commit 42c4fcc

5 files changed

Lines changed: 80 additions & 7 deletions

File tree

src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.html

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,21 @@
7676
</button>
7777
</div>
7878
<div class="btn-group pr-1">
79-
<button type="button" class="btn btn-danger" [disabled]="selectedLicense == null" (click)="deleteLicense()">
80-
{{'clarin-license.button.delete-license' | translate}}
81-
</button>
79+
<span
80+
[ngbTooltip]="isSelectedLicenseInUse() ? ('clarin-license.button.delete-license.disabled-tooltip' | translate:{name: selectedLicense?.name}) : null"
81+
container="body"
82+
placement="top"
83+
[attr.tabindex]="isSelectedLicenseInUse() ? 0 : null">
84+
<button
85+
type="button"
86+
class="btn btn-danger"
87+
[dsBtnDisabled]="selectedLicense == null || isSelectedLicenseInUse()"
88+
(click)="selectedLicense != null && !isSelectedLicenseInUse() && deleteLicense()"
89+
[attr.aria-label]="'clarin-license.button.delete-license' | translate">
90+
<i class="fas fa-trash" aria-hidden="true"></i>
91+
{{ 'clarin-license.button.delete-license' | translate }}
92+
</button>
93+
</span>
8294
</div>
8395
</div>
8496
</div>

src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.spec.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import { PaginationServiceStub } from '../../shared/testing/pagination-service.s
1616
import { NotificationsService } from '../../shared/notifications/notifications.service';
1717
import { defaultPagination } from '../clarin-license-table-pagination';
1818
import { ClarinLicenseLabelDataService } from '../../core/data/clarin/clarin-license-label-data.service';
19-
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
20-
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
19+
import { NgbActiveModal, NgbModal, NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
2120
import { HostWindowService } from '../../shared/host-window.service';
2221
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
2322
import {
@@ -67,6 +66,7 @@ describe('ClarinLicenseTableComponent', () => {
6766
findAll: mockLicenseRD$,
6867
create: createdLicenseRD$,
6968
put: createdLicenseRD$,
69+
delete: createNoContentRemoteDataObject$(),
7070
searchBy: mockLicenseRD$,
7171
getLinkPath: observableOf('')
7272
});
@@ -222,6 +222,55 @@ describe('ClarinLicenseTableComponent', () => {
222222
expect((component as ClarinLicenseTableComponent).licensesRD$).not.toBeNull();
223223
});
224224

225+
describe('license delete button', () => {
226+
const getDeleteControls = () => {
227+
const actionsRow = fixture.debugElement.query(By.css('.mt-2'));
228+
const deleteWrapper = actionsRow.query(By.css('.btn-group.pr-1:last-child span'));
229+
const deleteButton = deleteWrapper.query(By.css('button.btn-danger'));
230+
return { deleteWrapper, deleteButton };
231+
};
232+
233+
beforeEach(() => {
234+
(clarinLicenseDataService.delete as jasmine.Spy).calls.reset();
235+
});
236+
237+
it('should disable delete button and expose tooltip when selected license has bitstreams', () => {
238+
component.selectedLicense = Object.assign({}, mockLicense, { bitstreams: 2 });
239+
fixture.detectChanges();
240+
241+
const { deleteWrapper, deleteButton } = getDeleteControls();
242+
const deleteTooltip = deleteWrapper.injector.get(NgbTooltip);
243+
244+
expect(deleteButton.attributes['aria-disabled']).toBe('true');
245+
expect(deleteButton.nativeElement.classList.contains('disabled')).toBeTrue();
246+
expect((deleteWrapper.nativeElement as HTMLElement).getAttribute('tabindex')).toBe('0');
247+
expect(deleteTooltip.ngbTooltip as string).toContain('clarin-license.button.delete-l');
248+
});
249+
250+
it('should not call delete when clicking disabled delete button', () => {
251+
component.selectedLicense = Object.assign({}, mockLicense, { bitstreams: 1 });
252+
fixture.detectChanges();
253+
254+
const { deleteButton } = getDeleteControls();
255+
deleteButton.nativeElement.click();
256+
257+
expect((clarinLicenseDataService.delete as jasmine.Spy)).not.toHaveBeenCalled();
258+
});
259+
260+
it('should enable delete button and call delete when selected license has no bitstreams', () => {
261+
component.selectedLicense = Object.assign({}, mockLicense, { bitstreams: 0 });
262+
fixture.detectChanges();
263+
264+
const { deleteWrapper, deleteButton } = getDeleteControls();
265+
deleteButton.nativeElement.click();
266+
267+
expect(deleteButton.attributes['aria-disabled']).toBe('false');
268+
expect(deleteButton.nativeElement.classList.contains('disabled')).toBeFalse();
269+
expect((deleteWrapper.nativeElement as HTMLElement).getAttribute('tabindex')).toBeNull();
270+
expect((clarinLicenseDataService.delete as jasmine.Spy)).toHaveBeenCalledWith(String(mockLicense.id));
271+
});
272+
});
273+
225274
describe('label edit flow', () => {
226275
beforeEach(() => {
227276
notificationService.success.calls.reset();

src/app/clarin-licenses/clarin-license-table/clarin-license-table.component.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { DefineLicenseLabelFormComponent } from './modal/define-license-label-fo
1515
import { ClarinLicenseConfirmationSerializer } from '../../core/shared/clarin/clarin-license-confirmation-serializer';
1616
import { NotificationsService } from '../../shared/notifications/notifications.service';
1717
import { TranslateService } from '@ngx-translate/core';
18-
import { isNull } from '../../shared/empty.util';
18+
import { hasNoValue, isNull } from '../../shared/empty.util';
1919
import { ClarinLicenseLabel } from '../../core/shared/clarin/clarin-license-label.model';
2020
import { ClarinLicenseLabelDataService } from '../../core/data/clarin/clarin-license-label-data.service';
2121
import { ClarinLicenseLabelExtendedSerializer } from '../../core/shared/clarin/clarin-license-label-extended-serializer';
@@ -356,7 +356,7 @@ export class ClarinLicenseTableComponent implements OnInit, OnDestroy {
356356
* Delete selected license. If none license is selected do nothing.
357357
*/
358358
deleteLicense() {
359-
if (isNull(this.selectedLicense?.id)) {
359+
if (hasNoValue(this.selectedLicense?.id) || this.isSelectedLicenseInUse()) {
360360
return;
361361
}
362362
this.clarinLicenseService.delete(String(this.selectedLicense.id))
@@ -369,6 +369,13 @@ export class ClarinLicenseTableComponent implements OnInit, OnDestroy {
369369
});
370370
}
371371

372+
/**
373+
* Returns whether selected license has attached bitstreams.
374+
*/
375+
isSelectedLicenseInUse(): boolean {
376+
return this.selectedLicense?.bitstreams > 0;
377+
}
378+
372379
/**
373380
* Open the edit modal for the selected license label, pre-filling its current values.
374381
* On confirm, calls the PUT service and refreshes the label list.

src/assets/i18n/cs.json5

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9432,6 +9432,9 @@
94329432
// "clarin-license.button.delete-license": "Delete License",
94339433
"clarin-license.button.delete-license": "Odstranit licenci",
94349434

9435+
// "clarin-license.button.delete-license.disabled-tooltip": "License \"{{name}}\" cannot be deleted because it is attached to one or more bitstreams.",
9436+
"clarin-license.button.delete-license.disabled-tooltip": "Licenci \"{{name}}\" nelze smazat, protože jsou k ní připojeny jeden nebo více bitstreamů.",
9437+
94359438
// "clarin-license.button.search": "Search",
94369439
"clarin-license.button.search": "Hledat",
94379440

src/assets/i18n/en.json5

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6253,6 +6253,8 @@
62536253

62546254
"clarin-license.button.delete-license": "Delete License",
62556255

6256+
"clarin-license.button.delete-license.disabled-tooltip": "License \"{{name}}\" cannot be deleted because it is attached to one or more bitstreams.",
6257+
62566258
"clarin-license.button.search": "Search",
62576259

62586260
"clarin-license.button.search.placeholder": "Search by the license name ...",

0 commit comments

Comments
 (0)