spiderly 19.12.0 → 19.12.2
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 +5 -5
- package/agent/docs/e2e-testing/index.md +1 -1
- package/fesm2022/spiderly.mjs +629 -138
- package/fesm2022/spiderly.mjs.map +1 -1
- package/lib/components/spiderly-data-table/spiderly-data-table.component.d.ts +208 -21
- package/lib/filters/allowed-operators.d.ts +5 -4
- package/lib/filters/filter-store.d.ts +23 -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/fesm2022/spiderly.mjs
CHANGED
|
@@ -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 {
|
|
@@ -4754,6 +4925,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
4754
4925
|
function rootFontSizePx() {
|
|
4755
4926
|
return parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
4756
4927
|
}
|
|
4928
|
+
/** Order matters: `[a, b]` and `[b, a]` ask the server for different orderings. */
|
|
4929
|
+
function sortMetaEquals(a, b) {
|
|
4930
|
+
return (a.length === b.length &&
|
|
4931
|
+
a.every((m, i) => m.field === b[i].field && m.order === b[i].order));
|
|
4932
|
+
}
|
|
4757
4933
|
const DEFAULT_COLUMN_WIDTH_REM = {
|
|
4758
4934
|
text: 12,
|
|
4759
4935
|
date: 10,
|
|
@@ -4852,7 +5028,8 @@ class SpiderlyDataTableComponent {
|
|
|
4852
5028
|
/** Paginator page-size choices; merge rules and persistence: CLAUDE.md → "Rows-per-page". */
|
|
4853
5029
|
this.rowsPerPageOptions = [10, 25, 50, 100];
|
|
4854
5030
|
/**
|
|
4855
|
-
* The view currently selected. Seeded from the first view in `ngOnInit
|
|
5031
|
+
* The view currently selected. Seeded from the first view in `ngOnInit` — then overridden by
|
|
5032
|
+
* `restoreActiveView`, so a reload lands where the operator was — and never left null on a
|
|
4856
5033
|
* table that has views: `viewScope` keys the layout off it, so an unseeded table writes its
|
|
4857
5034
|
* width and order to the SAME key an unmigrated table uses — orphaned the moment the operator
|
|
4858
5035
|
* clicks the first tab — and no tab reads as selected until they do.
|
|
@@ -4971,6 +5148,18 @@ class SpiderlyDataTableComponent {
|
|
|
4971
5148
|
* `tableStyle` is a field.
|
|
4972
5149
|
*/
|
|
4973
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 = [];
|
|
4974
5163
|
}
|
|
4975
5164
|
ngAfterViewInit() {
|
|
4976
5165
|
this.setupRemovableSort();
|
|
@@ -5000,29 +5189,31 @@ class SpiderlyDataTableComponent {
|
|
|
5000
5189
|
if (sortMeta && sortMeta.order === -1) {
|
|
5001
5190
|
const mouseEvent = event.originalEvent;
|
|
5002
5191
|
const isMultiSortClick = mouseEvent.metaKey || mouseEvent.ctrlKey;
|
|
5003
|
-
// Un-sorting substitutes the declared default (when there is one) right here,
|
|
5004
|
-
// so sortMultiple() broadcasts and emits the final meta in one shot.
|
|
5005
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.
|
|
5006
5196
|
const remaining = this.table._multiSortMeta.filter((m) => m.field !== event.field);
|
|
5007
5197
|
this.table._multiSortMeta = remaining.length
|
|
5008
5198
|
? remaining
|
|
5009
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();
|
|
5010
5205
|
}
|
|
5011
5206
|
else {
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
this.table.firstChange.emit(0);
|
|
5016
|
-
}
|
|
5017
|
-
}
|
|
5018
|
-
this.table.sortMultiple();
|
|
5019
|
-
if (this.table.isStateful()) {
|
|
5020
|
-
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();
|
|
5021
5210
|
}
|
|
5022
|
-
this.table.anchorRowIndex = null;
|
|
5023
|
-
return;
|
|
5024
5211
|
}
|
|
5025
|
-
|
|
5212
|
+
else {
|
|
5213
|
+
originalSort(event);
|
|
5214
|
+
// PrimeNG's sort() pages/saves itself; the override slot is ours to record.
|
|
5215
|
+
this.persistActiveViewSort();
|
|
5216
|
+
}
|
|
5026
5217
|
};
|
|
5027
5218
|
}
|
|
5028
5219
|
/**
|
|
@@ -5048,12 +5239,21 @@ class SpiderlyDataTableComponent {
|
|
|
5048
5239
|
}
|
|
5049
5240
|
}
|
|
5050
5241
|
}
|
|
5051
|
-
ngOnChanges() {
|
|
5242
|
+
ngOnChanges(changes) {
|
|
5052
5243
|
// Only meaningful once ngOnInit has resolved the defaults; before that it is a no-op re-run.
|
|
5053
5244
|
if (this.rows != null)
|
|
5054
5245
|
this.mergeActivePageSizesIntoOptions();
|
|
5246
|
+
if (changes['views'])
|
|
5247
|
+
this.adoptStoredView();
|
|
5055
5248
|
}
|
|
5056
5249
|
ngOnInit() {
|
|
5250
|
+
// Fail loud, not half-broken: the store commits through the paginated request, and a
|
|
5251
|
+
// client-side (form-array) table has none — armed anyway, the first commit would call the
|
|
5252
|
+
// absent getPaginatedListObservableMethod inside the requery effect and strand the pending
|
|
5253
|
+
// overlay forever. Client-side store predicates are unbuilt (spiderly#407 territory).
|
|
5254
|
+
if (this.filters && !this.hasLazyLoad) {
|
|
5255
|
+
throw new Error('spiderly-data-table: [filters] requires a lazy table — client-side filtering through a store is not implemented. Remove [filters] or make the table lazy.');
|
|
5256
|
+
}
|
|
5057
5257
|
if (this.rows == null)
|
|
5058
5258
|
this.rows = this.configService.defaultPageSize;
|
|
5059
5259
|
this.activeViewId ??= this.views?.[0]?.id ?? null;
|
|
@@ -5071,23 +5271,38 @@ class SpiderlyDataTableComponent {
|
|
|
5071
5271
|
this.clientLoad();
|
|
5072
5272
|
}
|
|
5073
5273
|
this.mergeActivePageSizesIntoOptions();
|
|
5274
|
+
// AFTER `resolvedStateKey` is derived, which is what every restore below keys off — placed
|
|
5275
|
+
// above it they read a null key and silently restore nothing. The view restore runs FIRST,
|
|
5276
|
+
// because the other three read the `viewScope` it decides. And all of it before the effect
|
|
5277
|
+
// is armed, so the first request already carries the filters and the effect's skipped first
|
|
5278
|
+
// run sees the state it will be watching.
|
|
5279
|
+
const restoredView = this.restoreActiveView();
|
|
5074
5280
|
this.restoreColumnVisibility();
|
|
5075
5281
|
this.restoreColumnLayout();
|
|
5076
|
-
|
|
5077
|
-
// first, they read a null key and silently restored nothing. And before the effect is armed,
|
|
5078
|
-
// so the first request already carries the filters and the effect's skipped first run sees
|
|
5079
|
-
// the state it will be watching.
|
|
5080
|
-
this.restoreAppliedFilters();
|
|
5282
|
+
this.enterViewFilters(restoredView, 'refresh');
|
|
5081
5283
|
if (this.filters)
|
|
5082
5284
|
this.requeryOnAppliedFilters();
|
|
5083
5285
|
this.chooserCols = this.cols.filter(SpiderlyDataTableComponent.isDataColumn);
|
|
5084
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 }));
|
|
5085
5292
|
// Bound to p-table's [multiSortMeta] so the FIRST request already carries the right
|
|
5086
|
-
// sort.
|
|
5087
|
-
//
|
|
5088
|
-
//
|
|
5089
|
-
|
|
5090
|
-
|
|
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`).
|
|
5300
|
+
const tableState = this.persistedTableState();
|
|
5301
|
+
this.initialMultiSortMeta = this.views?.length
|
|
5302
|
+
? this.resolvedViewSort()
|
|
5303
|
+
: (this.sanitizedSortMeta(tableState?.multiSortMeta) ??
|
|
5304
|
+
this.tableDefaultMultiSortMeta());
|
|
5305
|
+
this.alignPersistedTableStateSort(tableState);
|
|
5091
5306
|
}
|
|
5092
5307
|
/** A data column shows values for a `field`; anything else (the actions column) always renders. */
|
|
5093
5308
|
static isDataColumn(col) {
|
|
@@ -5225,24 +5440,48 @@ class SpiderlyDataTableComponent {
|
|
|
5225
5440
|
}
|
|
5226
5441
|
this.persistColumnVisibility();
|
|
5227
5442
|
this.refreshVisibleCols();
|
|
5443
|
+
if (!visible)
|
|
5444
|
+
this.dropHiddenColumnSort([col]);
|
|
5228
5445
|
}
|
|
5229
5446
|
/**
|
|
5230
5447
|
* Restores every column to its declared default and forgets the stored overrides — visibility,
|
|
5231
5448
|
* wrap, order and width alike. It undid visibility only until the header menu shipped three
|
|
5232
5449
|
* more gestures beside it, and a layout with no way back is worse than one with no knobs.
|
|
5233
5450
|
*
|
|
5234
|
-
*
|
|
5235
|
-
* applied filter and the sort chip names the sort, hidden column or not
|
|
5236
|
-
*
|
|
5451
|
+
* On a STORE table, hiding — here or from the menu — never touches filters or sort: the chip
|
|
5452
|
+
* bar names every applied filter and the sort chip names the sort, hidden column or not
|
|
5453
|
+
* (decision 2b). A storeless table keeps the sort half of the legacy invariant instead — see
|
|
5454
|
+
* `dropHiddenColumnSort`.
|
|
5237
5455
|
*/
|
|
5238
5456
|
resetColumnLayout() {
|
|
5239
5457
|
this.columnWrap = {};
|
|
5240
5458
|
this.columnOrder = [];
|
|
5241
5459
|
this.columnWidths = {};
|
|
5242
5460
|
this.persistColumnLayout();
|
|
5461
|
+
const wasVisible = this.visibleCols.filter(SpiderlyDataTableComponent.isDataColumn);
|
|
5243
5462
|
this.columnVisibilityOverrides = {};
|
|
5244
5463
|
this.persistColumnVisibility();
|
|
5245
5464
|
this.refreshVisibleCols();
|
|
5465
|
+
// Columns the reset just hid follow the same rule as a manual hide.
|
|
5466
|
+
this.dropHiddenColumnSort(wasVisible.filter((col) => !this.isColumnVisible(col)));
|
|
5467
|
+
}
|
|
5468
|
+
/**
|
|
5469
|
+
* The sort half of the legacy "hidden contributes nothing" invariant, kept ONLY for storeless
|
|
5470
|
+
* tables (the generated M2M details grids, until spiderly#407 scaffolds their stores): they
|
|
5471
|
+
* have no sort chip, so a hidden column's sort would order the grid with nothing on screen
|
|
5472
|
+
* naming it and no control left to clear it. The filter half needs no heir — `lazyLoad`
|
|
5473
|
+
* overwrites the outgoing filters unconditionally. On a store table this is a no-op:
|
|
5474
|
+
* decision 2b's chips are the visible surface, hidden column or not.
|
|
5475
|
+
*/
|
|
5476
|
+
dropHiddenColumnSort(cols) {
|
|
5477
|
+
if (!this.table || this.filters)
|
|
5478
|
+
return;
|
|
5479
|
+
const sorted = cols.filter((col) => this.table._multiSortMeta?.some((m) => m.field === col.field));
|
|
5480
|
+
if (!sorted.length)
|
|
5481
|
+
return;
|
|
5482
|
+
this.table._multiSortMeta = this.table._multiSortMeta.filter((m) => !sorted.some((col) => col.field === m.field));
|
|
5483
|
+
this.table.tableService.onSort(this.table._multiSortMeta);
|
|
5484
|
+
this.table._filter(); // re-emits the lazy load and saves the cleaned state
|
|
5246
5485
|
}
|
|
5247
5486
|
/**
|
|
5248
5487
|
* Layout keys carry the ACTIVE VIEW, which is what makes a view more than a saved filter: a
|
|
@@ -5271,6 +5510,65 @@ class SpiderlyDataTableComponent {
|
|
|
5271
5510
|
get filterStorage() {
|
|
5272
5511
|
return this.stateStorage === 'local' ? localStorage : sessionStorage;
|
|
5273
5512
|
}
|
|
5513
|
+
/**
|
|
5514
|
+
* Where the active view id lives. Deliberately NOT view-scoped — it is the input `viewScope`
|
|
5515
|
+
* derives from — and it follows `stateStorage` like the filters rather than the layout's
|
|
5516
|
+
* always-local rule, because the id and the filters it scopes are one question: a durable id
|
|
5517
|
+
* over session-scoped filters would restore a tab with nothing under it.
|
|
5518
|
+
*/
|
|
5519
|
+
get viewStateKey() {
|
|
5520
|
+
return this.resolvedStateKey ? `${this.resolvedStateKey}:view` : null;
|
|
5521
|
+
}
|
|
5522
|
+
/** Only `selectView` writes; the `ngOnInit` seed does not — absence means "the default". */
|
|
5523
|
+
persistActiveView() {
|
|
5524
|
+
if (!this.viewStateKey || !this.activeViewId)
|
|
5525
|
+
return;
|
|
5526
|
+
writeStoredJson(this.filterStorage, this.viewStateKey, this.activeViewId);
|
|
5527
|
+
}
|
|
5528
|
+
/**
|
|
5529
|
+
* Puts a reload back on the view the operator was using. Every persisted key carries the view
|
|
5530
|
+
* segment, so before this existed the seed decided what got restored: F5 landed on the first
|
|
5531
|
+
* view and read `…:all:filters` — the tab snapped to "Sve" and the bar came back empty while
|
|
5532
|
+
* the operator's filters sat intact under the old view's segment (Filip, on /porudzbine).
|
|
5533
|
+
*
|
|
5534
|
+
* An id naming no current view keeps the seed AND the stored key: "vanished by deploy" and
|
|
5535
|
+
* "not yet arrived" are indistinguishable here (order-list's catalog-licensed "Za pakovanje"
|
|
5536
|
+
* is literally both), so `adoptStoredView` re-reads the key on every `[views]` change and the
|
|
5537
|
+
* key self-heals on the next explicit click.
|
|
5538
|
+
*/
|
|
5539
|
+
restoreActiveView() {
|
|
5540
|
+
if (!this.viewStateKey || !this.views?.length)
|
|
5541
|
+
return null;
|
|
5542
|
+
const storedId = readStoredJson(this.filterStorage, this.viewStateKey);
|
|
5543
|
+
if (typeof storedId !== 'string')
|
|
5544
|
+
return null;
|
|
5545
|
+
const view = this.viewById(storedId) ?? null;
|
|
5546
|
+
if (view)
|
|
5547
|
+
this.activeViewId = view.id;
|
|
5548
|
+
return view;
|
|
5549
|
+
}
|
|
5550
|
+
/**
|
|
5551
|
+
* The second half of `restoreActiveView`: a stored view that did not exist at init is adopted
|
|
5552
|
+
* when a later `[views]` change brings it — through `selectView`, so it gets the same
|
|
5553
|
+
* stored-wins/transient/layout/requery semantics a click would have had. There is no parked
|
|
5554
|
+
* copy of the stored id — the key itself is re-read, and because every explicit pick
|
|
5555
|
+
* overwrites it, the operator's own choice wins by construction. Accepted cost of adopting
|
|
5556
|
+
* late instead of holding the first request: a brief flash of the seeded view's rows and a
|
|
5557
|
+
* second request when the late view lands.
|
|
5558
|
+
*/
|
|
5559
|
+
adoptStoredView() {
|
|
5560
|
+
if (!this.viewStateKey)
|
|
5561
|
+
return;
|
|
5562
|
+
const storedId = readStoredJson(this.filterStorage, this.viewStateKey);
|
|
5563
|
+
if (typeof storedId !== 'string' || storedId === this.activeViewId)
|
|
5564
|
+
return;
|
|
5565
|
+
const view = this.viewById(storedId);
|
|
5566
|
+
if (view)
|
|
5567
|
+
this.selectView(view);
|
|
5568
|
+
}
|
|
5569
|
+
viewById(id) {
|
|
5570
|
+
return this.views?.find((candidate) => candidate.id === id);
|
|
5571
|
+
}
|
|
5274
5572
|
persistAppliedFilters() {
|
|
5275
5573
|
if (!this.filtersStateKey || !this.filters)
|
|
5276
5574
|
return;
|
|
@@ -5282,12 +5580,27 @@ class SpiderlyDataTableComponent {
|
|
|
5282
5580
|
writeStoredJson(this.filterStorage, this.filtersStateKey, snapshot);
|
|
5283
5581
|
}
|
|
5284
5582
|
}
|
|
5285
|
-
|
|
5286
|
-
|
|
5583
|
+
/**
|
|
5584
|
+
* Applies the right filters for entering `view` — the transient rule's one home. A TRANSIENT
|
|
5585
|
+
* view's `apply` is a function of NOW, so it always re-derives; restoring would put
|
|
5586
|
+
* yesterday's date under a tab claiming "today" (`persistAppliedFilters` has no transient
|
|
5587
|
+
* guard, so storage holds the LAST answer). A non-transient view's own stored answer wins
|
|
5588
|
+
* over its `apply`. The semantics argument is the one deliberate difference between the
|
|
5589
|
+
* callers: a 'click' re-asks the view's question when nothing is stored, a 'refresh' returns
|
|
5590
|
+
* the empty state the operator left — they cleared it, and F5 must give the cleared state
|
|
5591
|
+
* back. On a table with no views, `view` is null and this is a plain restore of the
|
|
5592
|
+
* `persistAppliedFilters` snapshot.
|
|
5593
|
+
*/
|
|
5594
|
+
enterViewFilters(view, semantics) {
|
|
5595
|
+
if (!this.filters)
|
|
5287
5596
|
return;
|
|
5288
|
-
const
|
|
5289
|
-
|
|
5290
|
-
|
|
5597
|
+
const stored = !view?.transient && this.filtersStateKey
|
|
5598
|
+
? readStoredJson(this.filterStorage, this.filtersStateKey)
|
|
5599
|
+
: null;
|
|
5600
|
+
if (stored)
|
|
5601
|
+
this.filters.restore(stored);
|
|
5602
|
+
else if (view?.transient || semantics === 'click')
|
|
5603
|
+
view?.apply?.(this.filters);
|
|
5291
5604
|
}
|
|
5292
5605
|
get layoutStateKey() {
|
|
5293
5606
|
return this.resolvedStateKey
|
|
@@ -5344,25 +5657,142 @@ class SpiderlyDataTableComponent {
|
|
|
5344
5657
|
return null;
|
|
5345
5658
|
return readStoredJson(this.stateStorage === 'local' ? localStorage : sessionStorage, this.resolvedStateKey);
|
|
5346
5659
|
}
|
|
5660
|
+
activeView() {
|
|
5661
|
+
return this.activeViewId ? this.viewById(this.activeViewId) : undefined;
|
|
5662
|
+
}
|
|
5663
|
+
/**
|
|
5664
|
+
* Where the operator's per-view sort override lives. Null on a table without views — those
|
|
5665
|
+
* keep the pure PrimeNG sort persistence they always had — and null on a STORELESS table:
|
|
5666
|
+
* there "hidden contributes nothing" retires a hidden column's sort (`dropHiddenColumnSort`)
|
|
5667
|
+
* with no persist call at the hide site, so an override restored blind at the next init
|
|
5668
|
+
* would re-impose a sort no chip can name. Store tables — where every viewed consumer
|
|
5669
|
+
* lives — have the chip. Follows `stateStorage` like the filters and the view id: the sort
|
|
5670
|
+
* is part of the same question they scope.
|
|
5671
|
+
*/
|
|
5672
|
+
get viewSortStateKey() {
|
|
5673
|
+
if (!this.views?.length || !this.filters)
|
|
5674
|
+
return null;
|
|
5675
|
+
return this.resolvedStateKey
|
|
5676
|
+
? `${this.resolvedStateKey}${this.viewScope}:sort`
|
|
5677
|
+
: null;
|
|
5678
|
+
}
|
|
5679
|
+
/** The operator's stored sort for the active view, sanitized like any persisted sort. */
|
|
5680
|
+
persistedViewSort() {
|
|
5681
|
+
if (!this.viewSortStateKey)
|
|
5682
|
+
return null;
|
|
5683
|
+
const stored = readStoredJson(this.filterStorage, this.viewSortStateKey);
|
|
5684
|
+
return Array.isArray(stored) ? this.sanitizedSortMeta(stored) : null;
|
|
5685
|
+
}
|
|
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();
|
|
5695
|
+
}
|
|
5696
|
+
/**
|
|
5697
|
+
* Records the current sort as the active view's override — called from the EXPLICIT sort
|
|
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.
|
|
5703
|
+
*/
|
|
5704
|
+
persistActiveViewSort() {
|
|
5705
|
+
const key = this.viewSortStateKey;
|
|
5706
|
+
if (!key)
|
|
5707
|
+
return;
|
|
5708
|
+
const current = this.table._multiSortMeta ?? [];
|
|
5709
|
+
if (sortMetaEquals(current, this.defaultMultiSortMeta() ?? [])) {
|
|
5710
|
+
this.filterStorage.removeItem(key);
|
|
5711
|
+
}
|
|
5712
|
+
else {
|
|
5713
|
+
writeStoredJson(this.filterStorage, key, current);
|
|
5714
|
+
}
|
|
5715
|
+
}
|
|
5716
|
+
/**
|
|
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.
|
|
5725
|
+
*/
|
|
5726
|
+
applyViewSort() {
|
|
5727
|
+
const target = this.resolvedViewSort() ?? [];
|
|
5728
|
+
if (sortMetaEquals(target, this.table._multiSortMeta ?? []))
|
|
5729
|
+
return false;
|
|
5730
|
+
this.broadcastSort(target);
|
|
5731
|
+
return true;
|
|
5732
|
+
}
|
|
5733
|
+
/**
|
|
5734
|
+
* Rewrites the sort inside PrimeNG's persisted state to the one `ngOnInit` resolved. The two
|
|
5735
|
+
* can disagree only on a viewed table — PrimeNG's state is table-global while the resolved
|
|
5736
|
+
* sort is the view's — and PrimeNG's restoreState() applies its copy on the first [value]
|
|
5737
|
+
* change, AFTER the initial request already left with ours: without this line the grid's
|
|
5738
|
+
* arrows and every later request silently fall back to the other view's sort. Handed the
|
|
5739
|
+
* already-parsed state: `ngOnInit` reads the blob once for the whole init pass.
|
|
5740
|
+
*/
|
|
5741
|
+
alignPersistedTableStateSort(state) {
|
|
5742
|
+
if (!this.views?.length || !this.resolvedStateKey || !state)
|
|
5743
|
+
return;
|
|
5744
|
+
const resolved = this.initialMultiSortMeta ?? [];
|
|
5745
|
+
if (sortMetaEquals(state.multiSortMeta ?? [], resolved))
|
|
5746
|
+
return;
|
|
5747
|
+
state.multiSortMeta = resolved.length ? resolved : undefined;
|
|
5748
|
+
writeStoredJson(this.filterStorage, this.resolvedStateKey, state);
|
|
5749
|
+
}
|
|
5347
5750
|
//#endregion
|
|
5348
5751
|
/**
|
|
5349
|
-
* The
|
|
5350
|
-
*
|
|
5351
|
-
*
|
|
5752
|
+
* The DECLARED default in the current context: the active view's own sort when it declares
|
|
5753
|
+
* one, else the table-level `defaultSortField`. Every "fall back to the declared default"
|
|
5754
|
+
* site — tri-state un-sort, Clear filters, the unsorted-load safety net — reads this, so
|
|
5755
|
+
* un-sorting inside a view lands on the view's ordering, not the table's.
|
|
5352
5756
|
*/
|
|
5353
5757
|
defaultMultiSortMeta() {
|
|
5758
|
+
return this.viewDeclaredMultiSortMeta() ?? this.tableDefaultMultiSortMeta();
|
|
5759
|
+
}
|
|
5760
|
+
/**
|
|
5761
|
+
* The active view's declared sort, sanitized: a non-sortable field would answer every load
|
|
5762
|
+
* with a 400 (`keepSortableMeta`'s rationale), so it is dropped rather than sent; a view
|
|
5763
|
+
* whose declaration sanitizes to nothing falls through to the table default.
|
|
5764
|
+
*/
|
|
5765
|
+
viewDeclaredMultiSortMeta() {
|
|
5766
|
+
return this.sanitizedSortMeta(this.activeView()?.sort);
|
|
5767
|
+
}
|
|
5768
|
+
/**
|
|
5769
|
+
* The table-level declared default as PrimeNG sort meta, or null when none is declared. On a
|
|
5770
|
+
* store table it applies even while its column is hidden — the sort chip names the ordering
|
|
5771
|
+
* whether or not the column is on screen (decision 2b). A STORELESS table has no chip, so a
|
|
5772
|
+
* hidden default would order the grid with nothing on screen naming it; there the default
|
|
5773
|
+
* dies with the column, exactly as the legacy invariant had it (see `dropHiddenColumnSort`).
|
|
5774
|
+
*/
|
|
5775
|
+
tableDefaultMultiSortMeta() {
|
|
5354
5776
|
if (!this.defaultSortField)
|
|
5355
5777
|
return null;
|
|
5356
5778
|
const defaultSortCol = this.cols?.find((col) => col.field === this.defaultSortField);
|
|
5779
|
+
if (defaultSortCol &&
|
|
5780
|
+
!this.filters &&
|
|
5781
|
+
!this.isColumnVisible(defaultSortCol))
|
|
5782
|
+
return null;
|
|
5357
5783
|
// A declared default on a non-sortable column is a consumer mistake the backend answers
|
|
5358
5784
|
// with a 400 on every load; fall back to its implicit Id DESC instead (see keepSortableMeta).
|
|
5359
5785
|
if (defaultSortCol && !this.isColumnSortable(defaultSortCol))
|
|
5360
5786
|
return null;
|
|
5361
5787
|
return [{ field: this.defaultSortField, order: this.defaultSortOrder }];
|
|
5362
5788
|
}
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5789
|
+
/**
|
|
5790
|
+
* `keepSortableMeta` with "sanitized to nothing" surfaced as null — the rule every sort
|
|
5791
|
+
* SOURCE shares ("a sort that sanitizes to nothing is no sort"), so the resolution chains
|
|
5792
|
+
* can `??` past an empty answer instead of stopping on an empty array.
|
|
5793
|
+
*/
|
|
5794
|
+
sanitizedSortMeta(multiSortMeta) {
|
|
5795
|
+
const sortable = this.keepSortableMeta(multiSortMeta);
|
|
5366
5796
|
return sortable.length ? sortable : null;
|
|
5367
5797
|
}
|
|
5368
5798
|
/**
|
|
@@ -5380,12 +5810,11 @@ class SpiderlyDataTableComponent {
|
|
|
5380
5810
|
return col == null || this.isColumnSortable(col);
|
|
5381
5811
|
});
|
|
5382
5812
|
}
|
|
5383
|
-
// Safety net: any lazy load still leaving unsorted (
|
|
5384
|
-
//
|
|
5385
|
-
//
|
|
5386
|
-
//
|
|
5387
|
-
//
|
|
5388
|
-
// 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().
|
|
5389
5818
|
applyDefaultSortIfUnsorted(event) {
|
|
5390
5819
|
if (event.multiSortMeta?.length)
|
|
5391
5820
|
return;
|
|
@@ -5393,10 +5822,13 @@ class SpiderlyDataTableComponent {
|
|
|
5393
5822
|
if (defaultSort == null)
|
|
5394
5823
|
return;
|
|
5395
5824
|
event.multiSortMeta = defaultSort;
|
|
5396
|
-
if (this.table)
|
|
5397
|
-
this.
|
|
5398
|
-
|
|
5399
|
-
|
|
5825
|
+
if (this.table)
|
|
5826
|
+
this.broadcastSort(defaultSort);
|
|
5827
|
+
}
|
|
5828
|
+
/** Puts `meta` on the grid and tells the header icons — no fetch; the caller owns that. */
|
|
5829
|
+
broadcastSort(meta) {
|
|
5830
|
+
this.table._multiSortMeta = meta;
|
|
5831
|
+
this.table.tableService.onSort(meta);
|
|
5400
5832
|
}
|
|
5401
5833
|
onPageChange(container) {
|
|
5402
5834
|
scrollElementIntoViewIfAboveViewport(container);
|
|
@@ -5553,30 +5985,40 @@ class SpiderlyDataTableComponent {
|
|
|
5553
5985
|
/**
|
|
5554
5986
|
* Switches to a view. Clears first, always: a view is a STATE rather than an addition, and two
|
|
5555
5987
|
* of them composed produce a question nobody asked. The clear runs through the store directly
|
|
5556
|
-
* rather than through `clear(
|
|
5557
|
-
* view
|
|
5988
|
+
* rather than through `clear()`, whose requery effect would spend a fetch before the entering
|
|
5989
|
+
* view's filters are in place.
|
|
5558
5990
|
*/
|
|
5559
5991
|
selectView(view) {
|
|
5560
5992
|
this.activeViewId = view.id;
|
|
5993
|
+
// Always writes — including the first view, so a deliberate "Sve" during a deploy race
|
|
5994
|
+
// sticks, and so an explicit pick beats a stored id whose view has not arrived yet
|
|
5995
|
+
// (`adoptStoredView` compares against this key).
|
|
5996
|
+
this.persistActiveView();
|
|
5561
5997
|
// The keys just changed under us, so the layout has to be re-read for the view being entered
|
|
5562
5998
|
// — otherwise the one being left keeps rendering until something else forces a reload.
|
|
5563
5999
|
this.restoreColumnVisibility();
|
|
5564
6000
|
this.restoreColumnLayout();
|
|
5565
6001
|
this.refreshVisibleCols();
|
|
5566
|
-
|
|
6002
|
+
// Sort is view state too (decision 10's sort half). Swapped BEFORE the filter transition so
|
|
6003
|
+
// the one request the requery effect sends carries both.
|
|
6004
|
+
const sortChanged = this.applyViewSort();
|
|
6005
|
+
if (!this.filters) {
|
|
6006
|
+
if (sortChanged)
|
|
6007
|
+
this.table.sortMultiple();
|
|
5567
6008
|
return;
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
//
|
|
5571
|
-
//
|
|
6009
|
+
}
|
|
6010
|
+
const appliedBefore = this.filters.applied();
|
|
6011
|
+
// The filter key moved with the view too; `enterViewFilters` holds the stored-wins and
|
|
6012
|
+
// transient rules.
|
|
5572
6013
|
this.filters.clear();
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
6014
|
+
this.enterViewFilters(view, 'click');
|
|
6015
|
+
// The requery effect runs only when the applied set actually changed — `clear()` on an
|
|
6016
|
+
// already-empty store writes nothing (two views sharing one filter answer are exactly that)
|
|
6017
|
+
// — so a sort-only switch fetches here. `applied` is a computed: reference equality across
|
|
6018
|
+
// the transition means none of its sources moved.
|
|
6019
|
+
if (sortChanged && this.filters.applied() === appliedBefore) {
|
|
6020
|
+
this.refetchFromFirstPage();
|
|
6021
|
+
}
|
|
5580
6022
|
}
|
|
5581
6023
|
/**
|
|
5582
6024
|
* The filter this column stands for. Defaults to the column's own `field`, because a store id
|
|
@@ -5625,8 +6067,7 @@ class SpiderlyDataTableComponent {
|
|
|
5625
6067
|
this.columnMenu().hide();
|
|
5626
6068
|
if (!col?.field || !this.isColumnSortable(col))
|
|
5627
6069
|
return;
|
|
5628
|
-
this.
|
|
5629
|
-
this.table.sortMultiple();
|
|
6070
|
+
this.applySort([{ field: col.field, order }]);
|
|
5630
6071
|
}
|
|
5631
6072
|
/**
|
|
5632
6073
|
* Sizes the column to the widest thing in it. The cheaper escape from a cramped column: no
|
|
@@ -5736,10 +6177,50 @@ class SpiderlyDataTableComponent {
|
|
|
5736
6177
|
* left falls back to its own name rather than vanishing.
|
|
5737
6178
|
*/
|
|
5738
6179
|
refreshSortKeys() {
|
|
5739
|
-
|
|
5740
|
-
|
|
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)),
|
|
5741
6189
|
descending: key.order === -1,
|
|
5742
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() ?? []);
|
|
5743
6224
|
}
|
|
5744
6225
|
/** This column's share, override first, then its declared width, then the per-type default. */
|
|
5745
6226
|
columnShare(col) {
|
|
@@ -6003,11 +6484,24 @@ class SpiderlyDataTableComponent {
|
|
|
6003
6484
|
return;
|
|
6004
6485
|
}
|
|
6005
6486
|
this.persistAppliedFilters();
|
|
6006
|
-
this.
|
|
6007
|
-
this.table.firstChange.emit(0);
|
|
6008
|
-
this.lazyLoad(this.table.createLazyLoadMetadata());
|
|
6487
|
+
this.refetchFromFirstPage();
|
|
6009
6488
|
}, { injector: this.injector });
|
|
6010
6489
|
}
|
|
6490
|
+
/**
|
|
6491
|
+
* Back to page one, state saved, one fetch off FRESH metadata — the shared tail of the
|
|
6492
|
+
* requery effect and `selectView`'s sort-only switch. The save is load-bearing: PrimeNG
|
|
6493
|
+
* saves state only from its own interactions, so without it the pre-change page offset
|
|
6494
|
+
* survives in storage and the NEXT visit replays it against a narrowed result set — the
|
|
6495
|
+
* backend answers data=[] for an offset the filtered total no longer reaches, and the grid
|
|
6496
|
+
* renders empty under chips claiming matches.
|
|
6497
|
+
*/
|
|
6498
|
+
refetchFromFirstPage() {
|
|
6499
|
+
this.table.first = 0;
|
|
6500
|
+
this.table.firstChange.emit(0);
|
|
6501
|
+
if (this.table.isStateful())
|
|
6502
|
+
this.table.saveState();
|
|
6503
|
+
this.lazyLoad(this.table.createLazyLoadMetadata());
|
|
6504
|
+
}
|
|
6011
6505
|
reload() {
|
|
6012
6506
|
// Nothing to replay before the table's own initial load, which is already on its way.
|
|
6013
6507
|
if (this.lastLazyLoadEvent == null)
|
|
@@ -6108,14 +6602,17 @@ class SpiderlyDataTableComponent {
|
|
|
6108
6602
|
exportListToExcel(this.exportListToExcelObservableMethod, filter);
|
|
6109
6603
|
}
|
|
6110
6604
|
/**
|
|
6111
|
-
* Clears
|
|
6112
|
-
*
|
|
6113
|
-
*
|
|
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.
|
|
6114
6613
|
*/
|
|
6115
|
-
clear(
|
|
6614
|
+
clear() {
|
|
6116
6615
|
this.filters?.clear();
|
|
6117
|
-
table.clear();
|
|
6118
|
-
table.clearState();
|
|
6119
6616
|
}
|
|
6120
6617
|
//#region Selection
|
|
6121
6618
|
setFakeIsAllSelected() {
|
|
@@ -6289,7 +6786,7 @@ class SpiderlyDataTableComponent {
|
|
|
6289
6786
|
}
|
|
6290
6787
|
}
|
|
6291
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 }); }
|
|
6292
|
-
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"] }] }); }
|
|
6293
6790
|
}
|
|
6294
6791
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyDataTableComponent, decorators: [{
|
|
6295
6792
|
type: Component,
|
|
@@ -6305,7 +6802,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
6305
6802
|
SpiderlyFilterBarComponent,
|
|
6306
6803
|
SpiderlyOverflowTitleDirective,
|
|
6307
6804
|
TooltipModule,
|
|
6308
|
-
], 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"] }]
|
|
6309
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: [{
|
|
6310
6807
|
type: Inject,
|
|
6311
6808
|
args: [LOCALE_ID]
|
|
@@ -6415,12 +6912,13 @@ class Action {
|
|
|
6415
6912
|
}
|
|
6416
6913
|
}
|
|
6417
6914
|
class Column {
|
|
6418
|
-
constructor({ name, field, filterType, filterId, showMatchModes, dropdownOrMultiselectValues, actions, editable, showTime, decimalPlaces, sortable, visible, lockVisible, width, onCellClick, } = {}) {
|
|
6915
|
+
constructor({ name, field, filterType, filterId, showMatchModes, filterField, dropdownOrMultiselectValues, actions, editable, showTime, decimalPlaces, sortable, visible, lockVisible, width, onCellClick, } = {}) {
|
|
6419
6916
|
this.name = name;
|
|
6420
6917
|
this.field = field;
|
|
6421
6918
|
this.filterType = filterType;
|
|
6422
6919
|
this.filterId = filterId;
|
|
6423
6920
|
this.showMatchModes = showMatchModes;
|
|
6921
|
+
this.filterField = filterField;
|
|
6424
6922
|
this.dropdownOrMultiselectValues = dropdownOrMultiselectValues;
|
|
6425
6923
|
this.actions = actions;
|
|
6426
6924
|
this.editable = editable;
|
|
@@ -6462,10 +6960,11 @@ var MatchModeCodes;
|
|
|
6462
6960
|
* paginator implements per type (`PaginatedResultGenerator`): `In` on a string column answers
|
|
6463
6961
|
* with InvalidMatchMode, PrimeNG's own text list adds three modes the backend 400s, and so on.
|
|
6464
6962
|
*
|
|
6465
|
-
* Deliberately a LEAF module (imports nothing but the codes) so
|
|
6466
|
-
* it without a cycle: the store's runtime validation and offered lists (`filter-store.ts`),
|
|
6467
|
-
*
|
|
6468
|
-
* hand-kept copies; edit the
|
|
6963
|
+
* Deliberately a LEAF module (imports nothing but the codes) so every consumer can derive from
|
|
6964
|
+
* it without a cycle: the store's runtime validation and offered lists (`filter-store.ts`), the
|
|
6965
|
+
* compile-time `FilterRule.matchMode` union (`entities/filter-rule.ts`), and the data view's
|
|
6966
|
+
* kind-level lists (via `operatorOptionsForKind`). These used to be hand-kept copies; edit the
|
|
6967
|
+
* table here and every surface follows.
|
|
6469
6968
|
*
|
|
6470
6969
|
* Tuple order is DISPLAY order for every operator picker (Equals, then "less/before", then
|
|
6471
6970
|
* "more/after" — the order the header dropdowns always shipped).
|
|
@@ -6563,6 +7062,17 @@ function toOperatorOptions(kind, operators) {
|
|
|
6563
7062
|
labelKey: OPERATOR_WORDS[kind][value]?.pickerKey ?? value,
|
|
6564
7063
|
}));
|
|
6565
7064
|
}
|
|
7065
|
+
/**
|
|
7066
|
+
* A KIND's operator picker options, for callers with no filter behind them — today that is
|
|
7067
|
+
* `spiderly-data-view`, whose own filter config derives its operator lists here instead of
|
|
7068
|
+
* hand-keeping copies (the drift `allowed-operators.ts` exists to end). Per-FILTER narrowing
|
|
7069
|
+
* lives in the store (`createFilterStore` resolves it once per declaration), so this cannot be
|
|
7070
|
+
* handed a fake definition to satisfy — that shape shipped once and interpolated an empty
|
|
7071
|
+
* filter name into its own error message.
|
|
7072
|
+
*/
|
|
7073
|
+
function operatorOptionsForKind(kind, hasOptions = false) {
|
|
7074
|
+
return toOperatorOptions(kind, baseOperators(kind, hasOptions));
|
|
7075
|
+
}
|
|
6566
7076
|
/**
|
|
6567
7077
|
* The operator applied when nobody picked one, which has to be what a person MEANS by filling in
|
|
6568
7078
|
* the control. `Contains` for a fragment typed into a box; `GreaterThan` for a date, never
|
|
@@ -6916,34 +7426,15 @@ class SpiderlyDataViewComponent {
|
|
|
6916
7426
|
};
|
|
6917
7427
|
}
|
|
6918
7428
|
ngOnInit() {
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
{
|
|
6929
|
-
label: this.translocoService.translate('DatesAfter'),
|
|
6930
|
-
value: MatchModeCodes.GreaterThan,
|
|
6931
|
-
},
|
|
6932
|
-
];
|
|
6933
|
-
this.matchModeNumberOptions = [
|
|
6934
|
-
{
|
|
6935
|
-
label: this.translocoService.translate('Equals'),
|
|
6936
|
-
value: MatchModeCodes.Equals,
|
|
6937
|
-
},
|
|
6938
|
-
{
|
|
6939
|
-
label: this.translocoService.translate('LessThan'),
|
|
6940
|
-
value: MatchModeCodes.LessThan,
|
|
6941
|
-
},
|
|
6942
|
-
{
|
|
6943
|
-
label: this.translocoService.translate('MoreThan'),
|
|
6944
|
-
value: MatchModeCodes.GreaterThan,
|
|
6945
|
-
},
|
|
6946
|
-
];
|
|
7429
|
+
// Derived from the ONE operator table (`filters/allowed-operators.ts`) rather than
|
|
7430
|
+
// hand-kept: these were the last hand-written copies of the per-kind lists, and a backend
|
|
7431
|
+
// operator change edited in the shared table would silently never have reached them.
|
|
7432
|
+
const matchModeOptionsFor = (kind) => operatorOptionsForKind(kind).map((option) => ({
|
|
7433
|
+
label: this.translocoService.translate(option.labelKey),
|
|
7434
|
+
value: option.value,
|
|
7435
|
+
}));
|
|
7436
|
+
this.matchModeDateOptions = matchModeOptionsFor('date');
|
|
7437
|
+
this.matchModeNumberOptions = matchModeOptionsFor('number');
|
|
6947
7438
|
}
|
|
6948
7439
|
// Paging semantics and the scroll-margin contract are documented once, on the data table:
|
|
6949
7440
|
// spiderly-data-table/CLAUDE.md → "Scroll back to the table on page change".
|
|
@@ -7584,5 +8075,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
7584
8075
|
* Generated bundle index. Do not edit.
|
|
7585
8076
|
*/
|
|
7586
8077
|
|
|
7587
|
-
export { ALLOWED_OPERATORS, Action, AllClickEvent, ApiErrorCodes, ApiSecurityService, AppSidebarComponent, AuthCardComponent, AuthGuard, AuthResult, AuthResultWithCookies, AuthServiceBase, BaseAutocompleteControl, BaseControl, BaseDropdownControl, BaseEntity, BaseFormComponent, BaseFormService, CardSkeletonComponent, Codebook, Column, ConfigServiceBase, DEFAULT_EXTERNAL_PROVIDER_ICONS, DEFAULT_OPERATOR, ExternalLoginComponent, ExternalProvider, ExternalProviderPublic, Filter, FilterRule, FilterSortMeta, IndexCardComponent, InfoCardComponent, InitCompanyAuthDialogDetails, InitTopBarData, IsAuthorizedForSaveEvent, LastMenuIconIndexClicked, LayoutServiceBase, LazyLoadSelectedIdsResult, Login, LoginVerificationComponent, LoginVerificationToken, MatchModeCodes, MenuChangeEvent, MenuitemComponent, Namebook, NotAuthGuard, NotFoundComponent, PROPS_KEY, PaginatedResult, PanelBodyComponent, PanelFooterComponent, PanelHeaderComponent, PrimengOption, ProfileAvatarComponent, ReflectProp, RefreshTokenRequest, RequiredComponent, RowClickEvent, SecurityPermissionCodes, SendLoginVerificationEmailResult, SideMenuTopBarComponent, SidebarMenuComponent, SimpleSaveResult, SpiderlyAutocompleteComponent, SpiderlyButtonBaseComponent, SpiderlyButtonComponent, SpiderlyCalendarComponent, SpiderlyCardComponent, SpiderlyCellTemplateDirective, SpiderlyCheckboxComponent, SpiderlyColorPickerComponent, SpiderlyControlsModule, SpiderlyDataTableActionsDirective, SpiderlyDataTableComponent, SpiderlyDataViewComponent, SpiderlyDeleteConfirmationComponent, SpiderlyDropdownComponent, SpiderlyEditorComponent, SpiderlyErrorHandler, SpiderlyFileComponent, SpiderlyFileSelectEvent, SpiderlyFilterBarComponent, SpiderlyFilterTemplateDirective, SpiderlyFormArray, SpiderlyFormControl, SpiderlyFormGroup, SpiderlyLayoutComponent, SpiderlyLoginComponent, SpiderlyMarkdownComponent, SpiderlyMessageService, SpiderlyMultiAutocompleteComponent, SpiderlyMultiSelectComponent, SpiderlyNumberComponent, SpiderlyOverflowTitleDirective, SpiderlyPanelComponent, SpiderlyPanelsModule, SpiderlyPasswordComponent, SpiderlyReturnButtonComponent, SpiderlySplitButtonComponent, SpiderlyTab, SpiderlyTemplateTypeDirective, SpiderlyTextareaComponent, SpiderlyTextboxComponent, SpiderlyTranslocoLoader, TopBarComponent, UserBase, UserRole, ValidatorAbstractService, VerificationTokenRequest, VerificationTypeCodes, VerificationWrapperComponent, adjustColor, authInitializer, booleanFilter, capitalizeFirstChar, createFilterStore, dateFilter, deleteAction, exportListToExcel, firstCharToUpper, foldForSearch, getFileNameFromContentDisposition, getFilterOptions, getHtmlImgDisplayString64, getImageDimensions, getMimeTypeForFileName, getMonth, getParentUrl, getPrimengAutocompleteCodebookOptions, getPrimengAutocompleteNamebookOptions, getPrimengDropdownCodebookOptions, getPrimengDropdownNamebookOptions, getPrimengNamebookOptions, httpLoadingInterceptor, isExcelFileType, isFileImageType, isNullOrEmpty, jsonHttpInterceptor, jwtInterceptor, kebabToTitleCase, nameOf, nameof, numberFilter, parseDateOnlyLocal, pushAction, saveResponseAsFile, scrollElementIntoViewIfAboveViewport, selectedTab, singleOrDefault, splitPascalCase, textFilter, toCommaSeparatedString, unauthorizedInterceptor, validatePrecisionScale };
|
|
8078
|
+
export { ALLOWED_OPERATORS, Action, AllClickEvent, ApiErrorCodes, ApiSecurityService, AppSidebarComponent, AuthCardComponent, AuthGuard, AuthResult, AuthResultWithCookies, AuthServiceBase, BaseAutocompleteControl, BaseControl, BaseDropdownControl, BaseEntity, BaseFormComponent, BaseFormService, CardSkeletonComponent, Codebook, Column, ConfigServiceBase, DEFAULT_EXTERNAL_PROVIDER_ICONS, DEFAULT_OPERATOR, ExternalLoginComponent, ExternalProvider, ExternalProviderPublic, Filter, FilterRule, FilterSortMeta, IndexCardComponent, InfoCardComponent, InitCompanyAuthDialogDetails, InitTopBarData, IsAuthorizedForSaveEvent, LastMenuIconIndexClicked, LayoutServiceBase, LazyLoadSelectedIdsResult, Login, LoginVerificationComponent, LoginVerificationToken, MatchModeCodes, MenuChangeEvent, MenuitemComponent, Namebook, NotAuthGuard, NotFoundComponent, PROPS_KEY, PaginatedResult, PanelBodyComponent, PanelFooterComponent, PanelHeaderComponent, PrimengOption, ProfileAvatarComponent, ReflectProp, RefreshTokenRequest, RequiredComponent, RowClickEvent, SecurityPermissionCodes, SendLoginVerificationEmailResult, SideMenuTopBarComponent, SidebarMenuComponent, SimpleSaveResult, SpiderlyAutocompleteComponent, SpiderlyButtonBaseComponent, SpiderlyButtonComponent, SpiderlyCalendarComponent, SpiderlyCardComponent, SpiderlyCellTemplateDirective, SpiderlyCheckboxComponent, SpiderlyColorPickerComponent, SpiderlyControlsModule, SpiderlyDataTableActionsDirective, SpiderlyDataTableComponent, SpiderlyDataViewComponent, SpiderlyDeleteConfirmationComponent, SpiderlyDropdownComponent, SpiderlyEditorComponent, SpiderlyErrorHandler, SpiderlyFileComponent, SpiderlyFileSelectEvent, SpiderlyFilterBarComponent, SpiderlyFilterTemplateDirective, SpiderlyFormArray, SpiderlyFormControl, SpiderlyFormGroup, SpiderlyLayoutComponent, SpiderlyLoginComponent, SpiderlyMarkdownComponent, SpiderlyMessageService, SpiderlyMultiAutocompleteComponent, SpiderlyMultiSelectComponent, SpiderlyNumberComponent, SpiderlyOverflowTitleDirective, SpiderlyPanelComponent, SpiderlyPanelsModule, SpiderlyPasswordComponent, SpiderlyReturnButtonComponent, SpiderlySplitButtonComponent, SpiderlyTab, SpiderlyTemplateTypeDirective, SpiderlyTextareaComponent, SpiderlyTextboxComponent, SpiderlyTranslocoLoader, TopBarComponent, UserBase, UserRole, ValidatorAbstractService, VerificationTokenRequest, VerificationTypeCodes, VerificationWrapperComponent, adjustColor, authInitializer, booleanFilter, capitalizeFirstChar, createFilterStore, dateFilter, deleteAction, exportListToExcel, firstCharToUpper, foldForSearch, getFileNameFromContentDisposition, getFilterOptions, getHtmlImgDisplayString64, getImageDimensions, getMimeTypeForFileName, getMonth, getParentUrl, getPrimengAutocompleteCodebookOptions, getPrimengAutocompleteNamebookOptions, getPrimengDropdownCodebookOptions, getPrimengDropdownNamebookOptions, getPrimengNamebookOptions, httpLoadingInterceptor, isExcelFileType, isFileImageType, isNullOrEmpty, jsonHttpInterceptor, jwtInterceptor, kebabToTitleCase, nameOf, nameof, numberFilter, operatorOptionsForKind, parseDateOnlyLocal, pushAction, saveResponseAsFile, scrollElementIntoViewIfAboveViewport, selectedTab, singleOrDefault, splitPascalCase, textFilter, toCommaSeparatedString, unauthorizedInterceptor, validatePrecisionScale };
|
|
7588
8079
|
//# sourceMappingURL=spiderly.mjs.map
|