spiderly 19.12.1 → 19.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/docs/angular-customization/index.md +3 -3
- package/agent/manifest.json +0 -5
- package/fesm2022/spiderly.mjs +356 -106
- package/fesm2022/spiderly.mjs.map +1 -1
- package/lib/components/spiderly-data-table/spiderly-data-table.component.d.ts +62 -19
- package/lib/filters/filter-store.d.ts +14 -4
- package/lib/filters/spiderly-filter-bar.component.d.ts +33 -2
- package/package.json +1 -1
- package/styles/filters/spiderly-filter-bar.component.scss +37 -0
- package/agent/skills/verify-ui/SKILL.md +0 -166
- package/agent/skills/verify-ui/scripts/get-admin-token.mjs +0 -134
package/fesm2022/spiderly.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { TooltipModule } from 'primeng/tooltip';
|
|
|
17
17
|
import * as FileSaver from 'file-saver';
|
|
18
18
|
import mime from 'mime';
|
|
19
19
|
import 'reflect-metadata';
|
|
20
|
-
import { map, Subject, throttleTime, BehaviorSubject, of, filter as filter$1, firstValueFrom, take, finalize as finalize$1, tap as tap$1, throwError } from 'rxjs';
|
|
20
|
+
import { map, Subject, throttleTime, BehaviorSubject, of, filter as filter$1, firstValueFrom, take, finalize as finalize$1, tap as tap$1, throwError, from } from 'rxjs';
|
|
21
21
|
import * as i4$2 from 'primeng/checkbox';
|
|
22
22
|
import { CheckboxModule } from 'primeng/checkbox';
|
|
23
23
|
import * as i4$3 from 'primeng/colorpicker';
|
|
@@ -49,7 +49,7 @@ import * as i3$2 from '@angular/router';
|
|
|
49
49
|
import { NavigationEnd, RouterModule } from '@angular/router';
|
|
50
50
|
import * as i1$1 from 'primeng/api';
|
|
51
51
|
import { ConfirmationService } from 'primeng/api';
|
|
52
|
-
import { filter, map as map$1, finalize, catchError, delay, tap, takeUntil } from 'rxjs/operators';
|
|
52
|
+
import { filter, map as map$1, finalize, catchError, delay, tap, takeUntil, switchMap } from 'rxjs/operators';
|
|
53
53
|
import * as i1$2 from '@angular/common/http';
|
|
54
54
|
import { HttpParams, HttpHeaders, HttpErrorResponse, HttpResponse } from '@angular/common/http';
|
|
55
55
|
import { InputOtp } from 'primeng/inputotp';
|
|
@@ -4101,8 +4101,20 @@ class SpiderlyFilterBarComponent {
|
|
|
4101
4101
|
* about columns, so whoever owns the sort resolves the names.
|
|
4102
4102
|
*/
|
|
4103
4103
|
this.sort = input([]);
|
|
4104
|
+
/**
|
|
4105
|
+
* What the picker offers, labelled by the owner — every sortable column, HIDDEN ones
|
|
4106
|
+
* included (their ordering is otherwise unreachable, which is the trap the picker closes).
|
|
4107
|
+
* Empty keeps the chip read-only and the picker off.
|
|
4108
|
+
*/
|
|
4109
|
+
this.sortOptions = input([]);
|
|
4110
|
+
/** Whether the grid already sits on its default ordering — gates the reset row, nothing else. */
|
|
4111
|
+
this.sortIsDefault = input(true);
|
|
4104
4112
|
/** Asked for, never done here — see the button's comment. */
|
|
4105
4113
|
this.clearAll = output();
|
|
4114
|
+
/** A single-sort ask. Multi-sort stays a header gesture (ctrl-click); the picker replaces. */
|
|
4115
|
+
this.sortPick = output();
|
|
4116
|
+
/** Back to the owner's default ordering — the owner knows what that resolves to, not the bar. */
|
|
4117
|
+
this.sortReset = output();
|
|
4106
4118
|
this.transloco = inject(TranslocoService);
|
|
4107
4119
|
/** For the chip's date wording — the same locale mechanism the table's cells format with. */
|
|
4108
4120
|
this.locale = inject(LOCALE_ID);
|
|
@@ -4113,6 +4125,7 @@ class SpiderlyFilterBarComponent {
|
|
|
4113
4125
|
*/
|
|
4114
4126
|
this.controlTemplates = input(undefined);
|
|
4115
4127
|
this.isAddOpen = signal(false);
|
|
4128
|
+
this.isSortOpen = signal(false);
|
|
4116
4129
|
/** Cleared when the popover closes, so reopening never starts inside someone's old query. */
|
|
4117
4130
|
this.addSearch = signal('');
|
|
4118
4131
|
/** The filter whose control is open. One at a time: the bar edits, it is not a form. */
|
|
@@ -4143,6 +4156,22 @@ class SpiderlyFilterBarComponent {
|
|
|
4143
4156
|
.filter(([id, definition]) => !onBar.has(id) && definition.offered !== false)
|
|
4144
4157
|
.map(([id, definition]) => ({ id, ...definition }));
|
|
4145
4158
|
});
|
|
4159
|
+
/**
|
|
4160
|
+
* The menu's rows: each option decorated with its ACTIVE direction's arrow (any key, not
|
|
4161
|
+
* just the primary), or null. A `computed`, not a per-row method call in the binding — the
|
|
4162
|
+
* popover's content is projected, so it change-detects even while closed, and a `find` per
|
|
4163
|
+
* option per pass is exactly what `operatorChoices` documents against.
|
|
4164
|
+
*/
|
|
4165
|
+
this.sortRows = computed(() => {
|
|
4166
|
+
const active = new Map(this.sort().map((key) => [key.field, key.descending]));
|
|
4167
|
+
return this.sortOptions().map((option) => {
|
|
4168
|
+
const descending = active.get(option.field);
|
|
4169
|
+
return {
|
|
4170
|
+
...option,
|
|
4171
|
+
arrow: descending == null ? null : descending ? '↓' : '↑',
|
|
4172
|
+
};
|
|
4173
|
+
});
|
|
4174
|
+
});
|
|
4146
4175
|
/**
|
|
4147
4176
|
* The open editor's operator options, already translated — p-select takes labels, not keys.
|
|
4148
4177
|
*
|
|
@@ -4166,6 +4195,16 @@ class SpiderlyFilterBarComponent {
|
|
|
4166
4195
|
this.isAddOpen.set(false);
|
|
4167
4196
|
this.editing.set(this.filters().get(id));
|
|
4168
4197
|
}
|
|
4198
|
+
/**
|
|
4199
|
+
* Ascending first; picking the already-ascending primary flips it. The direction is decided
|
|
4200
|
+
* HERE so the owner applies exactly what the menu showed — an owner guessing "toggle" from its
|
|
4201
|
+
* own state could disagree with a stale menu after a concurrent header click.
|
|
4202
|
+
*/
|
|
4203
|
+
pickSort(option) {
|
|
4204
|
+
const primary = this.sort()[0];
|
|
4205
|
+
const order = primary?.field === option.field && !primary.descending ? -1 : 1;
|
|
4206
|
+
this.sortPick.emit({ field: option.field, order });
|
|
4207
|
+
}
|
|
4169
4208
|
/** The raw-string controls only. Nothing reaches the bar or the query until `apply`. */
|
|
4170
4209
|
draft(handle, event) {
|
|
4171
4210
|
this.draftValue(handle, this.coerce(handle.kind, event.target.value));
|
|
@@ -4234,7 +4273,7 @@ class SpiderlyFilterBarComponent {
|
|
|
4234
4273
|
: spellOptionValue(options, chip.value);
|
|
4235
4274
|
}
|
|
4236
4275
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyFilterBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
4237
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.13", type: SpiderlyFilterBarComponent, isStandalone: true, selector: "spiderly-filter-bar", inputs: { filters: { classPropertyName: "filters", publicName: "filters", isSignal: true, isRequired: true, transformFunction: null }, totalRecords: { classPropertyName: "totalRecords", publicName: "totalRecords", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, controlTemplates: { classPropertyName: "controlTemplates", publicName: "controlTemplates", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clearAll: "clearAll" }, ngImport: i0, template: `
|
|
4276
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.13", type: SpiderlyFilterBarComponent, isStandalone: true, selector: "spiderly-filter-bar", inputs: { filters: { classPropertyName: "filters", publicName: "filters", isSignal: true, isRequired: true, transformFunction: null }, totalRecords: { classPropertyName: "totalRecords", publicName: "totalRecords", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, sortOptions: { classPropertyName: "sortOptions", publicName: "sortOptions", isSignal: true, isRequired: false, transformFunction: null }, sortIsDefault: { classPropertyName: "sortIsDefault", publicName: "sortIsDefault", isSignal: true, isRequired: false, transformFunction: null }, controlTemplates: { classPropertyName: "controlTemplates", publicName: "controlTemplates", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clearAll: "clearAll", sortPick: "sortPick", sortReset: "sortReset" }, ngImport: i0, template: `
|
|
4238
4277
|
<ng-container *transloco="let t">
|
|
4239
4278
|
<div class="filter-bar" data-testid="filter-bar">
|
|
4240
4279
|
@for (chip of filters().applied(); track chip.id) {
|
|
@@ -4277,23 +4316,49 @@ class SpiderlyFilterBarComponent {
|
|
|
4277
4316
|
</span>
|
|
4278
4317
|
}
|
|
4279
4318
|
|
|
4280
|
-
@if (
|
|
4281
|
-
<!--
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4319
|
+
@if (sortOptions().length) {
|
|
4320
|
+
<!-- The chip is the PICKER's trigger (2026-09-06, grilled) — Polaris's always-present
|
|
4321
|
+
SortButton, in the slot the read-only chip held. Always rendered, sorted or not:
|
|
4322
|
+
with the trigger only on an active sort, an unsorted grid would have no way to
|
|
4323
|
+
sort by a hidden column, which is the capability the picker exists for. It
|
|
4324
|
+
replaced a read-only chip whose stated rationale ("an x would lie — the default
|
|
4325
|
+
snaps back") inverted the day a viewed table had no default: the operator was
|
|
4326
|
+
left with an inherited sort, no chip x, no clear button, and the one header that
|
|
4327
|
+
could un-sort it hidden. -->
|
|
4328
|
+
<button
|
|
4329
|
+
type="button"
|
|
4330
|
+
class="sort-chip"
|
|
4331
|
+
data-testid="sort-chip"
|
|
4332
|
+
[attr.aria-expanded]="isSortOpen()"
|
|
4333
|
+
(click)="sortMenu.toggle($event); isSortOpen.set(!isSortOpen())"
|
|
4334
|
+
>
|
|
4335
|
+
<ng-container *ngTemplateOutlet="sortChipBody"></ng-container>
|
|
4336
|
+
<i class="pi pi-chevron-down sort-chip-caret" aria-hidden="true"></i>
|
|
4337
|
+
</button>
|
|
4338
|
+
} @else if (sort().length) {
|
|
4339
|
+
<!-- No options handed over — the chip stays what it was: one read-only place naming
|
|
4340
|
+
the ordering and which key is primary. -->
|
|
4286
4341
|
<span class="sort-chip" data-testid="sort-chip">
|
|
4342
|
+
<ng-container *ngTemplateOutlet="sortChipBody"></ng-container>
|
|
4343
|
+
</span>
|
|
4344
|
+
}
|
|
4345
|
+
|
|
4346
|
+
<!-- One home for the chip's sentence, whichever element renders it — the read-only
|
|
4347
|
+
branch never reaches the @else (it is gated on sort().length). -->
|
|
4348
|
+
<ng-template #sortChipBody>
|
|
4349
|
+
@if (sort().length) {
|
|
4287
4350
|
<span class="sort-chip-label">{{ t('SortedBy') }}</span>
|
|
4288
|
-
@for (key of sort(); track key.
|
|
4351
|
+
@for (key of sort(); track key.field; let last = $last) {
|
|
4289
4352
|
<span class="sort-chip-key"
|
|
4290
4353
|
>{{ key.label }} {{ key.descending ? '↓' : '↑' }}{{
|
|
4291
4354
|
last ? '' : ','
|
|
4292
4355
|
}}</span
|
|
4293
4356
|
>
|
|
4294
4357
|
}
|
|
4295
|
-
|
|
4296
|
-
|
|
4358
|
+
} @else {
|
|
4359
|
+
<span class="sort-chip-label">{{ t('Sort') }}</span>
|
|
4360
|
+
}
|
|
4361
|
+
</ng-template>
|
|
4297
4362
|
|
|
4298
4363
|
@if (totalRecords() !== null) {
|
|
4299
4364
|
<!-- What came back, next to the chips that say what was asked. An empty grid under a
|
|
@@ -4325,8 +4390,8 @@ class SpiderlyFilterBarComponent {
|
|
|
4325
4390
|
@if (filters().applied().length) {
|
|
4326
4391
|
<!-- The bar owns clearing now: the toolbar's button sat a row away from the chips it
|
|
4327
4392
|
cleared, which is two affordances for one job. Emits rather than clearing the store
|
|
4328
|
-
itself, because whoever owns the query
|
|
4329
|
-
that a
|
|
4393
|
+
itself, because whoever owns the query owns the requery and the persisted snapshot
|
|
4394
|
+
that follow a clear. Filters only — sort is its own channel (the picker above). -->
|
|
4330
4395
|
<button
|
|
4331
4396
|
pButton
|
|
4332
4397
|
type="button"
|
|
@@ -4338,6 +4403,46 @@ class SpiderlyFilterBarComponent {
|
|
|
4338
4403
|
></button>
|
|
4339
4404
|
}
|
|
4340
4405
|
|
|
4406
|
+
<p-popover #sortMenu (onHide)="isSortOpen.set(false)">
|
|
4407
|
+
<div class="filter-add-menu" role="menu">
|
|
4408
|
+
@for (option of sortRows(); track option.field) {
|
|
4409
|
+
<!-- Click sorts ascending; clicking the already-ascending option flips it. One
|
|
4410
|
+
gesture, look, one gesture back — the header's cycle without needing the
|
|
4411
|
+
header, so a hidden column's ordering is reachable too. -->
|
|
4412
|
+
<button
|
|
4413
|
+
type="button"
|
|
4414
|
+
role="menuitem"
|
|
4415
|
+
class="filter-add-option sort-menu-option"
|
|
4416
|
+
data-testid="sort-menu-option"
|
|
4417
|
+
(click)="pickSort(option); sortMenu.hide()"
|
|
4418
|
+
>
|
|
4419
|
+
<!-- &ngsp; keeps a real space between label and arrow — same collapse the
|
|
4420
|
+
filter chip documents: without it the copied/read text is "Naziv↓". -->
|
|
4421
|
+
<span>{{ option.label }}</span>&ngsp;
|
|
4422
|
+
@if (option.arrow) {
|
|
4423
|
+
<span class="sort-menu-direction" aria-hidden="true">{{
|
|
4424
|
+
option.arrow
|
|
4425
|
+
}}</span>
|
|
4426
|
+
}
|
|
4427
|
+
</button>
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
@if (!sortIsDefault()) {
|
|
4431
|
+
<!-- Only while there is an override to undo: at the default there is nothing to
|
|
4432
|
+
reset, and a row that does nothing teaches people to stop reading the menu. -->
|
|
4433
|
+
<button
|
|
4434
|
+
type="button"
|
|
4435
|
+
role="menuitem"
|
|
4436
|
+
class="filter-add-option sort-menu-reset"
|
|
4437
|
+
data-testid="sort-menu-reset"
|
|
4438
|
+
(click)="sortReset.emit(); sortMenu.hide()"
|
|
4439
|
+
>
|
|
4440
|
+
{{ t('ResetSort') }}
|
|
4441
|
+
</button>
|
|
4442
|
+
}
|
|
4443
|
+
</div>
|
|
4444
|
+
</p-popover>
|
|
4445
|
+
|
|
4341
4446
|
<p-popover #addMenu (onHide)="isAddOpen.set(false); addSearch.set('')">
|
|
4342
4447
|
<div class="filter-add-menu" role="menu">
|
|
4343
4448
|
<input
|
|
@@ -4464,7 +4569,7 @@ class SpiderlyFilterBarComponent {
|
|
|
4464
4569
|
</div>
|
|
4465
4570
|
}
|
|
4466
4571
|
</ng-container>
|
|
4467
|
-
`, isInline: true, styles: [":host{display:block}.filter-bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 0 0}.filter-chip{display:inline-flex;align-items:center;gap:2px;border:1px solid var(--p-content-border-color);border-radius:999px;background:var(--p-content-background);padding:0;font-size:.875rem;line-height:1.4}.filter-chip-edit{border:none;background:none;color:inherit;font:inherit;cursor:pointer;display:inline-flex;align-items:center;gap:4px;padding:4px 4px 4px 10px;border-radius:999px 0 0 999px}.filter-chip-edit:hover .filter-chip-value{text-decoration:underline}.filter-chip-label{color:var(--p-text-muted-color)}.filter-chip-value{font-weight:600}.filter-chip-remove{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);font-size:1.05rem;line-height:1;padding:4px 9px 5px 4px;border-radius:0 999px 999px 0}.filter-chip-remove:hover{color:var(--p-primary-color)}.filter-add{flex:none}.filter-add-menu{display:flex;flex-direction:column;align-items:stretch;gap:2px;min-width:14rem;max-height:20rem;overflow-y:auto}.filter-add-search{margin-bottom:4px;position:sticky;top:0}.filter-add-option{border:none;background:none;color:inherit;font:inherit;cursor:pointer;text-align:left;padding:6px 10px;border-radius:var(--p-content-border-radius)}.filter-add-option:hover{background:var(--p-content-hover-background)}.filter-editor{display:flex;margin-top:8px;flex-wrap:wrap;align-items:center;gap:8px;border:1px solid var(--p-content-border-color);border-radius:var(--p-content-border-radius);background:var(--p-content-background);padding:8px}.filter-editor-label{font-weight:600}.filter-editor-tick{display:inline-flex;align-items:center;gap:4px;cursor:pointer}.filter-bar-count{color:var(--p-text-muted-color);font-size:.875rem;margin-left:4px}.sort-chip{display:inline-flex;align-items:center;gap:4px;color:var(--p-text-muted-color);font-size:.875rem}.sort-chip-key{color:var(--p-text-color)}.filter-bar-clear{margin-left:auto;flex:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3$3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "fluid", "label", "icon", "buttonProps"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["value", "name", "disabled", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "inputStyle", "styleClass", "inputClass", "indeterminate", "size", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DatePickerModule }, { kind: "component", type: i4$1.DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "style", "styleClass", "inputStyle", "inputId", "name", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "disabled", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "fluid", "icon", "appendTo", "readonlyInput", "shortYearCutoff", "monthNavigator", "yearNavigator", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "required", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "variant", "size", "minDate", "maxDate", "disabledDates", "disabledDays", "yearRange", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "locale", "view", "defaultDate"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i5$1.InputText, selector: "[pInputText]", inputs: ["variant", "fluid", "pSize"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i7.MultiSelect, selector: "p-multiSelect, p-multiselect, p-multi-select", inputs: ["id", "ariaLabel", "style", "styleClass", "panelStyle", "panelStyleClass", "inputId", "disabled", "fluid", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "variant", "appendTo", "dataKey", "name", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "chipIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "size", "showClear", "autofocus", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "defaultLabel", "placeholder", "options", "filterValue", "itemSize", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus", "highlightOnSelect"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i8.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i3$1.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "size", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "fluid", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }] }); }
|
|
4572
|
+
`, isInline: true, styles: [":host{display:block}.filter-bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 0 0}.filter-chip{display:inline-flex;align-items:center;gap:2px;border:1px solid var(--p-content-border-color);border-radius:999px;background:var(--p-content-background);padding:0;font-size:.875rem;line-height:1.4}.filter-chip-edit{border:none;background:none;color:inherit;font:inherit;cursor:pointer;display:inline-flex;align-items:center;gap:4px;padding:4px 4px 4px 10px;border-radius:999px 0 0 999px}.filter-chip-edit:hover .filter-chip-value{text-decoration:underline}.filter-chip-label{color:var(--p-text-muted-color)}.filter-chip-value{font-weight:600}.filter-chip-remove{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);font-size:1.05rem;line-height:1;padding:4px 9px 5px 4px;border-radius:0 999px 999px 0}.filter-chip-remove:hover{color:var(--p-primary-color)}.filter-add{flex:none}.filter-add-menu{display:flex;flex-direction:column;align-items:stretch;gap:2px;min-width:14rem;max-height:20rem;overflow-y:auto}.filter-add-search{margin-bottom:4px;position:sticky;top:0}.filter-add-option{border:none;background:none;color:inherit;font:inherit;cursor:pointer;text-align:left;padding:6px 10px;border-radius:var(--p-content-border-radius)}.filter-add-option:hover{background:var(--p-content-hover-background)}.filter-editor{display:flex;margin-top:8px;flex-wrap:wrap;align-items:center;gap:8px;border:1px solid var(--p-content-border-color);border-radius:var(--p-content-border-radius);background:var(--p-content-background);padding:8px}.filter-editor-label{font-weight:600}.filter-editor-tick{display:inline-flex;align-items:center;gap:4px;cursor:pointer}.filter-bar-count{color:var(--p-text-muted-color);font-size:.875rem;margin-left:4px}.sort-chip{display:inline-flex;align-items:center;gap:4px;color:var(--p-text-muted-color);font-size:.875rem}button.sort-chip{border:none;background:none;color:inherit;font:inherit;padding:0;cursor:pointer}button.sort-chip:hover .sort-chip-key,button.sort-chip:hover .sort-chip-label{text-decoration:underline}.sort-chip-caret{font-size:.65rem}.sort-chip-key{color:var(--p-text-color)}.sort-menu-option{display:flex;justify-content:space-between;align-items:center;gap:12px}.sort-menu-direction{color:var(--p-primary-color)}.sort-menu-reset{border-top:1px solid var(--p-content-border-color);border-radius:0;color:var(--p-text-muted-color)}.filter-bar-clear{margin-left:auto;flex:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3$3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "fluid", "label", "icon", "buttonProps"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["value", "name", "disabled", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "inputStyle", "styleClass", "inputClass", "indeterminate", "size", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DatePickerModule }, { kind: "component", type: i4$1.DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "style", "styleClass", "inputStyle", "inputId", "name", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "disabled", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "fluid", "icon", "appendTo", "readonlyInput", "shortYearCutoff", "monthNavigator", "yearNavigator", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "required", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "variant", "size", "minDate", "maxDate", "disabledDates", "disabledDays", "yearRange", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "locale", "view", "defaultDate"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i5$1.InputText, selector: "[pInputText]", inputs: ["variant", "fluid", "pSize"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i7.MultiSelect, selector: "p-multiSelect, p-multiselect, p-multi-select", inputs: ["id", "ariaLabel", "style", "styleClass", "panelStyle", "panelStyleClass", "inputId", "disabled", "fluid", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "variant", "appendTo", "dataKey", "name", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "chipIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "size", "showClear", "autofocus", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "defaultLabel", "placeholder", "options", "filterValue", "itemSize", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus", "highlightOnSelect"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i8.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i3$1.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "size", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "fluid", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }] }); }
|
|
4468
4573
|
}
|
|
4469
4574
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyFilterBarComponent, decorators: [{
|
|
4470
4575
|
type: Component,
|
|
@@ -4522,23 +4627,49 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
4522
4627
|
</span>
|
|
4523
4628
|
}
|
|
4524
4629
|
|
|
4525
|
-
@if (
|
|
4526
|
-
<!--
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4630
|
+
@if (sortOptions().length) {
|
|
4631
|
+
<!-- The chip is the PICKER's trigger (2026-09-06, grilled) — Polaris's always-present
|
|
4632
|
+
SortButton, in the slot the read-only chip held. Always rendered, sorted or not:
|
|
4633
|
+
with the trigger only on an active sort, an unsorted grid would have no way to
|
|
4634
|
+
sort by a hidden column, which is the capability the picker exists for. It
|
|
4635
|
+
replaced a read-only chip whose stated rationale ("an x would lie — the default
|
|
4636
|
+
snaps back") inverted the day a viewed table had no default: the operator was
|
|
4637
|
+
left with an inherited sort, no chip x, no clear button, and the one header that
|
|
4638
|
+
could un-sort it hidden. -->
|
|
4639
|
+
<button
|
|
4640
|
+
type="button"
|
|
4641
|
+
class="sort-chip"
|
|
4642
|
+
data-testid="sort-chip"
|
|
4643
|
+
[attr.aria-expanded]="isSortOpen()"
|
|
4644
|
+
(click)="sortMenu.toggle($event); isSortOpen.set(!isSortOpen())"
|
|
4645
|
+
>
|
|
4646
|
+
<ng-container *ngTemplateOutlet="sortChipBody"></ng-container>
|
|
4647
|
+
<i class="pi pi-chevron-down sort-chip-caret" aria-hidden="true"></i>
|
|
4648
|
+
</button>
|
|
4649
|
+
} @else if (sort().length) {
|
|
4650
|
+
<!-- No options handed over — the chip stays what it was: one read-only place naming
|
|
4651
|
+
the ordering and which key is primary. -->
|
|
4531
4652
|
<span class="sort-chip" data-testid="sort-chip">
|
|
4653
|
+
<ng-container *ngTemplateOutlet="sortChipBody"></ng-container>
|
|
4654
|
+
</span>
|
|
4655
|
+
}
|
|
4656
|
+
|
|
4657
|
+
<!-- One home for the chip's sentence, whichever element renders it — the read-only
|
|
4658
|
+
branch never reaches the @else (it is gated on sort().length). -->
|
|
4659
|
+
<ng-template #sortChipBody>
|
|
4660
|
+
@if (sort().length) {
|
|
4532
4661
|
<span class="sort-chip-label">{{ t('SortedBy') }}</span>
|
|
4533
|
-
@for (key of sort(); track key.
|
|
4662
|
+
@for (key of sort(); track key.field; let last = $last) {
|
|
4534
4663
|
<span class="sort-chip-key"
|
|
4535
4664
|
>{{ key.label }} {{ key.descending ? '↓' : '↑' }}{{
|
|
4536
4665
|
last ? '' : ','
|
|
4537
4666
|
}}</span
|
|
4538
4667
|
>
|
|
4539
4668
|
}
|
|
4540
|
-
|
|
4541
|
-
|
|
4669
|
+
} @else {
|
|
4670
|
+
<span class="sort-chip-label">{{ t('Sort') }}</span>
|
|
4671
|
+
}
|
|
4672
|
+
</ng-template>
|
|
4542
4673
|
|
|
4543
4674
|
@if (totalRecords() !== null) {
|
|
4544
4675
|
<!-- What came back, next to the chips that say what was asked. An empty grid under a
|
|
@@ -4570,8 +4701,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
4570
4701
|
@if (filters().applied().length) {
|
|
4571
4702
|
<!-- The bar owns clearing now: the toolbar's button sat a row away from the chips it
|
|
4572
4703
|
cleared, which is two affordances for one job. Emits rather than clearing the store
|
|
4573
|
-
itself, because whoever owns the query
|
|
4574
|
-
that a
|
|
4704
|
+
itself, because whoever owns the query owns the requery and the persisted snapshot
|
|
4705
|
+
that follow a clear. Filters only — sort is its own channel (the picker above). -->
|
|
4575
4706
|
<button
|
|
4576
4707
|
pButton
|
|
4577
4708
|
type="button"
|
|
@@ -4583,6 +4714,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
4583
4714
|
></button>
|
|
4584
4715
|
}
|
|
4585
4716
|
|
|
4717
|
+
<p-popover #sortMenu (onHide)="isSortOpen.set(false)">
|
|
4718
|
+
<div class="filter-add-menu" role="menu">
|
|
4719
|
+
@for (option of sortRows(); track option.field) {
|
|
4720
|
+
<!-- Click sorts ascending; clicking the already-ascending option flips it. One
|
|
4721
|
+
gesture, look, one gesture back — the header's cycle without needing the
|
|
4722
|
+
header, so a hidden column's ordering is reachable too. -->
|
|
4723
|
+
<button
|
|
4724
|
+
type="button"
|
|
4725
|
+
role="menuitem"
|
|
4726
|
+
class="filter-add-option sort-menu-option"
|
|
4727
|
+
data-testid="sort-menu-option"
|
|
4728
|
+
(click)="pickSort(option); sortMenu.hide()"
|
|
4729
|
+
>
|
|
4730
|
+
<!-- &ngsp; keeps a real space between label and arrow — same collapse the
|
|
4731
|
+
filter chip documents: without it the copied/read text is "Naziv↓". -->
|
|
4732
|
+
<span>{{ option.label }}</span>&ngsp;
|
|
4733
|
+
@if (option.arrow) {
|
|
4734
|
+
<span class="sort-menu-direction" aria-hidden="true">{{
|
|
4735
|
+
option.arrow
|
|
4736
|
+
}}</span>
|
|
4737
|
+
}
|
|
4738
|
+
</button>
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
@if (!sortIsDefault()) {
|
|
4742
|
+
<!-- Only while there is an override to undo: at the default there is nothing to
|
|
4743
|
+
reset, and a row that does nothing teaches people to stop reading the menu. -->
|
|
4744
|
+
<button
|
|
4745
|
+
type="button"
|
|
4746
|
+
role="menuitem"
|
|
4747
|
+
class="filter-add-option sort-menu-reset"
|
|
4748
|
+
data-testid="sort-menu-reset"
|
|
4749
|
+
(click)="sortReset.emit(); sortMenu.hide()"
|
|
4750
|
+
>
|
|
4751
|
+
{{ t('ResetSort') }}
|
|
4752
|
+
</button>
|
|
4753
|
+
}
|
|
4754
|
+
</div>
|
|
4755
|
+
</p-popover>
|
|
4756
|
+
|
|
4586
4757
|
<p-popover #addMenu (onHide)="isAddOpen.set(false); addSearch.set('')">
|
|
4587
4758
|
<div class="filter-add-menu" role="menu">
|
|
4588
4759
|
<input
|
|
@@ -4709,7 +4880,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
4709
4880
|
</div>
|
|
4710
4881
|
}
|
|
4711
4882
|
</ng-container>
|
|
4712
|
-
`, styles: [":host{display:block}.filter-bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 0 0}.filter-chip{display:inline-flex;align-items:center;gap:2px;border:1px solid var(--p-content-border-color);border-radius:999px;background:var(--p-content-background);padding:0;font-size:.875rem;line-height:1.4}.filter-chip-edit{border:none;background:none;color:inherit;font:inherit;cursor:pointer;display:inline-flex;align-items:center;gap:4px;padding:4px 4px 4px 10px;border-radius:999px 0 0 999px}.filter-chip-edit:hover .filter-chip-value{text-decoration:underline}.filter-chip-label{color:var(--p-text-muted-color)}.filter-chip-value{font-weight:600}.filter-chip-remove{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);font-size:1.05rem;line-height:1;padding:4px 9px 5px 4px;border-radius:0 999px 999px 0}.filter-chip-remove:hover{color:var(--p-primary-color)}.filter-add{flex:none}.filter-add-menu{display:flex;flex-direction:column;align-items:stretch;gap:2px;min-width:14rem;max-height:20rem;overflow-y:auto}.filter-add-search{margin-bottom:4px;position:sticky;top:0}.filter-add-option{border:none;background:none;color:inherit;font:inherit;cursor:pointer;text-align:left;padding:6px 10px;border-radius:var(--p-content-border-radius)}.filter-add-option:hover{background:var(--p-content-hover-background)}.filter-editor{display:flex;margin-top:8px;flex-wrap:wrap;align-items:center;gap:8px;border:1px solid var(--p-content-border-color);border-radius:var(--p-content-border-radius);background:var(--p-content-background);padding:8px}.filter-editor-label{font-weight:600}.filter-editor-tick{display:inline-flex;align-items:center;gap:4px;cursor:pointer}.filter-bar-count{color:var(--p-text-muted-color);font-size:.875rem;margin-left:4px}.sort-chip{display:inline-flex;align-items:center;gap:4px;color:var(--p-text-muted-color);font-size:.875rem}.sort-chip-key{color:var(--p-text-color)}.filter-bar-clear{margin-left:auto;flex:none}\n"] }]
|
|
4883
|
+
`, styles: [":host{display:block}.filter-bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 0 0}.filter-chip{display:inline-flex;align-items:center;gap:2px;border:1px solid var(--p-content-border-color);border-radius:999px;background:var(--p-content-background);padding:0;font-size:.875rem;line-height:1.4}.filter-chip-edit{border:none;background:none;color:inherit;font:inherit;cursor:pointer;display:inline-flex;align-items:center;gap:4px;padding:4px 4px 4px 10px;border-radius:999px 0 0 999px}.filter-chip-edit:hover .filter-chip-value{text-decoration:underline}.filter-chip-label{color:var(--p-text-muted-color)}.filter-chip-value{font-weight:600}.filter-chip-remove{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);font-size:1.05rem;line-height:1;padding:4px 9px 5px 4px;border-radius:0 999px 999px 0}.filter-chip-remove:hover{color:var(--p-primary-color)}.filter-add{flex:none}.filter-add-menu{display:flex;flex-direction:column;align-items:stretch;gap:2px;min-width:14rem;max-height:20rem;overflow-y:auto}.filter-add-search{margin-bottom:4px;position:sticky;top:0}.filter-add-option{border:none;background:none;color:inherit;font:inherit;cursor:pointer;text-align:left;padding:6px 10px;border-radius:var(--p-content-border-radius)}.filter-add-option:hover{background:var(--p-content-hover-background)}.filter-editor{display:flex;margin-top:8px;flex-wrap:wrap;align-items:center;gap:8px;border:1px solid var(--p-content-border-color);border-radius:var(--p-content-border-radius);background:var(--p-content-background);padding:8px}.filter-editor-label{font-weight:600}.filter-editor-tick{display:inline-flex;align-items:center;gap:4px;cursor:pointer}.filter-bar-count{color:var(--p-text-muted-color);font-size:.875rem;margin-left:4px}.sort-chip{display:inline-flex;align-items:center;gap:4px;color:var(--p-text-muted-color);font-size:.875rem}button.sort-chip{border:none;background:none;color:inherit;font:inherit;padding:0;cursor:pointer}button.sort-chip:hover .sort-chip-key,button.sort-chip:hover .sort-chip-label{text-decoration:underline}.sort-chip-caret{font-size:.65rem}.sort-chip-key{color:var(--p-text-color)}.sort-menu-option{display:flex;justify-content:space-between;align-items:center;gap:12px}.sort-menu-direction{color:var(--p-primary-color)}.sort-menu-reset{border-top:1px solid var(--p-content-border-color);border-radius:0;color:var(--p-text-muted-color)}.filter-bar-clear{margin-left:auto;flex:none}\n"] }]
|
|
4713
4884
|
}] });
|
|
4714
4885
|
|
|
4715
4886
|
class SpiderlyDeleteConfirmationComponent {
|
|
@@ -4977,6 +5148,18 @@ class SpiderlyDataTableComponent {
|
|
|
4977
5148
|
* `tableStyle` is a field.
|
|
4978
5149
|
*/
|
|
4979
5150
|
this.sortKeys = [];
|
|
5151
|
+
/**
|
|
5152
|
+
* Gates the picker's reset row. Refreshed with `sortKeys` — same request, same reason it is a
|
|
5153
|
+
* field rather than a getter. "Default" is view-aware (`defaultMultiSortMeta`), so inside a
|
|
5154
|
+
* view the row offers the view's ordering back, not the table's.
|
|
5155
|
+
*/
|
|
5156
|
+
this.sortIsDefault = true;
|
|
5157
|
+
/**
|
|
5158
|
+
* What the bar's picker offers: every sortable data column, hidden ones INCLUDED — the picker
|
|
5159
|
+
* exists so an ordering does not need its column on screen. Built once in `ngOnInit` (columns
|
|
5160
|
+
* are a declaration, not state) and labelled here because the bar knows nothing about columns.
|
|
5161
|
+
*/
|
|
5162
|
+
this.sortPickOptions = [];
|
|
4980
5163
|
}
|
|
4981
5164
|
ngAfterViewInit() {
|
|
4982
5165
|
this.setupRemovableSort();
|
|
@@ -5006,32 +5189,31 @@ class SpiderlyDataTableComponent {
|
|
|
5006
5189
|
if (sortMeta && sortMeta.order === -1) {
|
|
5007
5190
|
const mouseEvent = event.originalEvent;
|
|
5008
5191
|
const isMultiSortClick = mouseEvent.metaKey || mouseEvent.ctrlKey;
|
|
5009
|
-
// Un-sorting substitutes the declared default (when there is one) right here,
|
|
5010
|
-
// so sortMultiple() broadcasts and emits the final meta in one shot.
|
|
5011
5192
|
if (isMultiSortClick) {
|
|
5193
|
+
// Removing ONE level of a multi-sort keeps the reader's place (PrimeNG's metaKey
|
|
5194
|
+
// path never pages either), so this cannot ride applySort, whose page reset is
|
|
5195
|
+
// half the point. The substituted default keeps sortMultiple() to one emission.
|
|
5012
5196
|
const remaining = this.table._multiSortMeta.filter((m) => m.field !== event.field);
|
|
5013
5197
|
this.table._multiSortMeta = remaining.length
|
|
5014
5198
|
? remaining
|
|
5015
5199
|
: (this.defaultMultiSortMeta() ?? remaining);
|
|
5200
|
+
this.table.sortMultiple();
|
|
5201
|
+
if (this.table.isStateful())
|
|
5202
|
+
this.table.saveState();
|
|
5203
|
+
this.table.anchorRowIndex = null;
|
|
5204
|
+
this.persistActiveViewSort();
|
|
5016
5205
|
}
|
|
5017
5206
|
else {
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
this.table.firstChange.emit(0);
|
|
5022
|
-
}
|
|
5023
|
-
}
|
|
5024
|
-
this.table.sortMultiple();
|
|
5025
|
-
if (this.table.isStateful()) {
|
|
5026
|
-
this.table.saveState();
|
|
5207
|
+
// A single-click un-sort IS "back to the default" — the exact gesture the bar's
|
|
5208
|
+
// reset row asks for, so it uses the same body.
|
|
5209
|
+
this.resetSortFromBar();
|
|
5027
5210
|
}
|
|
5028
|
-
this.table.anchorRowIndex = null;
|
|
5029
5211
|
}
|
|
5030
5212
|
else {
|
|
5031
5213
|
originalSort(event);
|
|
5214
|
+
// PrimeNG's sort() pages/saves itself; the override slot is ours to record.
|
|
5215
|
+
this.persistActiveViewSort();
|
|
5032
5216
|
}
|
|
5033
|
-
// Every explicit header-sort gesture, un-sort included, records the view's override.
|
|
5034
|
-
this.persistActiveViewSort();
|
|
5035
5217
|
};
|
|
5036
5218
|
}
|
|
5037
5219
|
/**
|
|
@@ -5102,17 +5284,24 @@ class SpiderlyDataTableComponent {
|
|
|
5102
5284
|
this.requeryOnAppliedFilters();
|
|
5103
5285
|
this.chooserCols = this.cols.filter(SpiderlyDataTableComponent.isDataColumn);
|
|
5104
5286
|
this.refreshVisibleCols();
|
|
5287
|
+
// From chooserCols, not cols: same "every data column, hidden included" set the chooser
|
|
5288
|
+
// offers — one derivation of that list, so the picker cannot drift from it.
|
|
5289
|
+
this.sortPickOptions = this.chooserCols
|
|
5290
|
+
.filter((col) => this.isColumnSortable(col))
|
|
5291
|
+
.map((col) => ({ field: col.field, label: col.name }));
|
|
5105
5292
|
// Bound to p-table's [multiSortMeta] so the FIRST request already carries the right
|
|
5106
|
-
// sort.
|
|
5107
|
-
//
|
|
5108
|
-
//
|
|
5109
|
-
// the
|
|
5110
|
-
//
|
|
5293
|
+
// sort. On a viewed table the persisted table-global sort is a NON-SOURCE (2026-09-06):
|
|
5294
|
+
// sort is per-view state (override ?? declared ?? table default), and the blob — which
|
|
5295
|
+
// holds the last ordering ANY view used — would re-impose across views the exact
|
|
5296
|
+
// carry-over the per-view model removes, on every F5. It still carries rows/first there,
|
|
5297
|
+
// and stays the one sort memory of a view-less table. We read it all ourselves because
|
|
5298
|
+
// PrimeNG's restoreState() runs on the first [value] change — after the initial lazy emit
|
|
5299
|
+
// has already left — and is then told to agree (see `alignPersistedTableStateSort`).
|
|
5111
5300
|
const tableState = this.persistedTableState();
|
|
5112
|
-
this.initialMultiSortMeta =
|
|
5113
|
-
this.
|
|
5114
|
-
|
|
5115
|
-
this.tableDefaultMultiSortMeta();
|
|
5301
|
+
this.initialMultiSortMeta = this.views?.length
|
|
5302
|
+
? this.resolvedViewSort()
|
|
5303
|
+
: (this.sanitizedSortMeta(tableState?.multiSortMeta) ??
|
|
5304
|
+
this.tableDefaultMultiSortMeta());
|
|
5116
5305
|
this.alignPersistedTableStateSort(tableState);
|
|
5117
5306
|
}
|
|
5118
5307
|
/** A data column shows values for a `field`; anything else (the actions column) always renders. */
|
|
@@ -5494,17 +5683,23 @@ class SpiderlyDataTableComponent {
|
|
|
5494
5683
|
const stored = readStoredJson(this.filterStorage, this.viewSortStateKey);
|
|
5495
5684
|
return Array.isArray(stored) ? this.sanitizedSortMeta(stored) : null;
|
|
5496
5685
|
}
|
|
5497
|
-
/**
|
|
5498
|
-
|
|
5499
|
-
|
|
5686
|
+
/**
|
|
5687
|
+
* THE resolution ladder, named once: the operator's stored override, else the declared
|
|
5688
|
+
* default (`defaultMultiSortMeta` = view's declared sort ?? table default). Null means
|
|
5689
|
+
* "unsorted" — callers append their own `?? []` / blob terms rather than re-spelling the
|
|
5690
|
+
* ladder, so a future term (a per-view default refinement, decision 10's columns half)
|
|
5691
|
+
* lands everywhere at once.
|
|
5692
|
+
*/
|
|
5693
|
+
resolvedViewSort() {
|
|
5694
|
+
return this.persistedViewSort() ?? this.defaultMultiSortMeta();
|
|
5500
5695
|
}
|
|
5501
5696
|
/**
|
|
5502
5697
|
* Records the current sort as the active view's override — called from the EXPLICIT sort
|
|
5503
|
-
* gestures only (header click,
|
|
5504
|
-
* inherited at init must not crystallize into an override, or a
|
|
5505
|
-
* declared sort would stop flowing to operators who never
|
|
5506
|
-
* flows-through rule `columnVisibilityOverrides` documents. A sort
|
|
5507
|
-
* default is stored as ABSENCE for the same reason.
|
|
5698
|
+
* gestures only (header click, `applySort` — the menu and both bar entry points), never
|
|
5699
|
+
* from a load: a sort merely inherited at init must not crystallize into an override, or a
|
|
5700
|
+
* later change to the view's declared sort would stop flowing to operators who never
|
|
5701
|
+
* touched sort — the same flows-through rule `columnVisibilityOverrides` documents. A sort
|
|
5702
|
+
* equal to the declared default is stored as ABSENCE for the same reason.
|
|
5508
5703
|
*/
|
|
5509
5704
|
persistActiveViewSort() {
|
|
5510
5705
|
const key = this.viewSortStateKey;
|
|
@@ -5519,16 +5714,17 @@ class SpiderlyDataTableComponent {
|
|
|
5519
5714
|
}
|
|
5520
5715
|
}
|
|
5521
5716
|
/**
|
|
5522
|
-
* Puts the entering view's sort on the grid — stored override
|
|
5523
|
-
*
|
|
5524
|
-
*
|
|
5525
|
-
*
|
|
5526
|
-
*
|
|
5717
|
+
* Puts the entering view's sort on the grid — stored override, else declared sort, else the
|
|
5718
|
+
* table default, else UNSORTED. A view is a COMPLETE state, sort included (2026-09-06,
|
|
5719
|
+
* grilled — supersedes the "a view stating no preference keeps the ordering already there"
|
|
5720
|
+
* this first shipped with): the carried-over ordering read as a bug in its first live week,
|
|
5721
|
+
* when "Poslate"'s statusChangedAt rode onto "Sve", whose sorted column ships hidden —
|
|
5722
|
+
* read-only chip, no header to tri-state, no way out. Polaris IndexFilters is the model:
|
|
5723
|
+
* sort is part of a view's saved definition. Broadcast only — the caller owns the refetch,
|
|
5724
|
+
* because which request carries the new sort differs between the store and storeless paths.
|
|
5527
5725
|
*/
|
|
5528
5726
|
applyViewSort() {
|
|
5529
|
-
const target = this.
|
|
5530
|
-
if (!target)
|
|
5531
|
-
return false;
|
|
5727
|
+
const target = this.resolvedViewSort() ?? [];
|
|
5532
5728
|
if (sortMetaEquals(target, this.table._multiSortMeta ?? []))
|
|
5533
5729
|
return false;
|
|
5534
5730
|
this.broadcastSort(target);
|
|
@@ -5614,12 +5810,11 @@ class SpiderlyDataTableComponent {
|
|
|
5614
5810
|
return col == null || this.isColumnSortable(col);
|
|
5615
5811
|
});
|
|
5616
5812
|
}
|
|
5617
|
-
// Safety net: any lazy load still leaving unsorted (
|
|
5618
|
-
//
|
|
5619
|
-
//
|
|
5620
|
-
//
|
|
5621
|
-
//
|
|
5622
|
-
// substitutes the default at its source in setupRemovableSort().
|
|
5813
|
+
// Safety net: any lazy load still leaving unsorted (stale persisted state; a viewless
|
|
5814
|
+
// table with no sort memory yet) falls back to the declared default. Patching the event
|
|
5815
|
+
// before it becomes the backend Filter avoids a second fetch; the re-broadcast keeps the
|
|
5816
|
+
// header icons agreeing with what was actually sent. The tri-state un-sort path never
|
|
5817
|
+
// gets here — it substitutes the default at its source in setupRemovableSort().
|
|
5623
5818
|
applyDefaultSortIfUnsorted(event) {
|
|
5624
5819
|
if (event.multiSortMeta?.length)
|
|
5625
5820
|
return;
|
|
@@ -5790,8 +5985,8 @@ class SpiderlyDataTableComponent {
|
|
|
5790
5985
|
/**
|
|
5791
5986
|
* Switches to a view. Clears first, always: a view is a STATE rather than an addition, and two
|
|
5792
5987
|
* of them composed produce a question nobody asked. The clear runs through the store directly
|
|
5793
|
-
* rather than through `clear(
|
|
5794
|
-
* view
|
|
5988
|
+
* rather than through `clear()`, whose requery effect would spend a fetch before the entering
|
|
5989
|
+
* view's filters are in place.
|
|
5795
5990
|
*/
|
|
5796
5991
|
selectView(view) {
|
|
5797
5992
|
this.activeViewId = view.id;
|
|
@@ -5872,9 +6067,7 @@ class SpiderlyDataTableComponent {
|
|
|
5872
6067
|
this.columnMenu().hide();
|
|
5873
6068
|
if (!col?.field || !this.isColumnSortable(col))
|
|
5874
6069
|
return;
|
|
5875
|
-
this.
|
|
5876
|
-
this.table.sortMultiple();
|
|
5877
|
-
this.persistActiveViewSort();
|
|
6070
|
+
this.applySort([{ field: col.field, order }]);
|
|
5878
6071
|
}
|
|
5879
6072
|
/**
|
|
5880
6073
|
* Sizes the column to the widest thing in it. The cheaper escape from a cramped column: no
|
|
@@ -5984,10 +6177,50 @@ class SpiderlyDataTableComponent {
|
|
|
5984
6177
|
* left falls back to its own name rather than vanishing.
|
|
5985
6178
|
*/
|
|
5986
6179
|
refreshSortKeys() {
|
|
5987
|
-
|
|
5988
|
-
|
|
6180
|
+
const meta = this.lastLazyLoadEvent?.multiSortMeta ?? [];
|
|
6181
|
+
this.sortKeys = meta.map((key) => ({
|
|
6182
|
+
field: key.field,
|
|
6183
|
+
// A sort may name a field no column declares (keepSortableMeta allows it), and its chip
|
|
6184
|
+
// must still speak the operator's language: the same PascalCase-key convention the
|
|
6185
|
+
// scaffold seeds resolves it (Transloco falls back to the key, so worst case reads
|
|
6186
|
+
// "CreatedAt", never the raw camelCase identifier).
|
|
6187
|
+
label: this.cols.find((col) => col.field === key.field)?.name ??
|
|
6188
|
+
this.translocoService.translate(firstCharToUpper(key.field)),
|
|
5989
6189
|
descending: key.order === -1,
|
|
5990
6190
|
}));
|
|
6191
|
+
this.sortIsDefault = sortMetaEquals(meta, this.defaultMultiSortMeta() ?? []);
|
|
6192
|
+
}
|
|
6193
|
+
/**
|
|
6194
|
+
* The one body of an explicit sort GESTURE — the thing `persistActiveViewSort`'s doc calls
|
|
6195
|
+
* "explicit gestures only": page one, meta on the grid, fetch, save, selection anchor
|
|
6196
|
+
* invalidated, override recorded. Shared by the header's single-click un-sort, the header
|
|
6197
|
+
* menu and both bar entry points; `broadcastSort` is its deliberate no-fetch sibling. The
|
|
6198
|
+
* page reset, `saveState` and anchor line mirror PrimeNG's own `sort()` (`sortMultiple()`
|
|
6199
|
+
* does none of the three); the "lands on page one" / "survives a reload" specs pin why.
|
|
6200
|
+
*/
|
|
6201
|
+
applySort(meta) {
|
|
6202
|
+
if (this.table.resetPageOnSort) {
|
|
6203
|
+
this.table._first = 0;
|
|
6204
|
+
this.table.firstChange.emit(0);
|
|
6205
|
+
}
|
|
6206
|
+
this.table._multiSortMeta = meta;
|
|
6207
|
+
this.table.sortMultiple();
|
|
6208
|
+
if (this.table.isStateful())
|
|
6209
|
+
this.table.saveState();
|
|
6210
|
+
this.table.anchorRowIndex = null;
|
|
6211
|
+
this.persistActiveViewSort();
|
|
6212
|
+
}
|
|
6213
|
+
/** The picker's ask: one sort, replacing the whole meta — the same contract as the header menu. */
|
|
6214
|
+
sortFromBar(pick) {
|
|
6215
|
+
this.applySort([pick]);
|
|
6216
|
+
}
|
|
6217
|
+
/**
|
|
6218
|
+
* Back to the resolved default — the view's declared sort, else the table default, else
|
|
6219
|
+
* unsorted. Persisting right after is what empties the override slot: a sort equal to the
|
|
6220
|
+
* default is stored as absence, so the declared ordering flows through again.
|
|
6221
|
+
*/
|
|
6222
|
+
resetSortFromBar() {
|
|
6223
|
+
this.applySort(this.defaultMultiSortMeta() ?? []);
|
|
5991
6224
|
}
|
|
5992
6225
|
/** This column's share, override first, then its declared width, then the per-type default. */
|
|
5993
6226
|
columnShare(col) {
|
|
@@ -6369,18 +6602,17 @@ class SpiderlyDataTableComponent {
|
|
|
6369
6602
|
exportListToExcel(this.exportListToExcelObservableMethod, filter);
|
|
6370
6603
|
}
|
|
6371
6604
|
/**
|
|
6372
|
-
* Clears
|
|
6373
|
-
*
|
|
6374
|
-
*
|
|
6605
|
+
* Clears the FILTERS — and nothing else (2026-09-06, grilled). It used to also reach the
|
|
6606
|
+
* sort and PrimeNG's persisted state, which made "Clear filters" a reset the label never
|
|
6607
|
+
* claimed: sort is its own channel with its own affordances (the bar's picker, the header's
|
|
6608
|
+
* tri-state), mirroring Polaris, where onClearAll is wired to the Filters while
|
|
6609
|
+
* sortSelected never passes through it. The store's requery effect owns the refetch and the
|
|
6610
|
+
* persisted-snapshot update — the button only renders with chips on the bar, so the applied
|
|
6611
|
+
* set always changes — and dropping table.clear() also drops the second, unsequenced
|
|
6612
|
+
* request the 2026-09-05 review flagged on every Clear.
|
|
6375
6613
|
*/
|
|
6376
|
-
clear(
|
|
6614
|
+
clear() {
|
|
6377
6615
|
this.filters?.clear();
|
|
6378
|
-
table.clear();
|
|
6379
|
-
table.clearState();
|
|
6380
|
-
// `table.clear()` landed the grid on the declared default (`applyDefaultSortIfUnsorted`),
|
|
6381
|
-
// so the view's sort override must go with it — or F5 would bring back the sort Clear
|
|
6382
|
-
// visibly removed.
|
|
6383
|
-
this.persistActiveViewSort();
|
|
6384
6616
|
}
|
|
6385
6617
|
//#region Selection
|
|
6386
6618
|
setFakeIsAllSelected() {
|
|
@@ -6554,7 +6786,7 @@ class SpiderlyDataTableComponent {
|
|
|
6554
6786
|
}
|
|
6555
6787
|
}
|
|
6556
6788
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyDataTableComponent, deps: [{ token: i3$2.Router }, { token: i1$5.DialogService }, { token: i3$2.ActivatedRoute }, { token: SpiderlyMessageService }, { token: i1.TranslocoService }, { token: ConfigServiceBase }, { token: i0.ErrorHandler }, { token: LOCALE_ID }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6557
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.2.13", type: SpiderlyDataTableComponent, isStandalone: true, selector: "spiderly-data-table", inputs: { tableTitle: "tableTitle", tableIcon: "tableIcon", items: "items", rows: "rows", rowsPerPageOptions: "rowsPerPageOptions", cols: "cols", filters: "filters", views: "views", showPaginator: "showPaginator", showCardWrapper: "showCardWrapper", readonly: "readonly", idField: "idField", getPaginatedListObservableMethod: "getPaginatedListObservableMethod", exportListToExcelObservableMethod: "exportListToExcelObservableMethod", deleteItemFromTableObservableMethod: "deleteItemFromTableObservableMethod", deleteListFromTableObservableMethod: "deleteListFromTableObservableMethod", newlySelectedItems: "newlySelectedItems", unselectedItems: "unselectedItems", selectionMode: "selectionMode", selectedLazyLoadObservableMethod: "selectedLazyLoadObservableMethod", additionalFilterIdLong: "additionalFilterIdLong", showAddButton: "showAddButton", showExportToExcelButton: "showExportToExcelButton", showReloadTableButton: "showReloadTableButton", getFormArrayItems: "getFormArrayItems", hasLazyLoad: "hasLazyLoad", stateKey: "stateKey", stateStorage: "stateStorage", defaultSortField: "defaultSortField", defaultSortOrder: "defaultSortOrder", getAlreadySelectedItemIds: "getAlreadySelectedItemIds", getAlreadySelectedItems: "getAlreadySelectedItems", getFormControl: "getFormControl", additionalIndexes: "additionalIndexes", navigateOnRowClick: "navigateOnRowClick", rowNavigationPath: "rowNavigationPath" }, outputs: { onTotalRecordsChange: "onTotalRecordsChange", onLazyLoad: "onLazyLoad", onIsAllSelectedChange: "onIsAllSelectedChange", onRowSelect: "onRowSelect", onRowUnselect: "onRowUnselect" }, queries: [{ propertyName: "actionsTemplate", first: true, predicate: SpiderlyDataTableActionsDirective, descendants: true, read: TemplateRef }, { propertyName: "cellTemplates", predicate: SpiderlyCellTemplateDirective }, { propertyName: "filterTemplates", predicate: SpiderlyFilterTemplateDirective }], viewQueries: [{ propertyName: "columnMenu", first: true, predicate: ["columnMenu"], descendants: true, isSignal: true }, { propertyName: "filterBar", first: true, predicate: SpiderlyFilterBarComponent, descendants: true, isSignal: true }, { propertyName: "selectionHeader", first: true, predicate: ["selectionHeader"], descendants: true, isSignal: true }, { propertyName: "table", first: true, predicate: ["dt"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div\n #tableContainer\n class=\"spiderly-table-container\"\n [attr.aria-busy]=\"isPending\"\n [ngClass]=\"{\n 'card responsive-card-padding overflow-auto': showCardWrapper\n }\"\n >\n <p-table\n #dt\n [value]=\"items\"\n [rows]=\"rows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [rowHover]=\"true\"\n [paginator]=\"showPaginator\"\n responsiveLayout=\"scroll\"\n [lazy]=\"hasLazyLoad\"\n (onLazyLoad)=\"lazyLoad($event)\"\n (onPage)=\"onPageChange(tableContainer)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"isPending\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n sortMode=\"multiple\"\n [multiSortMeta]=\"initialMultiSortMeta\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\n [tableStyle]=\"tableStyle\"\n >\n <ng-template pTemplate=\"caption\">\n <div class=\"table-header overflow-auto\">\n <div style=\"display: flex; align-items: center; gap: 8px\">\n <i class=\"{{ tableIcon }}\" style=\"font-size: 20px\"></i>\n <div style=\"margin: 0px; font-size: 17.5px\">{{ tableTitle }}</div>\n </div>\n <div style=\"display: flex; gap: 8px\">\n <ng-container\n *ngIf=\"actionsTemplate\"\n [ngTemplateOutlet]=\"actionsTemplate\"\n ></ng-container>\n <button\n pButton\n *ngIf=\"hasLazyLoad\"\n data-testid=\"column-chooser-button\"\n [label]=\"t('Columns')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-sliders-h\"\n (click)=\"columnChooser.toggle($event)\"\n ></button>\n <button\n pButton\n *ngIf=\"showExportToExcelButton\"\n [label]=\"t('ExportToExcel')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-download\"\n (click)=\"exportListToExcel()\"\n ></button>\n <button\n pButton\n *ngIf=\"showReloadTableButton\"\n [label]=\"t('Reload')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-refresh\"\n (click)=\"reload()\"\n ></button>\n <button\n pButton\n *ngIf=\"deleteListFromTableObservableMethod && rowsSelectedNumber > 0\"\n [label]=\"t('DeleteSelected') + ' (' + rowsSelectedNumber + ')'\"\n class=\"p-button-danger\"\n style=\"flex: none\"\n icon=\"pi pi-trash\"\n (click)=\"deleteSelectedObjects()\"\n ></button>\n </div>\n </div>\n <!-- ONE menu for every column, with the target held in `menuColumn`. Rendering its items\n inside an @if keyed on the column would put them inside the popover's projected\n content, and PrimeNG does not re-project content an @if has destroyed \u2014 the filter\n bar's editor lost a whole afternoon to exactly that. -->\n <p-popover #columnMenu>\n <div class=\"column-menu\" role=\"menu\">\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-hide\"\n [disabled]=\"!menuColumn || !canToggleColumn(menuColumn)\"\n (click)=\"hideMenuColumn()\"\n >\n <i class=\"pi pi-eye-slash\"></i>\n <span class=\"column-menu-item-label\">{{ t('HideColumn') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-filter\"\n [disabled]=\"!menuColumn || !canFilterColumn(menuColumn)\"\n (click)=\"filterMenuColumn()\"\n >\n <i class=\"pi pi-filter\"></i>\n <span class=\"column-menu-item-label\">{{ t('Filter') }}\u2026</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-asc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(1)\"\n >\n <i class=\"pi pi-sort-amount-up-alt\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortAscending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-desc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(-1)\"\n >\n <i class=\"pi pi-sort-amount-down\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortDescending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-left\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, -1)\"\n (click)=\"moveMenuColumn(-1)\"\n >\n <i class=\"pi pi-arrow-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveLeft') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-right\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, 1)\"\n (click)=\"moveMenuColumn(1)\"\n >\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveRight') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-fit\"\n (click)=\"fitMenuColumn()\"\n >\n <i class=\"pi pi-arrows-h\"></i>\n <span class=\"column-menu-item-label\">{{ t('FitWidth') }}</span>\n </button>\n <!-- menuitemcheckbox, not menuitem: this one TOGGLES, and a label reading the same\n in both states leaves the operator to find out by clicking \u2014 which on a wide\n column means re-laying the grid out to learn what it was already set to. The\n check is the state; the label stays put, which is what the role is for.\n Disabled for an editable column: its cell holds an input, not clamped text, so\n the toggle could only persist a preference that changes nothing. -->\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-wrap\"\n [disabled]=\"!menuColumn || menuColumn.editable === true\"\n [attr.aria-checked]=\"isMenuColumnWrapped\"\n (click)=\"toggleWrapForMenuColumn()\"\n >\n <i class=\"pi pi-align-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('WrapText') }}</span>\n <i *ngIf=\"isMenuColumnWrapped\" class=\"pi pi-check\"></i>\n </button>\n </div>\n </p-popover>\n\n <div class=\"table-views\" *ngIf=\"views?.length\" role=\"tablist\">\n <button\n *ngFor=\"let view of views\"\n type=\"button\"\n role=\"tab\"\n class=\"table-view\"\n data-testid=\"table-view\"\n [class.table-view-active]=\"activeViewId === view.id\"\n [attr.aria-selected]=\"activeViewId === view.id\"\n (click)=\"selectView(view)\"\n >\n {{ view.label }}\n </button>\n </div>\n\n <spiderly-filter-bar\n *ngIf=\"filters\"\n [filters]=\"filters\"\n [totalRecords]=\"totalRecords\"\n [sort]=\"sortKeys\"\n [controlTemplates]=\"filterTemplates\"\n (clearAll)=\"clear(dt)\"\n ></spiderly-filter-bar>\n </ng-template>\n <ng-template pTemplate=\"header\">\n <tr>\n <!-- Sized for the checkbox plus the counter beside it, which shows `totalRecords` on a\n select-all and so reaches five digits on a large table. Not `0rem` (see CLAUDE.md \u2192\n Column widths), and not routed through getColWidth: this column has no `Column`. -->\n <th\n #selectionHeader\n class=\"frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.width]=\"selectionColumnWidth\"\n [style.left]=\"'0'\"\n *ngIf=\"selectionMode == 'multiple'\"\n >\n <div style=\"display: flex; gap: 8px\">\n <p-checkbox\n *ngIf=\"showSelectAllCheckbox\"\n [disabled]=\"readonly\"\n (onChange)=\"selectAll($event.checked)\"\n [(ngModel)]=\"fakeIsAllSelected\"\n [binary]=\"true\"\n ></p-checkbox>\n <span class=\"selection-count\">({{ rowsSelectedNumber }})</span>\n </div>\n </th>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <th\n #headerCell\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"!isColumnSortable(col)\"\n [style.width]=\"getColWidth(col)\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n [attr.draggable]=\"hasLazyLoad && !col.lockVisible ? true : null\"\n (mousedown)=\"onHeaderMouseDown($event)\"\n (dragstart)=\"onHeaderDragStart(col, $event)\"\n (dragover)=\"onHeaderDragOver(col, $event)\"\n (drop)=\"onHeaderDrop(col, $event)\"\n >\n <!-- Ours, not PrimeNG's: enabling [resizableColumns] makes it persist widths by\n th:nth-child INDEX into our own stateKey, which slides every stored width onto\n the wrong column the first time the chooser hides one (CLAUDE.md -> decision\n 5). We take the gesture and keep the state. -->\n <span\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n class=\"column-resizer\"\n data-testid=\"column-resizer\"\n (mousedown)=\"startColumnResize(col, $event, headerCell)\"\n ></span>\n <div\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n \"\n >\n <span style=\"display: flex; align-items: center; gap: 4px\">\n {{ col.name }}\n <p-sortIcon\n *ngIf=\"isColumnSortable(col)\"\n [field]=\"col.field!\"\n ></p-sortIcon>\n <!-- stopPropagation is load-bearing: the th carries pSortableColumn, so a click\n that reaches it sorts the grid. Opening a menu must not reorder the table on\n the way. -->\n <button\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n type=\"button\"\n class=\"column-menu-button\"\n data-testid=\"column-menu\"\n [attr.aria-label]=\"t('Columns')\"\n (click)=\"$event.stopPropagation(); openColumnMenu(col, $event)\"\n >\n <i class=\"pi pi-chevron-down\"></i>\n </button>\n </span>\n </div>\n </th>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template\n pTemplate=\"body\"\n let-rowData\n let-index=\"rowIndex\"\n let-editing=\"editing\"\n >\n <!-- The cell rendering every column gets unless it projects a spiderlyCellTemplate. Declared\n once here rather than inside the column loop, and fed the same context the projected\n template gets, so getRowData is evaluated exactly once per cell either way. -->\n <ng-template #defaultCell let-col=\"col\" let-displayValue=\"displayValue\">\n <ng-container *ngIf=\"col.filterType === 'blob'\">\n <img width=\"45\" [src]=\"displayValue\" alt=\"\" />\n </ng-container>\n <ng-container *ngIf=\"col.filterType !== 'blob'\">\n <span spiderlyOverflowTitle class=\"cell-text\">{{\n displayValue\n }}</span>\n </ng-container>\n </ng-template>\n <tr\n [class.clickable]=\"navigateOnRowClick\"\n (click)=\"onRowClick(rowData, $event)\"\n >\n <!-- row-interactive: a selection gesture must never double as row navigation (onRowClick). -->\n <td\n *ngIf=\"selectionMode == 'multiple'\"\n class=\"selection-cell row-interactive frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.left]=\"'0'\"\n (mousedown)=\"onSelectionCellMouseDown($event, rowData[idField])\"\n >\n <p-checkbox\n [disabled]=\"readonly\"\n (onChange)=\"selectRow(rowData[idField], rowData.index)\"\n [ngModel]=\"isRowSelected(rowData[idField])\"\n [binary]=\"true\"\n ></p-checkbox>\n </td>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <td\n [style]=\"getStyleForBodyColumn(col)\"\n [class.clickable]=\"col.onCellClick\"\n [class.row-interactive]=\"col.onCellClick\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [class.cell-wrap]=\"isColumnWrapped(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n (click)=\"onCellClick(col, rowData, $event)\"\n *ngIf=\"!col.editable\"\n >\n <!-- getColWidth reserves this column's width from `actions.length`; this gap is\n the geometry it reserves for, so changing it belongs in the same edit. -->\n <div\n style=\"\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 18px;\n \"\n >\n <ng-container\n *ngFor=\"let action of col.actions; trackBy: actionTrackByFn\"\n >\n <span\n [pTooltip]=\"action.name\"\n [class]=\"getClassForAction(action) + ' row-interactive'\"\n [style]=\"getStyleForAction(action)\"\n (click)=\"getMethodForAction(action, rowData, $event)\"\n ></span>\n </ng-container>\n </div>\n <ng-container\n [ngTemplateOutlet]=\"getCellTemplate(col) ?? defaultCell\"\n [ngTemplateOutletContext]=\"{\n $implicit: rowData,\n value: rowData[col.field],\n displayValue: getRowData(rowData, col),\n col: col\n }\"\n ></ng-container>\n </td>\n <td\n *ngIf=\"col.editable\"\n class=\"row-interactive\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n >\n <spiderly-number\n [control]=\"getFormArrayControlByIndex(col.field, rowData.index)\"\n [showLabel]=\"false\"\n ></spiderly-number>\n </td>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n <tr>\n <td\n [attr.colspan]=\"\n visibleCols.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"NoRecordsFound\") }}\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template pTemplate=\"paginatorright\">\n <div style=\"display: flex; justify-content: end; gap: 10px\">\n <spiderly-button\n *ngIf=\"showAddButton\"\n [label]=\"t('AddNew')\"\n icon=\"pi pi-plus\"\n (onClick)=\"navigateToDetails(0)\"\n ></spiderly-button>\n </div>\n </ng-template>\n </p-table>\n <p-popover #columnChooser>\n <div class=\"column-chooser\">\n <label\n *ngFor=\"let col of chooserCols; trackBy: colTrackByFn\"\n data-testid=\"column-chooser-option\"\n class=\"column-chooser-option\"\n >\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isColumnVisible(col)\"\n [disabled]=\"!canToggleColumn(col)\"\n (onChange)=\"toggleColumn(col, $event.checked)\"\n ></p-checkbox>\n <span>{{ col.name }}</span>\n </label>\n <button\n type=\"button\"\n data-testid=\"column-chooser-reset\"\n class=\"column-chooser-reset\"\n (click)=\"resetColumnLayout()\"\n >\n {{ t('ResetToDefault') }}\n </button>\n </div>\n </p-popover>\n </div>\n</ng-container>\n", styles: [".table-header{display:flex;justify-content:space-between;align-items:center}@media (max-width: 640px){.table-header{display:flex;flex-direction:column;align-items:start;gap:14px}}.spiderly-table .p-paginator{padding:1rem}@media (min-width: 1400px){.spiderly-table .p-paginator-left-content{position:absolute;padding:14px;left:0}}@media (min-width: 1400px){.spiderly-table .p-paginator-right-content{position:absolute;padding:14px;right:0}}.column-chooser{display:flex;flex-direction:column;gap:8px}.column-chooser-option{display:flex;align-items:center;gap:8px;cursor:pointer}.column-chooser-reset{border:none;background:none;color:inherit;font:inherit;cursor:pointer;padding:0;margin-top:4px;color:var(--p-primary-color);align-self:flex-start;font-size:.875rem}.column-chooser-reset:hover{text-decoration:underline}.column-menu{display:flex;flex-direction:column;min-width:11rem}.column-menu-item{display:flex;align-items:center;gap:8px;border:none;background:none;color:inherit;font:inherit;cursor:pointer;text-align:left;padding:6px 10px;border-radius:var(--p-content-border-radius)}.column-menu-item .column-menu-item-label{flex:1}.column-menu-item:hover:not(:disabled){background:var(--p-content-hover-background)}.column-menu-item:disabled{color:var(--p-text-muted-color);cursor:default}:host .spiderly-table-container{scroll-margin-top:calc(var(--spiderly-viewport-top-inset, 0px) + 1rem)}:host ::ng-deep .clickable{cursor:pointer}:host ::ng-deep td.clickable:hover{text-decoration:underline}:host .column-menu-button{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:0 2px;opacity:0;font-size:.75rem}:host th:hover .column-menu-button,:host .column-menu-button:focus-visible{opacity:1}:host .table-views{display:flex;flex-wrap:wrap;gap:4px;padding-top:8px}:host .table-view{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:4px 10px;border-radius:var(--p-content-border-radius)}:host .table-view:hover{background:var(--p-content-hover-background)}:host .table-view-active{color:var(--p-primary-color);font-weight:600;background:var(--p-content-hover-background)}:host .frozen-column{position:sticky;z-index:1}:host td.frozen-column{background:var(--p-datatable-row-background, var(--p-content-background));transition:background-color var(--p-transition-duration, .2s)}:host tr:hover>td.frozen-column{background:var(--p-datatable-row-hover-background, inherit)}:host .frozen-column-last{box-shadow:2px 0 4px -2px #00000026}:host th{position:relative}:host .column-resizer{position:absolute;top:0;right:0;width:8px;height:100%;cursor:col-resize;-webkit-user-select:none;user-select:none}:host .selection-count{font-size:.75rem;color:var(--p-text-muted-color)}:host td.selection-cell{-webkit-user-select:none;user-select:none}:host td.cell-wrap{--spiderly-cell-overflow: visible;--spiderly-cell-white-space: normal}:host .cell-text{display:block;overflow:var(--spiderly-cell-overflow, hidden);text-overflow:ellipsis;white-space:var(--spiderly-cell-white-space, nowrap)}:host ::ng-deep .p-datatable-mask{background:color-mix(in srgb,var(--p-content-background, #ffffff) 65%,transparent);color:var(--p-primary-color, #334155);align-items:flex-start;padding-top:6rem}:host ::ng-deep .p-datatable-thead{position:unset!important}:host ::ng-deep .p-datatable-header{border-radius:6px 6px 0 0}:host ::ng-deep .p-datatable{border-radius:var(--p-content-border-radius);border:1px solid var(--p-datatable-border-color)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: SpiderlyControlsModule }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "component", type: SpiderlyNumberComponent, selector: "spiderly-number", inputs: ["prefix", "showButtons", "decimal", "maxFractionDigits"] }, { kind: "ngmodule", type: TableModule }, { kind: "component", type: i10.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "style", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "scrollDirection", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "responsive", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "autoLayout", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "virtualRowHeight", "selectAll"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "directive", type: i1$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "directive", type: i10.SortableColumn, selector: "[pSortableColumn]", inputs: ["pSortableColumn", "pSortableColumnDisabled"] }, { kind: "component", type: i10.SortIcon, selector: "p-sortIcon", inputs: ["field"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3$3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "fluid", "label", "icon", "buttonProps"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i8.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["value", "name", "disabled", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "inputStyle", "styleClass", "inputClass", "indeterminate", "size", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "component", type: SpiderlyFilterBarComponent, selector: "spiderly-filter-bar", inputs: ["filters", "totalRecords", "sort", "controlTemplates"], outputs: ["clearAll"] }, { kind: "directive", type: SpiderlyOverflowTitleDirective, selector: "[spiderlyOverflowTitle]" }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }] }); }
|
|
6789
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.2.13", type: SpiderlyDataTableComponent, isStandalone: true, selector: "spiderly-data-table", inputs: { tableTitle: "tableTitle", tableIcon: "tableIcon", items: "items", rows: "rows", rowsPerPageOptions: "rowsPerPageOptions", cols: "cols", filters: "filters", views: "views", showPaginator: "showPaginator", showCardWrapper: "showCardWrapper", readonly: "readonly", idField: "idField", getPaginatedListObservableMethod: "getPaginatedListObservableMethod", exportListToExcelObservableMethod: "exportListToExcelObservableMethod", deleteItemFromTableObservableMethod: "deleteItemFromTableObservableMethod", deleteListFromTableObservableMethod: "deleteListFromTableObservableMethod", newlySelectedItems: "newlySelectedItems", unselectedItems: "unselectedItems", selectionMode: "selectionMode", selectedLazyLoadObservableMethod: "selectedLazyLoadObservableMethod", additionalFilterIdLong: "additionalFilterIdLong", showAddButton: "showAddButton", showExportToExcelButton: "showExportToExcelButton", showReloadTableButton: "showReloadTableButton", getFormArrayItems: "getFormArrayItems", hasLazyLoad: "hasLazyLoad", stateKey: "stateKey", stateStorage: "stateStorage", defaultSortField: "defaultSortField", defaultSortOrder: "defaultSortOrder", getAlreadySelectedItemIds: "getAlreadySelectedItemIds", getAlreadySelectedItems: "getAlreadySelectedItems", getFormControl: "getFormControl", additionalIndexes: "additionalIndexes", navigateOnRowClick: "navigateOnRowClick", rowNavigationPath: "rowNavigationPath" }, outputs: { onTotalRecordsChange: "onTotalRecordsChange", onLazyLoad: "onLazyLoad", onIsAllSelectedChange: "onIsAllSelectedChange", onRowSelect: "onRowSelect", onRowUnselect: "onRowUnselect" }, queries: [{ propertyName: "actionsTemplate", first: true, predicate: SpiderlyDataTableActionsDirective, descendants: true, read: TemplateRef }, { propertyName: "cellTemplates", predicate: SpiderlyCellTemplateDirective }, { propertyName: "filterTemplates", predicate: SpiderlyFilterTemplateDirective }], viewQueries: [{ propertyName: "columnMenu", first: true, predicate: ["columnMenu"], descendants: true, isSignal: true }, { propertyName: "filterBar", first: true, predicate: SpiderlyFilterBarComponent, descendants: true, isSignal: true }, { propertyName: "selectionHeader", first: true, predicate: ["selectionHeader"], descendants: true, isSignal: true }, { propertyName: "table", first: true, predicate: ["dt"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div\n #tableContainer\n class=\"spiderly-table-container\"\n [attr.aria-busy]=\"isPending\"\n [ngClass]=\"{\n 'card responsive-card-padding overflow-auto': showCardWrapper\n }\"\n >\n <p-table\n #dt\n [value]=\"items\"\n [rows]=\"rows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [rowHover]=\"true\"\n [paginator]=\"showPaginator\"\n responsiveLayout=\"scroll\"\n [lazy]=\"hasLazyLoad\"\n (onLazyLoad)=\"lazyLoad($event)\"\n (onPage)=\"onPageChange(tableContainer)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"isPending\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n sortMode=\"multiple\"\n [multiSortMeta]=\"initialMultiSortMeta\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\n [tableStyle]=\"tableStyle\"\n >\n <ng-template pTemplate=\"caption\">\n <div class=\"table-header overflow-auto\">\n <div style=\"display: flex; align-items: center; gap: 8px\">\n <i class=\"{{ tableIcon }}\" style=\"font-size: 20px\"></i>\n <div style=\"margin: 0px; font-size: 17.5px\">{{ tableTitle }}</div>\n </div>\n <div style=\"display: flex; gap: 8px\">\n <ng-container\n *ngIf=\"actionsTemplate\"\n [ngTemplateOutlet]=\"actionsTemplate\"\n ></ng-container>\n <button\n pButton\n *ngIf=\"hasLazyLoad\"\n data-testid=\"column-chooser-button\"\n [label]=\"t('Columns')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-sliders-h\"\n (click)=\"columnChooser.toggle($event)\"\n ></button>\n <button\n pButton\n *ngIf=\"showExportToExcelButton\"\n [label]=\"t('ExportToExcel')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-download\"\n (click)=\"exportListToExcel()\"\n ></button>\n <button\n pButton\n *ngIf=\"showReloadTableButton\"\n [label]=\"t('Reload')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-refresh\"\n (click)=\"reload()\"\n ></button>\n <button\n pButton\n *ngIf=\"deleteListFromTableObservableMethod && rowsSelectedNumber > 0\"\n [label]=\"t('DeleteSelected') + ' (' + rowsSelectedNumber + ')'\"\n class=\"p-button-danger\"\n style=\"flex: none\"\n icon=\"pi pi-trash\"\n (click)=\"deleteSelectedObjects()\"\n ></button>\n </div>\n </div>\n <!-- ONE menu for every column, with the target held in `menuColumn`. Rendering its items\n inside an @if keyed on the column would put them inside the popover's projected\n content, and PrimeNG does not re-project content an @if has destroyed \u2014 the filter\n bar's editor lost a whole afternoon to exactly that. -->\n <p-popover #columnMenu>\n <div class=\"column-menu\" role=\"menu\">\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-hide\"\n [disabled]=\"!menuColumn || !canToggleColumn(menuColumn)\"\n (click)=\"hideMenuColumn()\"\n >\n <i class=\"pi pi-eye-slash\"></i>\n <span class=\"column-menu-item-label\">{{ t('HideColumn') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-filter\"\n [disabled]=\"!menuColumn || !canFilterColumn(menuColumn)\"\n (click)=\"filterMenuColumn()\"\n >\n <i class=\"pi pi-filter\"></i>\n <span class=\"column-menu-item-label\">{{ t('Filter') }}\u2026</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-asc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(1)\"\n >\n <i class=\"pi pi-sort-amount-up-alt\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortAscending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-desc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(-1)\"\n >\n <i class=\"pi pi-sort-amount-down\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortDescending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-left\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, -1)\"\n (click)=\"moveMenuColumn(-1)\"\n >\n <i class=\"pi pi-arrow-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveLeft') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-right\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, 1)\"\n (click)=\"moveMenuColumn(1)\"\n >\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveRight') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-fit\"\n (click)=\"fitMenuColumn()\"\n >\n <i class=\"pi pi-arrows-h\"></i>\n <span class=\"column-menu-item-label\">{{ t('FitWidth') }}</span>\n </button>\n <!-- menuitemcheckbox, not menuitem: this one TOGGLES, and a label reading the same\n in both states leaves the operator to find out by clicking \u2014 which on a wide\n column means re-laying the grid out to learn what it was already set to. The\n check is the state; the label stays put, which is what the role is for.\n Disabled for an editable column: its cell holds an input, not clamped text, so\n the toggle could only persist a preference that changes nothing. -->\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-wrap\"\n [disabled]=\"!menuColumn || menuColumn.editable === true\"\n [attr.aria-checked]=\"isMenuColumnWrapped\"\n (click)=\"toggleWrapForMenuColumn()\"\n >\n <i class=\"pi pi-align-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('WrapText') }}</span>\n <i *ngIf=\"isMenuColumnWrapped\" class=\"pi pi-check\"></i>\n </button>\n </div>\n </p-popover>\n\n <div class=\"table-views\" *ngIf=\"views?.length\" role=\"tablist\">\n <button\n *ngFor=\"let view of views\"\n type=\"button\"\n role=\"tab\"\n class=\"table-view\"\n data-testid=\"table-view\"\n [class.table-view-active]=\"activeViewId === view.id\"\n [attr.aria-selected]=\"activeViewId === view.id\"\n (click)=\"selectView(view)\"\n >\n {{ view.label }}\n </button>\n </div>\n\n <spiderly-filter-bar\n *ngIf=\"filters\"\n [filters]=\"filters\"\n [totalRecords]=\"totalRecords\"\n [sort]=\"sortKeys\"\n [sortOptions]=\"sortPickOptions\"\n [sortIsDefault]=\"sortIsDefault\"\n [controlTemplates]=\"filterTemplates\"\n (clearAll)=\"clear()\"\n (sortPick)=\"sortFromBar($event)\"\n (sortReset)=\"resetSortFromBar()\"\n ></spiderly-filter-bar>\n </ng-template>\n <ng-template pTemplate=\"header\">\n <tr>\n <!-- Sized for the checkbox plus the counter beside it, which shows `totalRecords` on a\n select-all and so reaches five digits on a large table. Not `0rem` (see CLAUDE.md \u2192\n Column widths), and not routed through getColWidth: this column has no `Column`. -->\n <th\n #selectionHeader\n class=\"frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.width]=\"selectionColumnWidth\"\n [style.left]=\"'0'\"\n *ngIf=\"selectionMode == 'multiple'\"\n >\n <div style=\"display: flex; gap: 8px\">\n <p-checkbox\n *ngIf=\"showSelectAllCheckbox\"\n [disabled]=\"readonly\"\n (onChange)=\"selectAll($event.checked)\"\n [(ngModel)]=\"fakeIsAllSelected\"\n [binary]=\"true\"\n ></p-checkbox>\n <span class=\"selection-count\">({{ rowsSelectedNumber }})</span>\n </div>\n </th>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <th\n #headerCell\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"!isColumnSortable(col)\"\n [style.width]=\"getColWidth(col)\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n [attr.draggable]=\"hasLazyLoad && !col.lockVisible ? true : null\"\n (mousedown)=\"onHeaderMouseDown($event)\"\n (dragstart)=\"onHeaderDragStart(col, $event)\"\n (dragover)=\"onHeaderDragOver(col, $event)\"\n (drop)=\"onHeaderDrop(col, $event)\"\n >\n <!-- Ours, not PrimeNG's: enabling [resizableColumns] makes it persist widths by\n th:nth-child INDEX into our own stateKey, which slides every stored width onto\n the wrong column the first time the chooser hides one (CLAUDE.md -> decision\n 5). We take the gesture and keep the state. -->\n <span\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n class=\"column-resizer\"\n data-testid=\"column-resizer\"\n (mousedown)=\"startColumnResize(col, $event, headerCell)\"\n ></span>\n <div\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n \"\n >\n <span style=\"display: flex; align-items: center; gap: 4px\">\n {{ col.name }}\n <p-sortIcon\n *ngIf=\"isColumnSortable(col)\"\n [field]=\"col.field!\"\n ></p-sortIcon>\n <!-- stopPropagation is load-bearing: the th carries pSortableColumn, so a click\n that reaches it sorts the grid. Opening a menu must not reorder the table on\n the way. -->\n <button\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n type=\"button\"\n class=\"column-menu-button\"\n data-testid=\"column-menu\"\n [attr.aria-label]=\"t('Columns')\"\n (click)=\"$event.stopPropagation(); openColumnMenu(col, $event)\"\n >\n <i class=\"pi pi-chevron-down\"></i>\n </button>\n </span>\n </div>\n </th>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template\n pTemplate=\"body\"\n let-rowData\n let-index=\"rowIndex\"\n let-editing=\"editing\"\n >\n <!-- The cell rendering every column gets unless it projects a spiderlyCellTemplate. Declared\n once here rather than inside the column loop, and fed the same context the projected\n template gets, so getRowData is evaluated exactly once per cell either way. -->\n <ng-template #defaultCell let-col=\"col\" let-displayValue=\"displayValue\">\n <ng-container *ngIf=\"col.filterType === 'blob'\">\n <img width=\"45\" [src]=\"displayValue\" alt=\"\" />\n </ng-container>\n <ng-container *ngIf=\"col.filterType !== 'blob'\">\n <span spiderlyOverflowTitle class=\"cell-text\">{{\n displayValue\n }}</span>\n </ng-container>\n </ng-template>\n <tr\n [class.clickable]=\"navigateOnRowClick\"\n (click)=\"onRowClick(rowData, $event)\"\n >\n <!-- row-interactive: a selection gesture must never double as row navigation (onRowClick). -->\n <td\n *ngIf=\"selectionMode == 'multiple'\"\n class=\"selection-cell row-interactive frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.left]=\"'0'\"\n (mousedown)=\"onSelectionCellMouseDown($event, rowData[idField])\"\n >\n <p-checkbox\n [disabled]=\"readonly\"\n (onChange)=\"selectRow(rowData[idField], rowData.index)\"\n [ngModel]=\"isRowSelected(rowData[idField])\"\n [binary]=\"true\"\n ></p-checkbox>\n </td>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <td\n [style]=\"getStyleForBodyColumn(col)\"\n [class.clickable]=\"col.onCellClick\"\n [class.row-interactive]=\"col.onCellClick\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [class.cell-wrap]=\"isColumnWrapped(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n (click)=\"onCellClick(col, rowData, $event)\"\n *ngIf=\"!col.editable\"\n >\n <!-- getColWidth reserves this column's width from `actions.length`; this gap is\n the geometry it reserves for, so changing it belongs in the same edit. -->\n <div\n style=\"\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 18px;\n \"\n >\n <ng-container\n *ngFor=\"let action of col.actions; trackBy: actionTrackByFn\"\n >\n <span\n [pTooltip]=\"action.name\"\n [class]=\"getClassForAction(action) + ' row-interactive'\"\n [style]=\"getStyleForAction(action)\"\n (click)=\"getMethodForAction(action, rowData, $event)\"\n ></span>\n </ng-container>\n </div>\n <ng-container\n [ngTemplateOutlet]=\"getCellTemplate(col) ?? defaultCell\"\n [ngTemplateOutletContext]=\"{\n $implicit: rowData,\n value: rowData[col.field],\n displayValue: getRowData(rowData, col),\n col: col\n }\"\n ></ng-container>\n </td>\n <td\n *ngIf=\"col.editable\"\n class=\"row-interactive\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n >\n <spiderly-number\n [control]=\"getFormArrayControlByIndex(col.field, rowData.index)\"\n [showLabel]=\"false\"\n ></spiderly-number>\n </td>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n <tr>\n <td\n [attr.colspan]=\"\n visibleCols.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"NoRecordsFound\") }}\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template pTemplate=\"paginatorright\">\n <div style=\"display: flex; justify-content: end; gap: 10px\">\n <spiderly-button\n *ngIf=\"showAddButton\"\n [label]=\"t('AddNew')\"\n icon=\"pi pi-plus\"\n (onClick)=\"navigateToDetails(0)\"\n ></spiderly-button>\n </div>\n </ng-template>\n </p-table>\n <p-popover #columnChooser>\n <div class=\"column-chooser\">\n <label\n *ngFor=\"let col of chooserCols; trackBy: colTrackByFn\"\n data-testid=\"column-chooser-option\"\n class=\"column-chooser-option\"\n >\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isColumnVisible(col)\"\n [disabled]=\"!canToggleColumn(col)\"\n (onChange)=\"toggleColumn(col, $event.checked)\"\n ></p-checkbox>\n <span>{{ col.name }}</span>\n </label>\n <button\n type=\"button\"\n data-testid=\"column-chooser-reset\"\n class=\"column-chooser-reset\"\n (click)=\"resetColumnLayout()\"\n >\n {{ t('ResetToDefault') }}\n </button>\n </div>\n </p-popover>\n </div>\n</ng-container>\n", styles: [".table-header{display:flex;justify-content:space-between;align-items:center}@media (max-width: 640px){.table-header{display:flex;flex-direction:column;align-items:start;gap:14px}}.spiderly-table .p-paginator{padding:1rem}@media (min-width: 1400px){.spiderly-table .p-paginator-left-content{position:absolute;padding:14px;left:0}}@media (min-width: 1400px){.spiderly-table .p-paginator-right-content{position:absolute;padding:14px;right:0}}.column-chooser{display:flex;flex-direction:column;gap:8px}.column-chooser-option{display:flex;align-items:center;gap:8px;cursor:pointer}.column-chooser-reset{border:none;background:none;color:inherit;font:inherit;cursor:pointer;padding:0;margin-top:4px;color:var(--p-primary-color);align-self:flex-start;font-size:.875rem}.column-chooser-reset:hover{text-decoration:underline}.column-menu{display:flex;flex-direction:column;min-width:11rem}.column-menu-item{display:flex;align-items:center;gap:8px;border:none;background:none;color:inherit;font:inherit;cursor:pointer;text-align:left;padding:6px 10px;border-radius:var(--p-content-border-radius)}.column-menu-item .column-menu-item-label{flex:1}.column-menu-item:hover:not(:disabled){background:var(--p-content-hover-background)}.column-menu-item:disabled{color:var(--p-text-muted-color);cursor:default}:host .spiderly-table-container{scroll-margin-top:calc(var(--spiderly-viewport-top-inset, 0px) + 1rem)}:host ::ng-deep .clickable{cursor:pointer}:host ::ng-deep td.clickable:hover{text-decoration:underline}:host .column-menu-button{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:0 2px;opacity:0;font-size:.75rem}:host th:hover .column-menu-button,:host .column-menu-button:focus-visible{opacity:1}:host .table-views{display:flex;flex-wrap:wrap;gap:4px;padding-top:8px}:host .table-view{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:4px 10px;border-radius:var(--p-content-border-radius)}:host .table-view:hover{background:var(--p-content-hover-background)}:host .table-view-active{color:var(--p-primary-color);font-weight:600;background:var(--p-content-hover-background)}:host .frozen-column{position:sticky;z-index:1}:host td.frozen-column{background:var(--p-datatable-row-background, var(--p-content-background));transition:background-color var(--p-transition-duration, .2s)}:host tr:hover>td.frozen-column{background:var(--p-datatable-row-hover-background, inherit)}:host .frozen-column-last{box-shadow:2px 0 4px -2px #00000026}:host th{position:relative}:host .column-resizer{position:absolute;top:0;right:0;width:8px;height:100%;cursor:col-resize;-webkit-user-select:none;user-select:none}:host .selection-count{font-size:.75rem;color:var(--p-text-muted-color)}:host td.selection-cell{-webkit-user-select:none;user-select:none}:host td.cell-wrap{--spiderly-cell-overflow: visible;--spiderly-cell-white-space: normal}:host .cell-text{display:block;overflow:var(--spiderly-cell-overflow, hidden);text-overflow:ellipsis;white-space:var(--spiderly-cell-white-space, nowrap)}:host ::ng-deep .p-datatable-mask{background:color-mix(in srgb,var(--p-content-background, #ffffff) 65%,transparent);color:var(--p-primary-color, #334155);align-items:flex-start;padding-top:6rem}:host ::ng-deep .p-datatable-thead{position:unset!important}:host ::ng-deep .p-datatable-header{border-radius:6px 6px 0 0}:host ::ng-deep .p-datatable{border-radius:var(--p-content-border-radius);border:1px solid var(--p-datatable-border-color)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: SpiderlyControlsModule }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "component", type: SpiderlyNumberComponent, selector: "spiderly-number", inputs: ["prefix", "showButtons", "decimal", "maxFractionDigits"] }, { kind: "ngmodule", type: TableModule }, { kind: "component", type: i10.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "style", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "scrollDirection", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "responsive", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "autoLayout", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "virtualRowHeight", "selectAll"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "directive", type: i1$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "directive", type: i10.SortableColumn, selector: "[pSortableColumn]", inputs: ["pSortableColumn", "pSortableColumnDisabled"] }, { kind: "component", type: i10.SortIcon, selector: "p-sortIcon", inputs: ["field"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3$3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "fluid", "label", "icon", "buttonProps"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i8.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["value", "name", "disabled", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "inputStyle", "styleClass", "inputClass", "indeterminate", "size", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "component", type: SpiderlyFilterBarComponent, selector: "spiderly-filter-bar", inputs: ["filters", "totalRecords", "sort", "sortOptions", "sortIsDefault", "controlTemplates"], outputs: ["clearAll", "sortPick", "sortReset"] }, { kind: "directive", type: SpiderlyOverflowTitleDirective, selector: "[spiderlyOverflowTitle]" }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }] }); }
|
|
6558
6790
|
}
|
|
6559
6791
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyDataTableComponent, decorators: [{
|
|
6560
6792
|
type: Component,
|
|
@@ -6570,7 +6802,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
6570
6802
|
SpiderlyFilterBarComponent,
|
|
6571
6803
|
SpiderlyOverflowTitleDirective,
|
|
6572
6804
|
TooltipModule,
|
|
6573
|
-
], template: "<ng-container *transloco=\"let t\">\n <div\n #tableContainer\n class=\"spiderly-table-container\"\n [attr.aria-busy]=\"isPending\"\n [ngClass]=\"{\n 'card responsive-card-padding overflow-auto': showCardWrapper\n }\"\n >\n <p-table\n #dt\n [value]=\"items\"\n [rows]=\"rows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [rowHover]=\"true\"\n [paginator]=\"showPaginator\"\n responsiveLayout=\"scroll\"\n [lazy]=\"hasLazyLoad\"\n (onLazyLoad)=\"lazyLoad($event)\"\n (onPage)=\"onPageChange(tableContainer)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"isPending\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n sortMode=\"multiple\"\n [multiSortMeta]=\"initialMultiSortMeta\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\n [tableStyle]=\"tableStyle\"\n >\n <ng-template pTemplate=\"caption\">\n <div class=\"table-header overflow-auto\">\n <div style=\"display: flex; align-items: center; gap: 8px\">\n <i class=\"{{ tableIcon }}\" style=\"font-size: 20px\"></i>\n <div style=\"margin: 0px; font-size: 17.5px\">{{ tableTitle }}</div>\n </div>\n <div style=\"display: flex; gap: 8px\">\n <ng-container\n *ngIf=\"actionsTemplate\"\n [ngTemplateOutlet]=\"actionsTemplate\"\n ></ng-container>\n <button\n pButton\n *ngIf=\"hasLazyLoad\"\n data-testid=\"column-chooser-button\"\n [label]=\"t('Columns')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-sliders-h\"\n (click)=\"columnChooser.toggle($event)\"\n ></button>\n <button\n pButton\n *ngIf=\"showExportToExcelButton\"\n [label]=\"t('ExportToExcel')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-download\"\n (click)=\"exportListToExcel()\"\n ></button>\n <button\n pButton\n *ngIf=\"showReloadTableButton\"\n [label]=\"t('Reload')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-refresh\"\n (click)=\"reload()\"\n ></button>\n <button\n pButton\n *ngIf=\"deleteListFromTableObservableMethod && rowsSelectedNumber > 0\"\n [label]=\"t('DeleteSelected') + ' (' + rowsSelectedNumber + ')'\"\n class=\"p-button-danger\"\n style=\"flex: none\"\n icon=\"pi pi-trash\"\n (click)=\"deleteSelectedObjects()\"\n ></button>\n </div>\n </div>\n <!-- ONE menu for every column, with the target held in `menuColumn`. Rendering its items\n inside an @if keyed on the column would put them inside the popover's projected\n content, and PrimeNG does not re-project content an @if has destroyed \u2014 the filter\n bar's editor lost a whole afternoon to exactly that. -->\n <p-popover #columnMenu>\n <div class=\"column-menu\" role=\"menu\">\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-hide\"\n [disabled]=\"!menuColumn || !canToggleColumn(menuColumn)\"\n (click)=\"hideMenuColumn()\"\n >\n <i class=\"pi pi-eye-slash\"></i>\n <span class=\"column-menu-item-label\">{{ t('HideColumn') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-filter\"\n [disabled]=\"!menuColumn || !canFilterColumn(menuColumn)\"\n (click)=\"filterMenuColumn()\"\n >\n <i class=\"pi pi-filter\"></i>\n <span class=\"column-menu-item-label\">{{ t('Filter') }}\u2026</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-asc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(1)\"\n >\n <i class=\"pi pi-sort-amount-up-alt\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortAscending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-desc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(-1)\"\n >\n <i class=\"pi pi-sort-amount-down\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortDescending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-left\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, -1)\"\n (click)=\"moveMenuColumn(-1)\"\n >\n <i class=\"pi pi-arrow-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveLeft') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-right\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, 1)\"\n (click)=\"moveMenuColumn(1)\"\n >\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveRight') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-fit\"\n (click)=\"fitMenuColumn()\"\n >\n <i class=\"pi pi-arrows-h\"></i>\n <span class=\"column-menu-item-label\">{{ t('FitWidth') }}</span>\n </button>\n <!-- menuitemcheckbox, not menuitem: this one TOGGLES, and a label reading the same\n in both states leaves the operator to find out by clicking \u2014 which on a wide\n column means re-laying the grid out to learn what it was already set to. The\n check is the state; the label stays put, which is what the role is for.\n Disabled for an editable column: its cell holds an input, not clamped text, so\n the toggle could only persist a preference that changes nothing. -->\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-wrap\"\n [disabled]=\"!menuColumn || menuColumn.editable === true\"\n [attr.aria-checked]=\"isMenuColumnWrapped\"\n (click)=\"toggleWrapForMenuColumn()\"\n >\n <i class=\"pi pi-align-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('WrapText') }}</span>\n <i *ngIf=\"isMenuColumnWrapped\" class=\"pi pi-check\"></i>\n </button>\n </div>\n </p-popover>\n\n <div class=\"table-views\" *ngIf=\"views?.length\" role=\"tablist\">\n <button\n *ngFor=\"let view of views\"\n type=\"button\"\n role=\"tab\"\n class=\"table-view\"\n data-testid=\"table-view\"\n [class.table-view-active]=\"activeViewId === view.id\"\n [attr.aria-selected]=\"activeViewId === view.id\"\n (click)=\"selectView(view)\"\n >\n {{ view.label }}\n </button>\n </div>\n\n <spiderly-filter-bar\n *ngIf=\"filters\"\n [filters]=\"filters\"\n [totalRecords]=\"totalRecords\"\n [sort]=\"sortKeys\"\n [controlTemplates]=\"filterTemplates\"\n (clearAll)=\"clear(dt)\"\n ></spiderly-filter-bar>\n </ng-template>\n <ng-template pTemplate=\"header\">\n <tr>\n <!-- Sized for the checkbox plus the counter beside it, which shows `totalRecords` on a\n select-all and so reaches five digits on a large table. Not `0rem` (see CLAUDE.md \u2192\n Column widths), and not routed through getColWidth: this column has no `Column`. -->\n <th\n #selectionHeader\n class=\"frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.width]=\"selectionColumnWidth\"\n [style.left]=\"'0'\"\n *ngIf=\"selectionMode == 'multiple'\"\n >\n <div style=\"display: flex; gap: 8px\">\n <p-checkbox\n *ngIf=\"showSelectAllCheckbox\"\n [disabled]=\"readonly\"\n (onChange)=\"selectAll($event.checked)\"\n [(ngModel)]=\"fakeIsAllSelected\"\n [binary]=\"true\"\n ></p-checkbox>\n <span class=\"selection-count\">({{ rowsSelectedNumber }})</span>\n </div>\n </th>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <th\n #headerCell\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"!isColumnSortable(col)\"\n [style.width]=\"getColWidth(col)\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n [attr.draggable]=\"hasLazyLoad && !col.lockVisible ? true : null\"\n (mousedown)=\"onHeaderMouseDown($event)\"\n (dragstart)=\"onHeaderDragStart(col, $event)\"\n (dragover)=\"onHeaderDragOver(col, $event)\"\n (drop)=\"onHeaderDrop(col, $event)\"\n >\n <!-- Ours, not PrimeNG's: enabling [resizableColumns] makes it persist widths by\n th:nth-child INDEX into our own stateKey, which slides every stored width onto\n the wrong column the first time the chooser hides one (CLAUDE.md -> decision\n 5). We take the gesture and keep the state. -->\n <span\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n class=\"column-resizer\"\n data-testid=\"column-resizer\"\n (mousedown)=\"startColumnResize(col, $event, headerCell)\"\n ></span>\n <div\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n \"\n >\n <span style=\"display: flex; align-items: center; gap: 4px\">\n {{ col.name }}\n <p-sortIcon\n *ngIf=\"isColumnSortable(col)\"\n [field]=\"col.field!\"\n ></p-sortIcon>\n <!-- stopPropagation is load-bearing: the th carries pSortableColumn, so a click\n that reaches it sorts the grid. Opening a menu must not reorder the table on\n the way. -->\n <button\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n type=\"button\"\n class=\"column-menu-button\"\n data-testid=\"column-menu\"\n [attr.aria-label]=\"t('Columns')\"\n (click)=\"$event.stopPropagation(); openColumnMenu(col, $event)\"\n >\n <i class=\"pi pi-chevron-down\"></i>\n </button>\n </span>\n </div>\n </th>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template\n pTemplate=\"body\"\n let-rowData\n let-index=\"rowIndex\"\n let-editing=\"editing\"\n >\n <!-- The cell rendering every column gets unless it projects a spiderlyCellTemplate. Declared\n once here rather than inside the column loop, and fed the same context the projected\n template gets, so getRowData is evaluated exactly once per cell either way. -->\n <ng-template #defaultCell let-col=\"col\" let-displayValue=\"displayValue\">\n <ng-container *ngIf=\"col.filterType === 'blob'\">\n <img width=\"45\" [src]=\"displayValue\" alt=\"\" />\n </ng-container>\n <ng-container *ngIf=\"col.filterType !== 'blob'\">\n <span spiderlyOverflowTitle class=\"cell-text\">{{\n displayValue\n }}</span>\n </ng-container>\n </ng-template>\n <tr\n [class.clickable]=\"navigateOnRowClick\"\n (click)=\"onRowClick(rowData, $event)\"\n >\n <!-- row-interactive: a selection gesture must never double as row navigation (onRowClick). -->\n <td\n *ngIf=\"selectionMode == 'multiple'\"\n class=\"selection-cell row-interactive frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.left]=\"'0'\"\n (mousedown)=\"onSelectionCellMouseDown($event, rowData[idField])\"\n >\n <p-checkbox\n [disabled]=\"readonly\"\n (onChange)=\"selectRow(rowData[idField], rowData.index)\"\n [ngModel]=\"isRowSelected(rowData[idField])\"\n [binary]=\"true\"\n ></p-checkbox>\n </td>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <td\n [style]=\"getStyleForBodyColumn(col)\"\n [class.clickable]=\"col.onCellClick\"\n [class.row-interactive]=\"col.onCellClick\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [class.cell-wrap]=\"isColumnWrapped(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n (click)=\"onCellClick(col, rowData, $event)\"\n *ngIf=\"!col.editable\"\n >\n <!-- getColWidth reserves this column's width from `actions.length`; this gap is\n the geometry it reserves for, so changing it belongs in the same edit. -->\n <div\n style=\"\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 18px;\n \"\n >\n <ng-container\n *ngFor=\"let action of col.actions; trackBy: actionTrackByFn\"\n >\n <span\n [pTooltip]=\"action.name\"\n [class]=\"getClassForAction(action) + ' row-interactive'\"\n [style]=\"getStyleForAction(action)\"\n (click)=\"getMethodForAction(action, rowData, $event)\"\n ></span>\n </ng-container>\n </div>\n <ng-container\n [ngTemplateOutlet]=\"getCellTemplate(col) ?? defaultCell\"\n [ngTemplateOutletContext]=\"{\n $implicit: rowData,\n value: rowData[col.field],\n displayValue: getRowData(rowData, col),\n col: col\n }\"\n ></ng-container>\n </td>\n <td\n *ngIf=\"col.editable\"\n class=\"row-interactive\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n >\n <spiderly-number\n [control]=\"getFormArrayControlByIndex(col.field, rowData.index)\"\n [showLabel]=\"false\"\n ></spiderly-number>\n </td>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n <tr>\n <td\n [attr.colspan]=\"\n visibleCols.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"NoRecordsFound\") }}\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template pTemplate=\"paginatorright\">\n <div style=\"display: flex; justify-content: end; gap: 10px\">\n <spiderly-button\n *ngIf=\"showAddButton\"\n [label]=\"t('AddNew')\"\n icon=\"pi pi-plus\"\n (onClick)=\"navigateToDetails(0)\"\n ></spiderly-button>\n </div>\n </ng-template>\n </p-table>\n <p-popover #columnChooser>\n <div class=\"column-chooser\">\n <label\n *ngFor=\"let col of chooserCols; trackBy: colTrackByFn\"\n data-testid=\"column-chooser-option\"\n class=\"column-chooser-option\"\n >\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isColumnVisible(col)\"\n [disabled]=\"!canToggleColumn(col)\"\n (onChange)=\"toggleColumn(col, $event.checked)\"\n ></p-checkbox>\n <span>{{ col.name }}</span>\n </label>\n <button\n type=\"button\"\n data-testid=\"column-chooser-reset\"\n class=\"column-chooser-reset\"\n (click)=\"resetColumnLayout()\"\n >\n {{ t('ResetToDefault') }}\n </button>\n </div>\n </p-popover>\n </div>\n</ng-container>\n", styles: [".table-header{display:flex;justify-content:space-between;align-items:center}@media (max-width: 640px){.table-header{display:flex;flex-direction:column;align-items:start;gap:14px}}.spiderly-table .p-paginator{padding:1rem}@media (min-width: 1400px){.spiderly-table .p-paginator-left-content{position:absolute;padding:14px;left:0}}@media (min-width: 1400px){.spiderly-table .p-paginator-right-content{position:absolute;padding:14px;right:0}}.column-chooser{display:flex;flex-direction:column;gap:8px}.column-chooser-option{display:flex;align-items:center;gap:8px;cursor:pointer}.column-chooser-reset{border:none;background:none;color:inherit;font:inherit;cursor:pointer;padding:0;margin-top:4px;color:var(--p-primary-color);align-self:flex-start;font-size:.875rem}.column-chooser-reset:hover{text-decoration:underline}.column-menu{display:flex;flex-direction:column;min-width:11rem}.column-menu-item{display:flex;align-items:center;gap:8px;border:none;background:none;color:inherit;font:inherit;cursor:pointer;text-align:left;padding:6px 10px;border-radius:var(--p-content-border-radius)}.column-menu-item .column-menu-item-label{flex:1}.column-menu-item:hover:not(:disabled){background:var(--p-content-hover-background)}.column-menu-item:disabled{color:var(--p-text-muted-color);cursor:default}:host .spiderly-table-container{scroll-margin-top:calc(var(--spiderly-viewport-top-inset, 0px) + 1rem)}:host ::ng-deep .clickable{cursor:pointer}:host ::ng-deep td.clickable:hover{text-decoration:underline}:host .column-menu-button{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:0 2px;opacity:0;font-size:.75rem}:host th:hover .column-menu-button,:host .column-menu-button:focus-visible{opacity:1}:host .table-views{display:flex;flex-wrap:wrap;gap:4px;padding-top:8px}:host .table-view{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:4px 10px;border-radius:var(--p-content-border-radius)}:host .table-view:hover{background:var(--p-content-hover-background)}:host .table-view-active{color:var(--p-primary-color);font-weight:600;background:var(--p-content-hover-background)}:host .frozen-column{position:sticky;z-index:1}:host td.frozen-column{background:var(--p-datatable-row-background, var(--p-content-background));transition:background-color var(--p-transition-duration, .2s)}:host tr:hover>td.frozen-column{background:var(--p-datatable-row-hover-background, inherit)}:host .frozen-column-last{box-shadow:2px 0 4px -2px #00000026}:host th{position:relative}:host .column-resizer{position:absolute;top:0;right:0;width:8px;height:100%;cursor:col-resize;-webkit-user-select:none;user-select:none}:host .selection-count{font-size:.75rem;color:var(--p-text-muted-color)}:host td.selection-cell{-webkit-user-select:none;user-select:none}:host td.cell-wrap{--spiderly-cell-overflow: visible;--spiderly-cell-white-space: normal}:host .cell-text{display:block;overflow:var(--spiderly-cell-overflow, hidden);text-overflow:ellipsis;white-space:var(--spiderly-cell-white-space, nowrap)}:host ::ng-deep .p-datatable-mask{background:color-mix(in srgb,var(--p-content-background, #ffffff) 65%,transparent);color:var(--p-primary-color, #334155);align-items:flex-start;padding-top:6rem}:host ::ng-deep .p-datatable-thead{position:unset!important}:host ::ng-deep .p-datatable-header{border-radius:6px 6px 0 0}:host ::ng-deep .p-datatable{border-radius:var(--p-content-border-radius);border:1px solid var(--p-datatable-border-color)}\n"] }]
|
|
6805
|
+
], template: "<ng-container *transloco=\"let t\">\n <div\n #tableContainer\n class=\"spiderly-table-container\"\n [attr.aria-busy]=\"isPending\"\n [ngClass]=\"{\n 'card responsive-card-padding overflow-auto': showCardWrapper\n }\"\n >\n <p-table\n #dt\n [value]=\"items\"\n [rows]=\"rows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [rowHover]=\"true\"\n [paginator]=\"showPaginator\"\n responsiveLayout=\"scroll\"\n [lazy]=\"hasLazyLoad\"\n (onLazyLoad)=\"lazyLoad($event)\"\n (onPage)=\"onPageChange(tableContainer)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"isPending\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n sortMode=\"multiple\"\n [multiSortMeta]=\"initialMultiSortMeta\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\n [tableStyle]=\"tableStyle\"\n >\n <ng-template pTemplate=\"caption\">\n <div class=\"table-header overflow-auto\">\n <div style=\"display: flex; align-items: center; gap: 8px\">\n <i class=\"{{ tableIcon }}\" style=\"font-size: 20px\"></i>\n <div style=\"margin: 0px; font-size: 17.5px\">{{ tableTitle }}</div>\n </div>\n <div style=\"display: flex; gap: 8px\">\n <ng-container\n *ngIf=\"actionsTemplate\"\n [ngTemplateOutlet]=\"actionsTemplate\"\n ></ng-container>\n <button\n pButton\n *ngIf=\"hasLazyLoad\"\n data-testid=\"column-chooser-button\"\n [label]=\"t('Columns')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-sliders-h\"\n (click)=\"columnChooser.toggle($event)\"\n ></button>\n <button\n pButton\n *ngIf=\"showExportToExcelButton\"\n [label]=\"t('ExportToExcel')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-download\"\n (click)=\"exportListToExcel()\"\n ></button>\n <button\n pButton\n *ngIf=\"showReloadTableButton\"\n [label]=\"t('Reload')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-refresh\"\n (click)=\"reload()\"\n ></button>\n <button\n pButton\n *ngIf=\"deleteListFromTableObservableMethod && rowsSelectedNumber > 0\"\n [label]=\"t('DeleteSelected') + ' (' + rowsSelectedNumber + ')'\"\n class=\"p-button-danger\"\n style=\"flex: none\"\n icon=\"pi pi-trash\"\n (click)=\"deleteSelectedObjects()\"\n ></button>\n </div>\n </div>\n <!-- ONE menu for every column, with the target held in `menuColumn`. Rendering its items\n inside an @if keyed on the column would put them inside the popover's projected\n content, and PrimeNG does not re-project content an @if has destroyed \u2014 the filter\n bar's editor lost a whole afternoon to exactly that. -->\n <p-popover #columnMenu>\n <div class=\"column-menu\" role=\"menu\">\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-hide\"\n [disabled]=\"!menuColumn || !canToggleColumn(menuColumn)\"\n (click)=\"hideMenuColumn()\"\n >\n <i class=\"pi pi-eye-slash\"></i>\n <span class=\"column-menu-item-label\">{{ t('HideColumn') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-filter\"\n [disabled]=\"!menuColumn || !canFilterColumn(menuColumn)\"\n (click)=\"filterMenuColumn()\"\n >\n <i class=\"pi pi-filter\"></i>\n <span class=\"column-menu-item-label\">{{ t('Filter') }}\u2026</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-asc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(1)\"\n >\n <i class=\"pi pi-sort-amount-up-alt\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortAscending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-desc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(-1)\"\n >\n <i class=\"pi pi-sort-amount-down\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortDescending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-left\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, -1)\"\n (click)=\"moveMenuColumn(-1)\"\n >\n <i class=\"pi pi-arrow-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveLeft') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-right\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, 1)\"\n (click)=\"moveMenuColumn(1)\"\n >\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveRight') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-fit\"\n (click)=\"fitMenuColumn()\"\n >\n <i class=\"pi pi-arrows-h\"></i>\n <span class=\"column-menu-item-label\">{{ t('FitWidth') }}</span>\n </button>\n <!-- menuitemcheckbox, not menuitem: this one TOGGLES, and a label reading the same\n in both states leaves the operator to find out by clicking \u2014 which on a wide\n column means re-laying the grid out to learn what it was already set to. The\n check is the state; the label stays put, which is what the role is for.\n Disabled for an editable column: its cell holds an input, not clamped text, so\n the toggle could only persist a preference that changes nothing. -->\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-wrap\"\n [disabled]=\"!menuColumn || menuColumn.editable === true\"\n [attr.aria-checked]=\"isMenuColumnWrapped\"\n (click)=\"toggleWrapForMenuColumn()\"\n >\n <i class=\"pi pi-align-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('WrapText') }}</span>\n <i *ngIf=\"isMenuColumnWrapped\" class=\"pi pi-check\"></i>\n </button>\n </div>\n </p-popover>\n\n <div class=\"table-views\" *ngIf=\"views?.length\" role=\"tablist\">\n <button\n *ngFor=\"let view of views\"\n type=\"button\"\n role=\"tab\"\n class=\"table-view\"\n data-testid=\"table-view\"\n [class.table-view-active]=\"activeViewId === view.id\"\n [attr.aria-selected]=\"activeViewId === view.id\"\n (click)=\"selectView(view)\"\n >\n {{ view.label }}\n </button>\n </div>\n\n <spiderly-filter-bar\n *ngIf=\"filters\"\n [filters]=\"filters\"\n [totalRecords]=\"totalRecords\"\n [sort]=\"sortKeys\"\n [sortOptions]=\"sortPickOptions\"\n [sortIsDefault]=\"sortIsDefault\"\n [controlTemplates]=\"filterTemplates\"\n (clearAll)=\"clear()\"\n (sortPick)=\"sortFromBar($event)\"\n (sortReset)=\"resetSortFromBar()\"\n ></spiderly-filter-bar>\n </ng-template>\n <ng-template pTemplate=\"header\">\n <tr>\n <!-- Sized for the checkbox plus the counter beside it, which shows `totalRecords` on a\n select-all and so reaches five digits on a large table. Not `0rem` (see CLAUDE.md \u2192\n Column widths), and not routed through getColWidth: this column has no `Column`. -->\n <th\n #selectionHeader\n class=\"frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.width]=\"selectionColumnWidth\"\n [style.left]=\"'0'\"\n *ngIf=\"selectionMode == 'multiple'\"\n >\n <div style=\"display: flex; gap: 8px\">\n <p-checkbox\n *ngIf=\"showSelectAllCheckbox\"\n [disabled]=\"readonly\"\n (onChange)=\"selectAll($event.checked)\"\n [(ngModel)]=\"fakeIsAllSelected\"\n [binary]=\"true\"\n ></p-checkbox>\n <span class=\"selection-count\">({{ rowsSelectedNumber }})</span>\n </div>\n </th>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <th\n #headerCell\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"!isColumnSortable(col)\"\n [style.width]=\"getColWidth(col)\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n [attr.draggable]=\"hasLazyLoad && !col.lockVisible ? true : null\"\n (mousedown)=\"onHeaderMouseDown($event)\"\n (dragstart)=\"onHeaderDragStart(col, $event)\"\n (dragover)=\"onHeaderDragOver(col, $event)\"\n (drop)=\"onHeaderDrop(col, $event)\"\n >\n <!-- Ours, not PrimeNG's: enabling [resizableColumns] makes it persist widths by\n th:nth-child INDEX into our own stateKey, which slides every stored width onto\n the wrong column the first time the chooser hides one (CLAUDE.md -> decision\n 5). We take the gesture and keep the state. -->\n <span\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n class=\"column-resizer\"\n data-testid=\"column-resizer\"\n (mousedown)=\"startColumnResize(col, $event, headerCell)\"\n ></span>\n <div\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n \"\n >\n <span style=\"display: flex; align-items: center; gap: 4px\">\n {{ col.name }}\n <p-sortIcon\n *ngIf=\"isColumnSortable(col)\"\n [field]=\"col.field!\"\n ></p-sortIcon>\n <!-- stopPropagation is load-bearing: the th carries pSortableColumn, so a click\n that reaches it sorts the grid. Opening a menu must not reorder the table on\n the way. -->\n <button\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n type=\"button\"\n class=\"column-menu-button\"\n data-testid=\"column-menu\"\n [attr.aria-label]=\"t('Columns')\"\n (click)=\"$event.stopPropagation(); openColumnMenu(col, $event)\"\n >\n <i class=\"pi pi-chevron-down\"></i>\n </button>\n </span>\n </div>\n </th>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template\n pTemplate=\"body\"\n let-rowData\n let-index=\"rowIndex\"\n let-editing=\"editing\"\n >\n <!-- The cell rendering every column gets unless it projects a spiderlyCellTemplate. Declared\n once here rather than inside the column loop, and fed the same context the projected\n template gets, so getRowData is evaluated exactly once per cell either way. -->\n <ng-template #defaultCell let-col=\"col\" let-displayValue=\"displayValue\">\n <ng-container *ngIf=\"col.filterType === 'blob'\">\n <img width=\"45\" [src]=\"displayValue\" alt=\"\" />\n </ng-container>\n <ng-container *ngIf=\"col.filterType !== 'blob'\">\n <span spiderlyOverflowTitle class=\"cell-text\">{{\n displayValue\n }}</span>\n </ng-container>\n </ng-template>\n <tr\n [class.clickable]=\"navigateOnRowClick\"\n (click)=\"onRowClick(rowData, $event)\"\n >\n <!-- row-interactive: a selection gesture must never double as row navigation (onRowClick). -->\n <td\n *ngIf=\"selectionMode == 'multiple'\"\n class=\"selection-cell row-interactive frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.left]=\"'0'\"\n (mousedown)=\"onSelectionCellMouseDown($event, rowData[idField])\"\n >\n <p-checkbox\n [disabled]=\"readonly\"\n (onChange)=\"selectRow(rowData[idField], rowData.index)\"\n [ngModel]=\"isRowSelected(rowData[idField])\"\n [binary]=\"true\"\n ></p-checkbox>\n </td>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <td\n [style]=\"getStyleForBodyColumn(col)\"\n [class.clickable]=\"col.onCellClick\"\n [class.row-interactive]=\"col.onCellClick\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [class.cell-wrap]=\"isColumnWrapped(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n (click)=\"onCellClick(col, rowData, $event)\"\n *ngIf=\"!col.editable\"\n >\n <!-- getColWidth reserves this column's width from `actions.length`; this gap is\n the geometry it reserves for, so changing it belongs in the same edit. -->\n <div\n style=\"\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 18px;\n \"\n >\n <ng-container\n *ngFor=\"let action of col.actions; trackBy: actionTrackByFn\"\n >\n <span\n [pTooltip]=\"action.name\"\n [class]=\"getClassForAction(action) + ' row-interactive'\"\n [style]=\"getStyleForAction(action)\"\n (click)=\"getMethodForAction(action, rowData, $event)\"\n ></span>\n </ng-container>\n </div>\n <ng-container\n [ngTemplateOutlet]=\"getCellTemplate(col) ?? defaultCell\"\n [ngTemplateOutletContext]=\"{\n $implicit: rowData,\n value: rowData[col.field],\n displayValue: getRowData(rowData, col),\n col: col\n }\"\n ></ng-container>\n </td>\n <td\n *ngIf=\"col.editable\"\n class=\"row-interactive\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n >\n <spiderly-number\n [control]=\"getFormArrayControlByIndex(col.field, rowData.index)\"\n [showLabel]=\"false\"\n ></spiderly-number>\n </td>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n <tr>\n <td\n [attr.colspan]=\"\n visibleCols.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"NoRecordsFound\") }}\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template pTemplate=\"paginatorright\">\n <div style=\"display: flex; justify-content: end; gap: 10px\">\n <spiderly-button\n *ngIf=\"showAddButton\"\n [label]=\"t('AddNew')\"\n icon=\"pi pi-plus\"\n (onClick)=\"navigateToDetails(0)\"\n ></spiderly-button>\n </div>\n </ng-template>\n </p-table>\n <p-popover #columnChooser>\n <div class=\"column-chooser\">\n <label\n *ngFor=\"let col of chooserCols; trackBy: colTrackByFn\"\n data-testid=\"column-chooser-option\"\n class=\"column-chooser-option\"\n >\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isColumnVisible(col)\"\n [disabled]=\"!canToggleColumn(col)\"\n (onChange)=\"toggleColumn(col, $event.checked)\"\n ></p-checkbox>\n <span>{{ col.name }}</span>\n </label>\n <button\n type=\"button\"\n data-testid=\"column-chooser-reset\"\n class=\"column-chooser-reset\"\n (click)=\"resetColumnLayout()\"\n >\n {{ t('ResetToDefault') }}\n </button>\n </div>\n </p-popover>\n </div>\n</ng-container>\n", styles: [".table-header{display:flex;justify-content:space-between;align-items:center}@media (max-width: 640px){.table-header{display:flex;flex-direction:column;align-items:start;gap:14px}}.spiderly-table .p-paginator{padding:1rem}@media (min-width: 1400px){.spiderly-table .p-paginator-left-content{position:absolute;padding:14px;left:0}}@media (min-width: 1400px){.spiderly-table .p-paginator-right-content{position:absolute;padding:14px;right:0}}.column-chooser{display:flex;flex-direction:column;gap:8px}.column-chooser-option{display:flex;align-items:center;gap:8px;cursor:pointer}.column-chooser-reset{border:none;background:none;color:inherit;font:inherit;cursor:pointer;padding:0;margin-top:4px;color:var(--p-primary-color);align-self:flex-start;font-size:.875rem}.column-chooser-reset:hover{text-decoration:underline}.column-menu{display:flex;flex-direction:column;min-width:11rem}.column-menu-item{display:flex;align-items:center;gap:8px;border:none;background:none;color:inherit;font:inherit;cursor:pointer;text-align:left;padding:6px 10px;border-radius:var(--p-content-border-radius)}.column-menu-item .column-menu-item-label{flex:1}.column-menu-item:hover:not(:disabled){background:var(--p-content-hover-background)}.column-menu-item:disabled{color:var(--p-text-muted-color);cursor:default}:host .spiderly-table-container{scroll-margin-top:calc(var(--spiderly-viewport-top-inset, 0px) + 1rem)}:host ::ng-deep .clickable{cursor:pointer}:host ::ng-deep td.clickable:hover{text-decoration:underline}:host .column-menu-button{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:0 2px;opacity:0;font-size:.75rem}:host th:hover .column-menu-button,:host .column-menu-button:focus-visible{opacity:1}:host .table-views{display:flex;flex-wrap:wrap;gap:4px;padding-top:8px}:host .table-view{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:4px 10px;border-radius:var(--p-content-border-radius)}:host .table-view:hover{background:var(--p-content-hover-background)}:host .table-view-active{color:var(--p-primary-color);font-weight:600;background:var(--p-content-hover-background)}:host .frozen-column{position:sticky;z-index:1}:host td.frozen-column{background:var(--p-datatable-row-background, var(--p-content-background));transition:background-color var(--p-transition-duration, .2s)}:host tr:hover>td.frozen-column{background:var(--p-datatable-row-hover-background, inherit)}:host .frozen-column-last{box-shadow:2px 0 4px -2px #00000026}:host th{position:relative}:host .column-resizer{position:absolute;top:0;right:0;width:8px;height:100%;cursor:col-resize;-webkit-user-select:none;user-select:none}:host .selection-count{font-size:.75rem;color:var(--p-text-muted-color)}:host td.selection-cell{-webkit-user-select:none;user-select:none}:host td.cell-wrap{--spiderly-cell-overflow: visible;--spiderly-cell-white-space: normal}:host .cell-text{display:block;overflow:var(--spiderly-cell-overflow, hidden);text-overflow:ellipsis;white-space:var(--spiderly-cell-white-space, nowrap)}:host ::ng-deep .p-datatable-mask{background:color-mix(in srgb,var(--p-content-background, #ffffff) 65%,transparent);color:var(--p-primary-color, #334155);align-items:flex-start;padding-top:6rem}:host ::ng-deep .p-datatable-thead{position:unset!important}:host ::ng-deep .p-datatable-header{border-radius:6px 6px 0 0}:host ::ng-deep .p-datatable{border-radius:var(--p-content-border-radius);border:1px solid var(--p-datatable-border-color)}\n"] }]
|
|
6574
6806
|
}], ctorParameters: () => [{ type: i3$2.Router }, { type: i1$5.DialogService }, { type: i3$2.ActivatedRoute }, { type: SpiderlyMessageService }, { type: i1.TranslocoService }, { type: ConfigServiceBase }, { type: i0.ErrorHandler }, { type: undefined, decorators: [{
|
|
6575
6807
|
type: Inject,
|
|
6576
6808
|
args: [LOCALE_ID]
|
|
@@ -7732,7 +7964,7 @@ const unauthorizedInterceptor = (req, next) => {
|
|
|
7732
7964
|
const messageService = inject(SpiderlyMessageService);
|
|
7733
7965
|
const translocoService = inject(TranslocoService);
|
|
7734
7966
|
const authService = inject(AuthServiceBase);
|
|
7735
|
-
const reactToError = (err, request) => {
|
|
7967
|
+
const reactToError = (err, request, errorResponse) => {
|
|
7736
7968
|
// Unconditional, production included, and the only log for a failed request (the global
|
|
7737
7969
|
// ErrorHandler skips HTTP errors): the toast the user gets is deliberately vague, so this is
|
|
7738
7970
|
// where a developer reads which request failed and how. See SpiderlyErrorHandler for why
|
|
@@ -7740,17 +7972,9 @@ const unauthorizedInterceptor = (req, next) => {
|
|
|
7740
7972
|
// HttpErrorResponse on its own stringifies to "[object HttpErrorResponse]" in an error
|
|
7741
7973
|
// tracker's console breadcrumb; the object still follows, to expand in devtools.
|
|
7742
7974
|
console.error(`HTTP ${err.status} ${request.method} ${request.url}`, err);
|
|
7743
|
-
// TODO: type
|
|
7744
|
-
// so message/errorCode/traceId reads of the cross-language contract
|
|
7745
|
-
|
|
7746
|
-
if (request.responseType !== 'json' && typeof err.error === 'string') {
|
|
7747
|
-
try {
|
|
7748
|
-
errorResponse = JSON.parse(err.error);
|
|
7749
|
-
}
|
|
7750
|
-
catch {
|
|
7751
|
-
errorResponse = null;
|
|
7752
|
-
}
|
|
7753
|
-
}
|
|
7975
|
+
// TODO: type errorResponse as an ApiError interface (TS mirror of ApiErrorDTO, next to
|
|
7976
|
+
// errors/api-error-codes.ts) so message/errorCode/traceId reads of the cross-language contract
|
|
7977
|
+
// are compile-checked, not conventional.
|
|
7754
7978
|
// ApiErrorDTO.traceId is present only on reportable errors, so this is a no-op everywhere else —
|
|
7755
7979
|
// the server decides which responses carry a support reference, never this status-code chain.
|
|
7756
7980
|
const withReference = (detail) => errorResponse?.traceId
|
|
@@ -7783,10 +8007,36 @@ const unauthorizedInterceptor = (req, next) => {
|
|
|
7783
8007
|
messageService.errorMessage(withReference(translocoService.translate('UnexpectedErrorDetails')), translocoService.translate('UnexpectedErrorTitle'));
|
|
7784
8008
|
}
|
|
7785
8009
|
};
|
|
7786
|
-
return next(req).pipe(catchError((err) => {
|
|
7787
|
-
reactToError(err, req);
|
|
8010
|
+
return next(req).pipe(catchError((err) => readErrorBody(err, req).pipe(switchMap((errorResponse) => {
|
|
8011
|
+
reactToError(err, req, errorResponse);
|
|
7788
8012
|
return throwError(() => err);
|
|
7789
|
-
}));
|
|
8013
|
+
}))));
|
|
8014
|
+
};
|
|
8015
|
+
/**
|
|
8016
|
+
* The server's ApiErrorDTO, whatever body type the request asked for. Angular hands the ERROR body
|
|
8017
|
+
* back in the requested `responseType` too: a `'text'` request gets the JSON as a string, and a
|
|
8018
|
+
* `'blob'` request (every PDF / Excel download) gets it as a Blob, whose text is only readable
|
|
8019
|
+
* asynchronously — which is why this is an Observable and the toast waits for it. Before the Blob
|
|
8020
|
+
* branch existed, a BusinessException on a download endpoint reached the operator as the generic
|
|
8021
|
+
* "bad request" toast: the sentence the server had spelled out was sitting unread in the Blob.
|
|
8022
|
+
* A body that is not JSON resolves to null, so the status-code fallbacks below still apply.
|
|
8023
|
+
*/
|
|
8024
|
+
const readErrorBody = (err, request) => {
|
|
8025
|
+
if (err.error instanceof Blob) {
|
|
8026
|
+
return from(err.error.text()).pipe(map$1(parseJsonOrNull), catchError(() => of(null)));
|
|
8027
|
+
}
|
|
8028
|
+
if (request.responseType !== 'json' && typeof err.error === 'string') {
|
|
8029
|
+
return of(parseJsonOrNull(err.error));
|
|
8030
|
+
}
|
|
8031
|
+
return of(err.error);
|
|
8032
|
+
};
|
|
8033
|
+
const parseJsonOrNull = (text) => {
|
|
8034
|
+
try {
|
|
8035
|
+
return JSON.parse(text);
|
|
8036
|
+
}
|
|
8037
|
+
catch {
|
|
8038
|
+
return null;
|
|
8039
|
+
}
|
|
7790
8040
|
};
|
|
7791
8041
|
|
|
7792
8042
|
function authInitializer(authService, platformId) {
|