Skip to content

Commit 66a278c

Browse files
[Port to dtq-dev] license management (#1299)
* 84 license labels management (ufal#134) * Add PUT and DELETE to ClarinLicenseLabel service Replace BaseDataService with IdentifiableDataService and add PutData/DeleteData support for ClarinLicenseLabel. Wire PutDataImpl and DeleteDataImpl in the constructor, expose put(), delete() and deleteByHref() methods, and update imports (remove BaseDataService, add PutData, DeleteData, IdentifiableDataService and NoContent). Enables updating and deleting Clarin License Labels via the data service. * Add license labels table and placeholders Introduce a new License Labels section in the Clarin license table UI with a table, empty-state row, aria labels, and a ds-loading indicator (src/...component.html). Add CSS rule to size the actions column (src/...component.scss). In the component (src/...component.ts) add placeholder Observables (labels$, loading$) and stub methods editLabel and confirmDeleteLabel that log actions; data loading and wiring will be implemented in a follow-up task. * Add label selection and refresh logic Add selectable labels UI and wiring for label management: - Template: add a radio column with aria labels, mark selected row, disable Edit/Delete unless a label is selected, and adjust colspan for empty state. Edit/Delete buttons now call no-arg handlers. - Component: introduce selectedLabel, labels$ and loading$ BehaviorSubjects, refreshLabels() to fetch all labels via clarinLicenseLabelService.findAll (with loading/error handling), selectLabel() and isSelected() helpers, and call refreshLabels() on init and after label creation. Add ngUnsubscribe and ngOnDestroy to clean up subscriptions (takeUntil). Update scan initial state to use a defaultListState and import SortOptions. - Tests: update spec to provide mockLicenseLabelListRD$ and spy findAll. These changes enable selecting a license label for future edit/delete actions and keep the label list in sync with the backend. * Add license labels management and i18n Replace the static license-labels table with a paginated, RemoteData-driven table and translated headers; add loading state and accessible SR-only labels. Wire up edit and delete flows: open DefineLicenseLabelFormComponent for edits (convert file input, call PUT, notify and refresh) and ConfirmationModalComponent for deletes (call DELETE, notify and refresh). Introduce labelsRD$ stream, pagination options, labelsRefresh$ trigger and initializeLabelsPaginationStream to reactively fetch pages. Enhance DefineLicenseLabelFormComponent to support edit mode (prefill form, boolean extended options, aria/id fixes) and update serializer to accept booleans and legacy string values. Update and extend unit tests to cover edit/delete flows and template changes. Add new English and Czech i18n keys for the labels UI and actions. * Use dedicated i18n key for label load errors Introduce a specific i18n key for license label load failures and use it in the labels pagination stream. Added a labelsLoadErrorKey constant in ClarinLicenseTableComponent and replaced usages of the create-error key when handling load failures. Also added the new "clarin.license.label.load.error" translations to en.json5 and cs.json5. * Refresh licenses after label update Call loadAllLicenses() when a label PUT succeeds so dependent license lists are refreshed. The component now only calls refreshLabels() and loadAllLicenses() if the RemoteData indicates success. Updated unit test to spy on loadAllLicenses and assert it is called on modal submit; adjusted method comment accordingly. * Reload licenses after label deletion When a license label is successfully deleted, also reload the full license list to ensure UI reflects the change. Call loadAllLicenses() alongside refreshLabels() in the component, and update the spec to assert loadAllLicenses() is invoked (adding a spy and expectation). * Hide empty table row while loading Prevent the "no licenses" message from appearing while data is being fetched by adding a loading$ | async check to the empty-row *ngIf in clarin-license-table.component.html. The empty message now only shows when not loading and cLicenseLabels.page is empty, avoiding a flash of the empty state during load. * Disable delete for labels in use with tooltip Prevent deleting license labels that are referenced by any license: template changes wrap the delete button in a span that conditionally shows an ngbTooltip and uses dsBtnDisabled with a conditional click handler. Component logic now loads all licenses (paged) on init, builds a Set of in-use label IDs (inUseLabelIds) and exposes isLabelInUse to drive the UI; helper methods fetchAllLicensePages, rebuildLabelUsageSet and loadAllLicensesForUsage were added along with an allLicensesRD$ stream. Unit tests were extended to cover linked vs unlinked label behavior (disabled state, tooltip, and preventing the confirmation modal), and i18n entries for the disabled-tooltip were added for English and Czech. * Show labels empty row only on successful load Update clarin-license-table template to only render the labels empty-state row when the labels request has succeeded ((labelsRD$ | async)?.hasSucceeded). This prevents the empty message from appearing during failed or pending label requests. Add a unit test that simulates a failed labels request to verify the empty-state row is not shown, and import the needed createFailedRemoteDataObject helper for the test. * Optimize license usage loading and tests Add caching and locking around the expensive full license-usage crawl to avoid redundant requests. Introduce licenseUsageLoaded and licenseUsageLoading flags, a new ensureLicenseUsageLoaded(forceReload) helper, and a forceUsageReload parameter to loadAllLicenses so callers can explicitly invalidate the cache. Update create/edit/delete flows to force a usage reload, and set/clear the loading flags in the usage fetch success/error handlers. Add unit tests to verify that the full usage dataset is loaded only once across repeated reloads and that forcing a reload triggers a new fetch. * F1: disable label Delete until usage crawl completes The in-use set (inUseLabelIds) is empty until the async license usage crawl finishes, so during that window an in-use label's Delete button was briefly enabled. Add a usageReady$ stream that only emits true once the crawl succeeds and gate every row's Delete button on it: all Delete buttons stay disabled (with a "checking usage" tooltip) until usage resolves, after which in-use labels stay disabled and unused ones enable. On failure the buttons stay disabled so there is no unsafe window. Frontend-only mitigation; the usage crawl itself is unchanged. * F2: de-wrap double-nested modal in both license forms Both define-license-form and define-license-label-form nested their own .modal > .modal-dialog > .modal-content inside ng-bootstrap's NgbModalWindow, which already provides those wrappers. The duplicate chrome collapsed ngb's .modal-content and pinned the inner position:fixed .modal to the top of the viewport instead of centering. Remove the redundant inner wrappers so ngb owns the modal chrome, delete the now-dead `.modal { display: ... }` scss workarounds in both forms, fix the `modal-boy` -> `modal-body` typo in define-license-form, and open all four form modals with { centered: true } so the dialog is centered. * F3: surface newly created label without manual paging The labels endpoint lists labels in ascending insertion order and ignores the sort param, so a freshly created label received the highest id and landed on the last pagination page, invisible to the admin after Save. After a successful create, jump the labels table to the last page (derived from the current total plus the one just added) so the new row is shown immediately. * F4: allow clearing a label icon on edit and preview the current icon The label edit form could keep or replace an icon but never remove one, and gave no indication of the current icon. Add a "Remove current icon" checkbox and a small preview (reusing secureImageData) shown in edit mode when the label has an icon. When clear is requested and no new file is chosen, the table sends an empty icon array through the existing PUT path to remove it; selecting a new file still takes precedence. * F5: center the delete-confirmation modal and complete cs.json5 label parity Pass { centered: true } when opening ConfirmationModalComponent from confirmDeleteLabel() so the delete-confirmation dialog is vertically centered like the four license/label form dialogs, removing the top-aligned visual inconsistency. Also add the three Czech translations that were missing from cs.json5 (table.delete.checking-tooltip, edit.icon.preview, edit.icon.clear) so the en/cs label key sets are back in parity. * test: expect centered option in delete-confirmation modal spec The F5 change passes { centered: true } to modalService.open; update the assertion to match so the label delete flow spec passes. --------- Co-authored-by: kosarko <ko_ok@centrum.cz> (cherry picked from commit 87cb7a7) * Disable delete for in-use licenses, add tooltip (ufal#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) * Fix license label issues flagged in code review - Change label icon fallback from selectedLabel.icon to undefined to avoid sending a base64 string when the icon is unchanged - Correct 0‑based page parameter to 1‑based in fetchAllLicensePages - Replace hard‑coded aria-label on modal close button with a translatable key for accessibility --------- Co-authored-by: Amad Ul Hassan <hassan@ufal.mff.cuni.cz>
1 parent d154682 commit 66a278c

14 files changed

Lines changed: 1293 additions & 137 deletions

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

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,92 @@
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>
8597
</div>
98+
99+
<div class="labels-section mt-4">
100+
<h3 class="border-bottom pb-2">{{'clarin.license.label.section.title' | translate}}</h3>
101+
102+
<div *ngVar="(labelsRD$ | async)?.payload as cLicenseLabels">
103+
<ds-pagination
104+
[paginationOptions]="labelPaginationOptions"
105+
[collectionSize]="cLicenseLabels?.totalElements"
106+
[hideGear]="true"
107+
[hidePagerWhenSinglePage]="true"
108+
[retainScrollPosition]="true">
109+
<div class="table-responsive">
110+
<table class="table table-striped table-bordered align-middle my-2">
111+
<thead>
112+
<tr>
113+
<th>{{'clarin.license.label.table.header.label' | translate}}</th>
114+
<th>{{'clarin.license.label.table.header.title' | translate}}</th>
115+
<th>{{'clarin.license.label.table.header.extended' | translate}}</th>
116+
<th>{{'clarin.license.label.table.header.icon' | translate}}</th>
117+
<th class="labels-actions-column">{{'clarin.license.label.table.header.actions' | translate}}</th>
118+
</tr>
119+
</thead>
120+
<tbody>
121+
<tr *ngFor="let label of cLicenseLabels?.page">
122+
<td>{{label?.label}}</td>
123+
<td>{{label?.title}}</td>
124+
<td>{{label?.extended ? ('clarin.license.label.table.boolean.yes' | translate) : ('clarin.license.label.table.boolean.no' | translate)}}</td>
125+
<td>
126+
<i class="fas" [ngClass]="label?.icon?.length > 0 ? 'fa-check text-success' : 'fa-times text-muted'" aria-hidden="true"></i>
127+
<span class="sr-only">{{label?.icon?.length > 0 ? ('clarin.license.label.table.icon.available' | translate) : ('clarin.license.label.table.icon.none' | translate)}}</span>
128+
</td>
129+
<td>
130+
<button
131+
type="button"
132+
class="btn btn-warning btn-sm mr-1"
133+
(click)="editLabel(label)"
134+
[attr.aria-label]="('clarin.license.label.table.edit.aria' | translate:{label: (label?.label || '')})">
135+
<i class="fas fa-pen" aria-hidden="true"></i>
136+
{{'clarin.license.label.table.edit' | translate}}
137+
</button>
138+
<ng-container *ngVar="(labelUsageReady$ | async) as usageReady">
139+
<span [ngbTooltip]="!usageReady
140+
? ('clarin.license.label.table.delete.checking-tooltip' | translate)
141+
: (isLabelInUse(label) ? ('clarin.license.label.table.delete.disabled-tooltip' | translate:{ label: label.label }) : null)"
142+
[attr.tabindex]="(!usageReady || isLabelInUse(label)) ? 0 : null">
143+
<button class="btn btn-danger btn-sm"
144+
type="button"
145+
[dsBtnDisabled]="!usageReady || isLabelInUse(label)"
146+
[attr.aria-label]="('clarin.license.label.table.delete.aria' | translate:{label: (label?.label || '')})"
147+
(click)="usageReady && !isLabelInUse(label) && confirmDeleteLabel(label)">
148+
<i class="fas fa-trash" aria-hidden="true"></i>
149+
{{ 'clarin.license.label.table.delete' | translate }}
150+
</button>
151+
</span>
152+
</ng-container>
153+
</td>
154+
</tr>
155+
<tr *ngIf="!(loading$ | async) && (labelsRD$ | async)?.hasSucceeded && !(cLicenseLabels?.page?.length > 0)">
156+
<td colspan="5" class="text-center text-muted">{{'clarin.license.label.table.empty' | translate}}</td>
157+
</tr>
158+
</tbody>
159+
</table>
160+
</div>
161+
162+
<ds-loading *ngIf="loading$ | async" class="text-center"></ds-loading>
163+
<span *ngIf="loading$ | async" class="sr-only">{{'clarin.license.label.table.loading' | translate}}</span>
164+
</ds-pagination>
165+
</div>
166+
</div>
86167
</div>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@
1515
width: 3.5%;
1616
max-width: 3.5%;
1717
}
18+
19+
.labels-actions-column {
20+
width: 11rem;
21+
}

0 commit comments

Comments
 (0)