Skip to content

Commit ad8bc89

Browse files
amadulhaxxanikosarko
authored andcommitted
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. (cherry picked from commit 42c4fcc)
1 parent 6a279c9 commit ad8bc89

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
@@ -9423,6 +9423,9 @@
94239423
// "clarin-license.button.delete-license": "Delete License",
94249424
"clarin-license.button.delete-license": "Odstranit licenci",
94259425

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

src/assets/i18n/en.json5

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

62466246
"clarin-license.button.delete-license": "Delete License",
62476247

6248+
"clarin-license.button.delete-license.disabled-tooltip": "License \"{{name}}\" cannot be deleted because it is attached to one or more bitstreams.",
6249+
62486250
"clarin-license.button.search": "Search",
62496251

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

0 commit comments

Comments
 (0)