spiderly 19.11.9 → 19.12.1

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.
@@ -1,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Input, Component, EventEmitter, Output, Injectable, ViewChild, NgModule, PLATFORM_ID, Inject, HostBinding, Directive, LOCALE_ID, TemplateRef, ContentChildren, ContentChild, inject } from '@angular/core';
2
+ import { Input, Component, EventEmitter, Output, Injectable, ViewChild, NgModule, PLATFORM_ID, Inject, HostBinding, Directive, inject, ElementRef, NgZone, input, output, LOCALE_ID, signal, computed, Injector, viewChild, effect, TemplateRef, ContentChildren, ContentChild } from '@angular/core';
3
3
  import * as i1 from '@jsverse/transloco';
4
4
  import { TranslocoDirective, TranslocoService } from '@jsverse/transloco';
5
5
  import * as i2 from '@angular/common';
6
- import { CommonModule, isPlatformBrowser, formatNumber, formatDate } from '@angular/common';
6
+ import { CommonModule, isPlatformBrowser, formatDate, formatNumber } from '@angular/common';
7
7
  import * as i3 from '@angular/forms';
8
8
  import { FormControl, FormGroup, FormArray, ReactiveFormsModule, FormsModule } from '@angular/forms';
9
9
  import * as i4 from 'primeng/autocomplete';
@@ -26,22 +26,22 @@ import * as i5$1 from 'primeng/inputtext';
26
26
  import { InputTextModule } from 'primeng/inputtext';
27
27
  import * as i4$4 from 'primeng/password';
28
28
  import { PasswordModule } from 'primeng/password';
29
- import * as i4$5 from 'primeng/multiselect';
29
+ import * as i7 from 'primeng/multiselect';
30
30
  import { MultiSelectModule } from 'primeng/multiselect';
31
- import * as i4$6 from 'primeng/textarea';
31
+ import * as i4$5 from 'primeng/textarea';
32
32
  import { TextareaModule } from 'primeng/textarea';
33
- import * as i4$7 from 'primeng/inputnumber';
33
+ import * as i4$6 from 'primeng/inputnumber';
34
34
  import { InputNumberModule } from 'primeng/inputnumber';
35
35
  import * as i3$1 from 'primeng/select';
36
36
  import { SelectModule } from 'primeng/select';
37
- import * as i4$8 from 'primeng/editor';
37
+ import * as i4$7 from 'primeng/editor';
38
38
  import { EditorModule, Editor } from 'primeng/editor';
39
39
  import * as i5$2 from 'primeng/tabs';
40
40
  import { TabsModule } from 'primeng/tabs';
41
41
  import { MarkdownComponent } from 'ngx-markdown';
42
42
  import * as i5$3 from 'primeng/fileupload';
43
43
  import { FileUploadModule } from 'primeng/fileupload';
44
- import * as i12 from 'primeng/button';
44
+ import * as i3$3 from 'primeng/button';
45
45
  import { ButtonModule } from 'primeng/button';
46
46
  import * as i1$4 from 'primeng/splitbutton';
47
47
  import { SplitButtonModule } from 'primeng/splitbutton';
@@ -55,17 +55,17 @@ import { HttpParams, HttpHeaders, HttpErrorResponse, HttpResponse } from '@angul
55
55
  import { InputOtp } from 'primeng/inputotp';
56
56
  import * as i2$1 from 'primeng/menu';
57
57
  import { MenuModule } from 'primeng/menu';
58
- import * as i4$9 from 'primeng/panel';
58
+ import * as i4$8 from 'primeng/panel';
59
59
  import { PanelModule } from 'primeng/panel';
60
60
  import * as i1$3 from 'primeng/skeleton';
61
61
  import { SkeletonModule } from 'primeng/skeleton';
62
62
  import { trigger, state, transition, style, animate } from '@angular/animations';
63
- import * as i7 from 'primeng/avatar';
63
+ import * as i7$1 from 'primeng/avatar';
64
64
  import { AvatarModule } from 'primeng/avatar';
65
65
  import { BadgeModule } from 'primeng/badge';
66
- import * as i4$a from 'primeng/menubar';
66
+ import * as i4$9 from 'primeng/menubar';
67
67
  import { MenubarModule } from 'primeng/menubar';
68
- import * as i14 from 'primeng/popover';
68
+ import * as i8 from 'primeng/popover';
69
69
  import { PopoverModule } from 'primeng/popover';
70
70
  import * as i10 from 'primeng/table';
71
71
  import { TableModule } from 'primeng/table';
@@ -657,6 +657,15 @@ function exportListToExcel(exportListToExcelObservableMethod, filter) {
657
657
  function getPrimengNamebookOptions(namebookList) {
658
658
  return namebookList.map((x) => ({ label: x.displayName, code: x.id }));
659
659
  }
660
+ /**
661
+ * The filter store's twin of `getPrimengNamebookOptions` — same namebook in, the store's
662
+ * `{ value, label }` shape out (a `FilterOption` is not a `PrimengOption`: that one's `code` is
663
+ * deliberate, see `primeng-option.ts`). Every consumer filling a pick-list filter from a lookup
664
+ * needs this map; without it each page re-derives the three lines.
665
+ */
666
+ function getFilterOptions(namebookList) {
667
+ return namebookList.map((x) => ({ value: x.id, label: x.displayName }));
668
+ }
660
669
  function getPrimengDropdownNamebookOptions(getDropdownListObservable, parentEntityId) {
661
670
  return getDropdownListObservable(parentEntityId ?? 0).pipe(map((res) => getPrimengNamebookOptions(res)));
662
671
  }
@@ -1037,7 +1046,7 @@ class SpiderlyMultiSelectComponent extends BaseDropdownControl {
1037
1046
  super.ngOnInit();
1038
1047
  }
1039
1048
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyMultiSelectComponent, deps: [{ token: i1.TranslocoService }], target: i0.ɵɵFactoryTarget.Component }); }
1040
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyMultiSelectComponent, isStandalone: true, selector: "spiderly-multiselect", usesInheritance: true, ngImport: i0, template: "<div style=\"display: flex; flex-direction: column; gap: 0.5rem\">\n <div>\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n <p-multiSelect\n *ngIf=\"control\"\n [id]=\"label\"\n [formControl]=\"control\"\n [options]=\"options\"\n optionLabel=\"displayName\"\n optionValue=\"id\"\n display=\"chip\"\n [maxSelectedLabels]=\"20\"\n [placeholder]=\"placeholder\"\n [fluid]=\"true\"\n appendTo=\"body\"\n />\n <!-- [virtualScroll]=\"true\" FT: I think that this is not doing anything, check -->\n <!-- (onFilter)=\"search($event)\" FT: While they don't implement lazy load like here: https://github.com/yelhouti/primeng/blob/8.0.0-rc2/src/app/components/multiselect/multiselect.ts\n we need to use this and dropdown control only on the client -->\n <small *ngIf=\"control?.errors && control?.dirty\" class=\"spiderly-error-message\">\n {{ getValidationErrrorMessages() }}\n </small>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i4$5.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: "component", type: RequiredComponent, selector: "required" }] }); }
1049
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyMultiSelectComponent, isStandalone: true, selector: "spiderly-multiselect", usesInheritance: true, ngImport: i0, template: "<div style=\"display: flex; flex-direction: column; gap: 0.5rem\">\n <div>\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n <p-multiSelect\n *ngIf=\"control\"\n [id]=\"label\"\n [formControl]=\"control\"\n [options]=\"options\"\n optionLabel=\"displayName\"\n optionValue=\"id\"\n display=\"chip\"\n [maxSelectedLabels]=\"20\"\n [placeholder]=\"placeholder\"\n [fluid]=\"true\"\n appendTo=\"body\"\n />\n <!-- [virtualScroll]=\"true\" FT: I think that this is not doing anything, check -->\n <!-- (onFilter)=\"search($event)\" FT: While they don't implement lazy load like here: https://github.com/yelhouti/primeng/blob/8.0.0-rc2/src/app/components/multiselect/multiselect.ts\n we need to use this and dropdown control only on the client -->\n <small *ngIf=\"control?.errors && control?.dirty\" class=\"spiderly-error-message\">\n {{ getValidationErrrorMessages() }}\n </small>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormsModule }, { 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: "component", type: RequiredComponent, selector: "required" }] }); }
1041
1050
  }
1042
1051
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyMultiSelectComponent, decorators: [{
1043
1052
  type: Component,
@@ -1059,7 +1068,7 @@ class SpiderlyTextareaComponent extends BaseControl {
1059
1068
  super.ngOnInit();
1060
1069
  }
1061
1070
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyTextareaComponent, deps: [{ token: i1.TranslocoService }], target: i0.ɵɵFactoryTarget.Component }); }
1062
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyTextareaComponent, isStandalone: true, selector: "spiderly-textarea", usesInheritance: true, ngImport: i0, template: "<!-- Can't put (onBlur) in this control -->\n\n<div style=\"display: flex; flex-direction: column; gap: 0.5rem\">\n <div>\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n <textarea\n *ngIf=\"control\"\n pTextarea\n [formControl]=\"control\"\n [id]=\"control.label\"\n (blur)=\"control.markAsDirty()\"\n [autoResize]=\"true\"\n [class]=\"control.disabled ? 'disabled' : ''\"\n ></textarea>\n <small *ngIf=\"control?.errors && control?.dirty\" class=\"spiderly-error-message\">\n {{ getValidationErrrorMessages() }}\n </small>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: TextareaModule }, { kind: "directive", type: i4$6.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["autoResize", "variant", "fluid", "pSize"], outputs: ["onResize"] }, { kind: "component", type: RequiredComponent, selector: "required" }] }); }
1071
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyTextareaComponent, isStandalone: true, selector: "spiderly-textarea", usesInheritance: true, ngImport: i0, template: "<!-- Can't put (onBlur) in this control -->\n\n<div style=\"display: flex; flex-direction: column; gap: 0.5rem\">\n <div>\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n <textarea\n *ngIf=\"control\"\n pTextarea\n [formControl]=\"control\"\n [id]=\"control.label\"\n (blur)=\"control.markAsDirty()\"\n [autoResize]=\"true\"\n [class]=\"control.disabled ? 'disabled' : ''\"\n ></textarea>\n <small *ngIf=\"control?.errors && control?.dirty\" class=\"spiderly-error-message\">\n {{ getValidationErrrorMessages() }}\n </small>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: TextareaModule }, { kind: "directive", type: i4$5.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["autoResize", "variant", "fluid", "pSize"], outputs: ["onResize"] }, { kind: "component", type: RequiredComponent, selector: "required" }] }); }
1063
1072
  }
1064
1073
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyTextareaComponent, decorators: [{
1065
1074
  type: Component,
@@ -1084,7 +1093,7 @@ class SpiderlyNumberComponent extends BaseControl {
1084
1093
  super.ngOnInit();
1085
1094
  }
1086
1095
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyNumberComponent, deps: [{ token: i1.TranslocoService }], target: i0.ɵɵFactoryTarget.Component }); }
1087
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyNumberComponent, isStandalone: true, selector: "spiderly-number", inputs: { prefix: "prefix", showButtons: "showButtons", decimal: "decimal", maxFractionDigits: "maxFractionDigits" }, usesInheritance: true, ngImport: i0, template: "<div style=\"display: flex; flex-direction: column; gap: 0.5rem\">\n <div *ngIf=\"showLabel\">\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n <p-inputNumber\n *ngIf=\"control\"\n [formControl]=\"control\"\n [id]=\"control.label\"\n (onBlur)=\"control.markAsDirty()\"\n [prefix]=\"prefix\"\n [style]=\"{ width: '100%' }\"\n [inputStyle]=\"{ width: '100%' }\"\n [showButtons]=\"showButtons\"\n buttonLayout=\"horizontal\"\n [maxFractionDigits]=\"maxFractionDigits\"\n />\n <small *ngIf=\"control?.errors && control?.dirty\" class=\"spiderly-error-message\">\n {{ getValidationErrrorMessages() }}\n </small>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: InputNumberModule }, { kind: "component", type: i4$7.InputNumber, selector: "p-inputNumber, p-inputnumber, p-input-number", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaDescribedBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled", "fluid"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "component", type: RequiredComponent, selector: "required" }] }); }
1096
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyNumberComponent, isStandalone: true, selector: "spiderly-number", inputs: { prefix: "prefix", showButtons: "showButtons", decimal: "decimal", maxFractionDigits: "maxFractionDigits" }, usesInheritance: true, ngImport: i0, template: "<div style=\"display: flex; flex-direction: column; gap: 0.5rem\">\n <div *ngIf=\"showLabel\">\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n <p-inputNumber\n *ngIf=\"control\"\n [formControl]=\"control\"\n [id]=\"control.label\"\n (onBlur)=\"control.markAsDirty()\"\n [prefix]=\"prefix\"\n [style]=\"{ width: '100%' }\"\n [inputStyle]=\"{ width: '100%' }\"\n [showButtons]=\"showButtons\"\n buttonLayout=\"horizontal\"\n [maxFractionDigits]=\"maxFractionDigits\"\n />\n <small *ngIf=\"control?.errors && control?.dirty\" class=\"spiderly-error-message\">\n {{ getValidationErrrorMessages() }}\n </small>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: InputNumberModule }, { kind: "component", type: i4$6.InputNumber, selector: "p-inputNumber, p-inputnumber, p-input-number", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaDescribedBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled", "fluid"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "component", type: RequiredComponent, selector: "required" }] }); }
1088
1097
  }
1089
1098
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyNumberComponent, decorators: [{
1090
1099
  type: Component,
@@ -1193,7 +1202,7 @@ class SpiderlyEditorComponent extends BaseControl {
1193
1202
  };
1194
1203
  }
1195
1204
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyEditorComponent, deps: [{ token: i1.TranslocoService }], target: i0.ɵɵFactoryTarget.Component }); }
1196
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyEditorComponent, isStandalone: true, selector: "spiderly-editor", inputs: { uploadImageMethod: "uploadImageMethod", objectId: "objectId", acceptedFileTypes: "acceptedFileTypes" }, viewQueries: [{ propertyName: "editor", first: true, predicate: Editor, descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Can't put (onBlur) in this control -->\n\n<div style=\"display: flex; flex-direction: column; gap: 0.5rem; padding: 0 1px\">\n <div *ngIf=\"getTranslatedLabel() != '' && getTranslatedLabel() != null\">\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n\n <!-- Disable doesn't work on this control -->\n <p-editor\n *ngIf=\"control\"\n [formControl]=\"control\"\n [readonly]=\"control.disabled\"\n [class]=\"control.invalid && control.dirty ? 'control-error-border' : ''\"\n [id]=\"control.label\"\n [placeholder]=\"placeholder\"\n (click)=\"onClick()\"\n (onInit)=\"onEditorInit($event)\"\n [style]=\"{ height: '320px' }\"\n ></p-editor>\n <small *ngIf=\"control?.errors && control?.dirty\" class=\"spiderly-error-message\">\n {{ getValidationErrrorMessages() }}\n </small>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: EditorModule }, { kind: "component", type: i4$8.Editor, selector: "p-editor", inputs: ["style", "styleClass", "placeholder", "formats", "modules", "bounds", "scrollingContainer", "debug", "readonly"], outputs: ["onInit", "onTextChange", "onSelectionChange"] }, { kind: "component", type: RequiredComponent, selector: "required" }] }); }
1205
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyEditorComponent, isStandalone: true, selector: "spiderly-editor", inputs: { uploadImageMethod: "uploadImageMethod", objectId: "objectId", acceptedFileTypes: "acceptedFileTypes" }, viewQueries: [{ propertyName: "editor", first: true, predicate: Editor, descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Can't put (onBlur) in this control -->\n\n<div style=\"display: flex; flex-direction: column; gap: 0.5rem; padding: 0 1px\">\n <div *ngIf=\"getTranslatedLabel() != '' && getTranslatedLabel() != null\">\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n\n <!-- Disable doesn't work on this control -->\n <p-editor\n *ngIf=\"control\"\n [formControl]=\"control\"\n [readonly]=\"control.disabled\"\n [class]=\"control.invalid && control.dirty ? 'control-error-border' : ''\"\n [id]=\"control.label\"\n [placeholder]=\"placeholder\"\n (click)=\"onClick()\"\n (onInit)=\"onEditorInit($event)\"\n [style]=\"{ height: '320px' }\"\n ></p-editor>\n <small *ngIf=\"control?.errors && control?.dirty\" class=\"spiderly-error-message\">\n {{ getValidationErrrorMessages() }}\n </small>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: EditorModule }, { kind: "component", type: i4$7.Editor, selector: "p-editor", inputs: ["style", "styleClass", "placeholder", "formats", "modules", "bounds", "scrollingContainer", "debug", "readonly"], outputs: ["onInit", "onTextChange", "onSelectionChange"] }, { kind: "component", type: RequiredComponent, selector: "required" }] }); }
1197
1206
  }
1198
1207
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyEditorComponent, decorators: [{
1199
1208
  type: Component,
@@ -1300,7 +1309,7 @@ class SpiderlyMarkdownComponent extends BaseControl {
1300
1309
  this.control.markAsDirty();
1301
1310
  }
1302
1311
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyMarkdownComponent, deps: [{ token: i1.TranslocoService }], target: i0.ɵɵFactoryTarget.Component }); }
1303
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyMarkdownComponent, isStandalone: true, selector: "spiderly-markdown", inputs: { uploadImageMethod: "uploadImageMethod", objectId: "objectId" }, viewQueries: [{ propertyName: "textareaRef", first: true, predicate: ["textarea"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<div style=\"display: flex; flex-direction: column; gap: 0.5rem\">\n <div *ngIf=\"getTranslatedLabel() != '' && getTranslatedLabel() != null\">\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n\n <ng-container *transloco=\"let t\">\n <p-tabs *ngIf=\"control\" value=\"write\">\n <p-tablist>\n <p-tab value=\"write\">{{ t('Write') }}</p-tab>\n <p-tab value=\"preview\">{{ t('Preview') }}</p-tab>\n </p-tablist>\n <p-tabpanels>\n <p-tabpanel value=\"write\">\n <textarea\n #textarea\n pTextarea\n [formControl]=\"control\"\n [id]=\"control.label\"\n (blur)=\"control.markAsDirty()\"\n (paste)=\"onPaste($event)\"\n [autoResize]=\"true\"\n [class]=\"control.disabled ? 'disabled' : ''\"\n [style]=\"{ width: '100%', minHeight: '320px' }\"\n ></textarea>\n <small *ngIf=\"pendingImageUploads > 0\">{{ t('UploadingImage') }}</small>\n <small *ngIf=\"imageUploadFailed\" class=\"spiderly-error-message\">{{ t('ImageUploadFailed') }}</small>\n </p-tabpanel>\n <p-tabpanel value=\"preview\">\n <div class=\"spiderly-markdown-preview\" style=\"min-height: 320px\">\n <markdown [data]=\"control.value\"></markdown>\n </div>\n </p-tabpanel>\n </p-tabpanels>\n </p-tabs>\n </ng-container>\n\n <small *ngIf=\"control?.errors && control?.dirty\" class=\"spiderly-error-message\">\n {{ getValidationErrrorMessages() }}\n </small>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: TextareaModule }, { kind: "directive", type: i4$6.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["autoResize", "variant", "fluid", "pSize"], outputs: ["onResize"] }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5$2.Tabs, selector: "p-tabs", inputs: ["value", "scrollable", "lazy", "selectOnFocus", "showNavigators", "tabindex"], outputs: ["valueChange"] }, { kind: "component", type: i5$2.TabPanels, selector: "p-tabpanels" }, { kind: "component", type: i5$2.TabPanel, selector: "p-tabpanel", inputs: ["value"], outputs: ["valueChange"] }, { kind: "component", type: i5$2.TabList, selector: "p-tablist" }, { kind: "component", type: i5$2.Tab, selector: "p-tab", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "component", type: RequiredComponent, selector: "required" }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] }); }
1312
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyMarkdownComponent, isStandalone: true, selector: "spiderly-markdown", inputs: { uploadImageMethod: "uploadImageMethod", objectId: "objectId" }, viewQueries: [{ propertyName: "textareaRef", first: true, predicate: ["textarea"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<div style=\"display: flex; flex-direction: column; gap: 0.5rem\">\n <div *ngIf=\"getTranslatedLabel() != '' && getTranslatedLabel() != null\">\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n\n <ng-container *transloco=\"let t\">\n <p-tabs *ngIf=\"control\" value=\"write\">\n <p-tablist>\n <p-tab value=\"write\">{{ t('Write') }}</p-tab>\n <p-tab value=\"preview\">{{ t('Preview') }}</p-tab>\n </p-tablist>\n <p-tabpanels>\n <p-tabpanel value=\"write\">\n <textarea\n #textarea\n pTextarea\n [formControl]=\"control\"\n [id]=\"control.label\"\n (blur)=\"control.markAsDirty()\"\n (paste)=\"onPaste($event)\"\n [autoResize]=\"true\"\n [class]=\"control.disabled ? 'disabled' : ''\"\n [style]=\"{ width: '100%', minHeight: '320px' }\"\n ></textarea>\n <small *ngIf=\"pendingImageUploads > 0\">{{ t('UploadingImage') }}</small>\n <small *ngIf=\"imageUploadFailed\" class=\"spiderly-error-message\">{{ t('ImageUploadFailed') }}</small>\n </p-tabpanel>\n <p-tabpanel value=\"preview\">\n <div class=\"spiderly-markdown-preview\" style=\"min-height: 320px\">\n <markdown [data]=\"control.value\"></markdown>\n </div>\n </p-tabpanel>\n </p-tabpanels>\n </p-tabs>\n </ng-container>\n\n <small *ngIf=\"control?.errors && control?.dirty\" class=\"spiderly-error-message\">\n {{ getValidationErrrorMessages() }}\n </small>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: TextareaModule }, { kind: "directive", type: i4$5.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["autoResize", "variant", "fluid", "pSize"], outputs: ["onResize"] }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5$2.Tabs, selector: "p-tabs", inputs: ["value", "scrollable", "lazy", "selectOnFocus", "showNavigators", "tabindex"], outputs: ["valueChange"] }, { kind: "component", type: i5$2.TabPanels, selector: "p-tabpanels" }, { kind: "component", type: i5$2.TabPanel, selector: "p-tabpanel", inputs: ["value"], outputs: ["valueChange"] }, { kind: "component", type: i5$2.TabList, selector: "p-tablist" }, { kind: "component", type: i5$2.Tab, selector: "p-tab", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "component", type: RequiredComponent, selector: "required" }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] }); }
1304
1313
  }
1305
1314
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyMarkdownComponent, decorators: [{
1306
1315
  type: Component,
@@ -1393,7 +1402,7 @@ class SpiderlyButtonComponent extends SpiderlyButtonBaseComponent {
1393
1402
  this.type = 'button';
1394
1403
  }
1395
1404
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyButtonComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
1396
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyButtonComponent, isStandalone: true, selector: "spiderly-button", inputs: { type: "type" }, usesInheritance: true, ngImport: i0, template: "<p-button\n (onClick)=\"handleClick($event)\"\n [label]=\"iconUrl ? undefined : label\"\n [icon]=\"icon\"\n [outlined]=\"outlined\"\n [styleClass]=\"styleClass\"\n [severity]=\"severity\"\n [rounded]=\"rounded\"\n [style]=\"style\"\n [class]=\"class\"\n [disabled]=\"disabled\"\n [size]=\"size\"\n [type]=\"type\"\n>\n <ng-container *ngIf=\"iconUrl\">\n <img [src]=\"iconUrl\" [alt]=\"label\" style=\"height: 16px; width: 16px; margin-right: 8px\" />\n <span>{{ label }}</span>\n </ng-container>\n <ng-content></ng-content>\n</p-button>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i12.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "fluid", "buttonProps"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: SplitButtonModule }] }); }
1405
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyButtonComponent, isStandalone: true, selector: "spiderly-button", inputs: { type: "type" }, usesInheritance: true, ngImport: i0, template: "<p-button\n (onClick)=\"handleClick($event)\"\n [label]=\"iconUrl ? undefined : label\"\n [icon]=\"icon\"\n [outlined]=\"outlined\"\n [styleClass]=\"styleClass\"\n [severity]=\"severity\"\n [rounded]=\"rounded\"\n [style]=\"style\"\n [class]=\"class\"\n [disabled]=\"disabled\"\n [size]=\"size\"\n [type]=\"type\"\n>\n <ng-container *ngIf=\"iconUrl\">\n <img [src]=\"iconUrl\" [alt]=\"label\" style=\"height: 16px; width: 16px; margin-right: 8px\" />\n <span>{{ label }}</span>\n </ng-container>\n <ng-content></ng-content>\n</p-button>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3$3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "fluid", "buttonProps"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: SplitButtonModule }] }); }
1397
1406
  }
1398
1407
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyButtonComponent, decorators: [{
1399
1408
  type: Component,
@@ -2855,7 +2864,7 @@ class SpiderlyPanelComponent {
2855
2864
  this.onRemoveIconClick.next(null);
2856
2865
  }
2857
2866
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2858
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyPanelComponent, isStandalone: false, selector: "spiderly-panel", inputs: { isFirstMultiplePanel: "isFirstMultiplePanel", isMiddleMultiplePanel: "isMiddleMultiplePanel", isLastMultiplePanel: "isLastMultiplePanel", toggleable: "toggleable", toggler: "toggler", collapsed: "collapsed", crudMenu: "crudMenu", showCrudMenu: "showCrudMenu", showRemoveIcon: "showRemoveIcon", index: "index", showPanelHeader: "showPanelHeader" }, outputs: { onMenuIconClick: "onMenuIconClick", onRemoveIconClick: "onRemoveIconClick" }, viewQueries: [{ propertyName: "menu", first: true, predicate: ["menu"], descendants: true }], ngImport: i0, template: "<p-panel\n [showHeader]=\"showPanelHeader\"\n [ngClass]=\"{\n 'multiple-panel-first-without-header':\n isFirstMultiplePanel && !showPanelHeader,\n 'multiple-panel-first': isFirstMultiplePanel,\n 'multiple-panel-middle-without-header':\n isMiddleMultiplePanel && !showPanelHeader,\n 'multiple-panel-middle': isMiddleMultiplePanel,\n 'multiple-panel-last-without-header':\n isLastMultiplePanel && !showPanelHeader,\n 'multiple-panel-last': isLastMultiplePanel,\n 'panel-border': !isFirstMultiplePanel,\n 'spiderly-panel': true,\n 'panel-header-cursor-pointer': toggler === 'header' && toggleable,\n }\"\n [toggleable]=\"toggleable\"\n [toggler]=\"toggler\"\n [collapsed]=\"toggleable && collapsed\"\n expandIcon=\"pi pi-chevron-up\"\n collapseIcon=\"pi pi-chevron-down\"\n>\n <ng-template pTemplate=\"header\">\n <div>\n <ng-content select=\"panel-header\"></ng-content>\n <ng-content select=\"[panelHeader]\"></ng-content>\n </div>\n </ng-template>\n\n <ng-template pTemplate=\"icons\">\n <div *ngIf=\"showCrudMenu && crudMenu\" class=\"mr-2\">\n <i\n class=\"pi pi-ellipsis-h icon-hover\"\n (click)=\"menuItemClick(index, $event)\"\n ></i>\n <!-- https://github.com/primefaces/primeng/issues/13934 -->\n <p-menu\n #menu\n [model]=\"crudMenu\"\n [popup]=\"true\"\n appendTo=\"body\"\n [tabindex]=\"undefined\"\n ></p-menu>\n </div>\n <div *ngIf=\"showRemoveIcon\" class=\"mr-2\">\n <i\n class=\"pi pi-minus icon-hover error-color-light\"\n (click)=\"removeItemClick()\"\n ></i>\n </div>\n </ng-template>\n\n <div class=\"panel-body-wrapper\">\n <ng-content select=\"panel-body\"></ng-content>\n </div>\n\n <ng-content select=\"panel-footer\"></ng-content>\n</p-panel>\n", styles: [":host ::ng-deep .spiderly-panel .p-panel-content{padding:0}:host ::ng-deep .p-panel{overflow:hidden}:host ::ng-deep .p-panel-icons{display:flex;align-items:center}:host ::ng-deep .panel-header-cursor-pointer .p-panel-header{cursor:pointer!important}.multiple-panel-first-without-header .p-panel{border-bottom-left-radius:0;border-bottom-right-radius:0;border-top:1px solid var(--surface-border);border-top-left-radius:var(--p-content-border-radius);border-top-right-radius:var(--p-content-border-radius);border-bottom:none}.multiple-panel-middle-without-header .p-panel{border-radius:0/0px;border-top:1px solid var(--surface-border);border-bottom:none}.multiple-panel-last-without-header .p-panel{border-bottom-left-radius:var(--p-content-border-radius);border-bottom-right-radius:var(--p-content-border-radius);border-top:1px solid var(--surface-border);border-top-left-radius:0;border-top-right-radius:0}.multiple-panel-first .p-panel{border-bottom-left-radius:0;border-bottom-right-radius:0;border-bottom:none}.panel-border .p-panel{border-bottom-left-radius:var(--p-content-border-radius);border-bottom-right-radius:var(--p-content-border-radius);border-bottom:1px solid var(--surface-border)}.multiple-panel-middle .p-panel{border-radius:0;border-bottom:none}.multiple-panel-last .p-panel{border-top-left-radius:0;border-top-right-radius:0}.panel-body-wrapper{padding:18px}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2$1.Menu, selector: "p-menu", inputs: ["model", "popup", "style", "styleClass", "appendTo", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "ariaLabel", "ariaLabelledBy", "id", "tabindex"], outputs: ["onShow", "onHide", "onBlur", "onFocus"] }, { kind: "directive", type: i1$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i4$9.Panel, selector: "p-panel", inputs: ["toggleable", "header", "collapsed", "style", "styleClass", "iconPos", "expandIcon", "collapseIcon", "showHeader", "toggler", "transitionOptions", "toggleButtonProps"], outputs: ["collapsedChange", "onBeforeToggle", "onAfterToggle"] }] }); }
2867
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyPanelComponent, isStandalone: false, selector: "spiderly-panel", inputs: { isFirstMultiplePanel: "isFirstMultiplePanel", isMiddleMultiplePanel: "isMiddleMultiplePanel", isLastMultiplePanel: "isLastMultiplePanel", toggleable: "toggleable", toggler: "toggler", collapsed: "collapsed", crudMenu: "crudMenu", showCrudMenu: "showCrudMenu", showRemoveIcon: "showRemoveIcon", index: "index", showPanelHeader: "showPanelHeader" }, outputs: { onMenuIconClick: "onMenuIconClick", onRemoveIconClick: "onRemoveIconClick" }, viewQueries: [{ propertyName: "menu", first: true, predicate: ["menu"], descendants: true }], ngImport: i0, template: "<p-panel\n [showHeader]=\"showPanelHeader\"\n [ngClass]=\"{\n 'multiple-panel-first-without-header':\n isFirstMultiplePanel && !showPanelHeader,\n 'multiple-panel-first': isFirstMultiplePanel,\n 'multiple-panel-middle-without-header':\n isMiddleMultiplePanel && !showPanelHeader,\n 'multiple-panel-middle': isMiddleMultiplePanel,\n 'multiple-panel-last-without-header':\n isLastMultiplePanel && !showPanelHeader,\n 'multiple-panel-last': isLastMultiplePanel,\n 'panel-border': !isFirstMultiplePanel,\n 'spiderly-panel': true,\n 'panel-header-cursor-pointer': toggler === 'header' && toggleable,\n }\"\n [toggleable]=\"toggleable\"\n [toggler]=\"toggler\"\n [collapsed]=\"toggleable && collapsed\"\n expandIcon=\"pi pi-chevron-up\"\n collapseIcon=\"pi pi-chevron-down\"\n>\n <ng-template pTemplate=\"header\">\n <div>\n <ng-content select=\"panel-header\"></ng-content>\n <ng-content select=\"[panelHeader]\"></ng-content>\n </div>\n </ng-template>\n\n <ng-template pTemplate=\"icons\">\n <div *ngIf=\"showCrudMenu && crudMenu\" class=\"mr-2\">\n <i\n class=\"pi pi-ellipsis-h icon-hover\"\n (click)=\"menuItemClick(index, $event)\"\n ></i>\n <!-- https://github.com/primefaces/primeng/issues/13934 -->\n <p-menu\n #menu\n [model]=\"crudMenu\"\n [popup]=\"true\"\n appendTo=\"body\"\n [tabindex]=\"undefined\"\n ></p-menu>\n </div>\n <div *ngIf=\"showRemoveIcon\" class=\"mr-2\">\n <i\n class=\"pi pi-minus icon-hover error-color-light\"\n (click)=\"removeItemClick()\"\n ></i>\n </div>\n </ng-template>\n\n <div class=\"panel-body-wrapper\">\n <ng-content select=\"panel-body\"></ng-content>\n </div>\n\n <ng-content select=\"panel-footer\"></ng-content>\n</p-panel>\n", styles: [":host ::ng-deep .spiderly-panel .p-panel-content{padding:0}:host ::ng-deep .p-panel{overflow:hidden}:host ::ng-deep .p-panel-icons{display:flex;align-items:center}:host ::ng-deep .panel-header-cursor-pointer .p-panel-header{cursor:pointer!important}.multiple-panel-first-without-header .p-panel{border-bottom-left-radius:0;border-bottom-right-radius:0;border-top:1px solid var(--surface-border);border-top-left-radius:var(--p-content-border-radius);border-top-right-radius:var(--p-content-border-radius);border-bottom:none}.multiple-panel-middle-without-header .p-panel{border-radius:0/0px;border-top:1px solid var(--surface-border);border-bottom:none}.multiple-panel-last-without-header .p-panel{border-bottom-left-radius:var(--p-content-border-radius);border-bottom-right-radius:var(--p-content-border-radius);border-top:1px solid var(--surface-border);border-top-left-radius:0;border-top-right-radius:0}.multiple-panel-first .p-panel{border-bottom-left-radius:0;border-bottom-right-radius:0;border-bottom:none}.panel-border .p-panel{border-bottom-left-radius:var(--p-content-border-radius);border-bottom-right-radius:var(--p-content-border-radius);border-bottom:1px solid var(--surface-border)}.multiple-panel-middle .p-panel{border-radius:0;border-bottom:none}.multiple-panel-last .p-panel{border-top-left-radius:0;border-top-right-radius:0}.panel-body-wrapper{padding:18px}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2$1.Menu, selector: "p-menu", inputs: ["model", "popup", "style", "styleClass", "appendTo", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "ariaLabel", "ariaLabelledBy", "id", "tabindex"], outputs: ["onShow", "onHide", "onBlur", "onFocus"] }, { kind: "directive", type: i1$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i4$8.Panel, selector: "p-panel", inputs: ["toggleable", "header", "collapsed", "style", "styleClass", "iconPos", "expandIcon", "collapseIcon", "showHeader", "toggler", "transitionOptions", "toggleButtonProps"], outputs: ["collapsedChange", "onBeforeToggle", "onAfterToggle"] }] }); }
2859
2868
  }
2860
2869
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyPanelComponent, decorators: [{
2861
2870
  type: Component,
@@ -2953,7 +2962,7 @@ class VerificationWrapperComponent extends BaseFormComponent {
2953
2962
  this.onResendVerificationToken.next(null);
2954
2963
  }
2955
2964
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: VerificationWrapperComponent, deps: [{ token: i0.KeyValueDiffers }, { token: i1$2.HttpClient }, { token: SpiderlyMessageService }, { token: i0.ChangeDetectorRef }, { token: i3$2.Router }, { token: i3$2.ActivatedRoute }, { token: i1.TranslocoService }, { token: BaseFormService }], target: i0.ɵɵFactoryTarget.Component }); }
2956
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.13", type: VerificationWrapperComponent, isStandalone: true, selector: "verification-wrapper", inputs: { email: "email" }, outputs: { onResendVerificationToken: "onResendVerificationToken", onCodeSubmit: "onCodeSubmit" }, usesInheritance: true, ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div\n class=\"min-h-screen\"\n style=\"display: flex; justify-content: center; align-items: center\"\n >\n <div class=\"dialog\" style=\"padding: 0px\">\n @if (verificationTokenRequestFormGroup != null) {\n <spiderly-panel>\n <panel-header\n [title]=\"t('AccountVerificationHeader')\"\n [showBigTitle]=\"true\"\n icon=\"pi pi-envelope\"\n ></panel-header>\n\n <panel-body>\n <form class=\"spiderly-grid\">\n <div class=\"col-8\">\n <div style=\"font-size: 17.5px\">\n {{ t(\"AccountVerificationTitle\") }}\n </div>\n </div>\n <div class=\"col-8\">\n {{ t(\"AccountVerificationDescription\", { email: email }) }}\n </div>\n <div class=\"col-8\">\n <!--\n PrimeNG's InputOtp calls event.preventDefault() on the Enter keydown\n (Enter is a non-digit while integerOnly is on, and once all tokens are\n filled it blocks every key but Delete), which kills the browser's\n implicit submit via the type=\"submit\" button. Wire Enter straight to\n codeSubmit() so \"paste the code + Enter\" still submits. preventDefault\n doesn't stop propagation, so this bubbled keydown listener still fires.\n -->\n <p-inputOtp\n [formControl]=\"verificationTokenRequestFormGroup.getControl('verificationCode')\"\n [length]=\"6\"\n [integerOnly]=\"true\"\n [autofocus]=\"true\"\n (keydown.enter)=\"codeSubmit()\"\n ></p-inputOtp>\n </div>\n <div class=\"col-8\">\n <spiderly-button\n (onClick)=\"codeSubmit()\"\n [label]=\"t('Submit')\"\n icon=\"pi pi-verified\"\n type=\"submit\"\n ></spiderly-button>\n </div>\n </form>\n </panel-body>\n\n <panel-footer>\n <a\n pButton\n class=\"p-button-link\"\n [label]=\"t('GoToGmail')\"\n icon=\"pi pi-external-link\"\n href=\"https://mail.google.com/mail/u/\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n ></a>\n <a\n pButton\n class=\"p-button-link\"\n [label]=\"t('GoToYahoo')\"\n icon=\"pi pi-external-link\"\n href=\"https://mail.yahoo.com/d/folders/1/\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n ></a>\n </panel-footer>\n <panel-footer>\n <div>\n {{ t(\"ResendVerificationCodeFirstPart\") }}\n <a class=\"link\" (click)=\"resendVerificationToken()\">{{\n t(\"ResendVerificationCodeLinkSecondPart\")\n }}</a>\n </div>\n </panel-footer>\n </spiderly-panel>\n } @else {\n <!-- TODO FT: Add skeleton -->\n }\n </div>\n </div>\n</ng-container>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: SpiderlyControlsModule }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "ngmodule", type: SpiderlyPanelsModule }, { kind: "component", type: PanelHeaderComponent, selector: "panel-header", inputs: ["title", "showBigTitle", "showIcon", "icon", "index", "tabs"] }, { kind: "component", type: PanelBodyComponent, selector: "panel-body", inputs: ["normalBottomPadding"] }, { kind: "component", type: PanelFooterComponent, selector: "panel-footer" }, { kind: "component", type: SpiderlyPanelComponent, selector: "spiderly-panel", inputs: ["isFirstMultiplePanel", "isMiddleMultiplePanel", "isLastMultiplePanel", "toggleable", "toggler", "collapsed", "crudMenu", "showCrudMenu", "showRemoveIcon", "index", "showPanelHeader"], outputs: ["onMenuIconClick", "onRemoveIconClick"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i12.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "fluid", "label", "icon", "buttonProps"] }, { kind: "component", type: InputOtp, selector: "p-inputOtp, p-inputotp, p-input-otp", inputs: ["invalid", "disabled", "readonly", "variant", "tabindex", "length", "styleClass", "mask", "integerOnly", "autofocus", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] }); }
2965
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.13", type: VerificationWrapperComponent, isStandalone: true, selector: "verification-wrapper", inputs: { email: "email" }, outputs: { onResendVerificationToken: "onResendVerificationToken", onCodeSubmit: "onCodeSubmit" }, usesInheritance: true, ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div\n class=\"min-h-screen\"\n style=\"display: flex; justify-content: center; align-items: center\"\n >\n <div class=\"dialog\" style=\"padding: 0px\">\n @if (verificationTokenRequestFormGroup != null) {\n <spiderly-panel>\n <panel-header\n [title]=\"t('AccountVerificationHeader')\"\n [showBigTitle]=\"true\"\n icon=\"pi pi-envelope\"\n ></panel-header>\n\n <panel-body>\n <form class=\"spiderly-grid\">\n <div class=\"col-8\">\n <div style=\"font-size: 17.5px\">\n {{ t(\"AccountVerificationTitle\") }}\n </div>\n </div>\n <div class=\"col-8\">\n {{ t(\"AccountVerificationDescription\", { email: email }) }}\n </div>\n <div class=\"col-8\">\n <!--\n PrimeNG's InputOtp calls event.preventDefault() on the Enter keydown\n (Enter is a non-digit while integerOnly is on, and once all tokens are\n filled it blocks every key but Delete), which kills the browser's\n implicit submit via the type=\"submit\" button. Wire Enter straight to\n codeSubmit() so \"paste the code + Enter\" still submits. preventDefault\n doesn't stop propagation, so this bubbled keydown listener still fires.\n -->\n <p-inputOtp\n [formControl]=\"verificationTokenRequestFormGroup.getControl('verificationCode')\"\n [length]=\"6\"\n [integerOnly]=\"true\"\n [autofocus]=\"true\"\n (keydown.enter)=\"codeSubmit()\"\n ></p-inputOtp>\n </div>\n <div class=\"col-8\">\n <spiderly-button\n (onClick)=\"codeSubmit()\"\n [label]=\"t('Submit')\"\n icon=\"pi pi-verified\"\n type=\"submit\"\n ></spiderly-button>\n </div>\n </form>\n </panel-body>\n\n <panel-footer>\n <a\n pButton\n class=\"p-button-link\"\n [label]=\"t('GoToGmail')\"\n icon=\"pi pi-external-link\"\n href=\"https://mail.google.com/mail/u/\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n ></a>\n <a\n pButton\n class=\"p-button-link\"\n [label]=\"t('GoToYahoo')\"\n icon=\"pi pi-external-link\"\n href=\"https://mail.yahoo.com/d/folders/1/\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n ></a>\n </panel-footer>\n <panel-footer>\n <div>\n {{ t(\"ResendVerificationCodeFirstPart\") }}\n <a class=\"link\" (click)=\"resendVerificationToken()\">{{\n t(\"ResendVerificationCodeLinkSecondPart\")\n }}</a>\n </div>\n </panel-footer>\n </spiderly-panel>\n } @else {\n <!-- TODO FT: Add skeleton -->\n }\n </div>\n </div>\n</ng-container>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: SpiderlyControlsModule }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "ngmodule", type: SpiderlyPanelsModule }, { kind: "component", type: PanelHeaderComponent, selector: "panel-header", inputs: ["title", "showBigTitle", "showIcon", "icon", "index", "tabs"] }, { kind: "component", type: PanelBodyComponent, selector: "panel-body", inputs: ["normalBottomPadding"] }, { kind: "component", type: PanelFooterComponent, selector: "panel-footer" }, { kind: "component", type: SpiderlyPanelComponent, selector: "spiderly-panel", inputs: ["isFirstMultiplePanel", "isMiddleMultiplePanel", "isLastMultiplePanel", "toggleable", "toggler", "collapsed", "crudMenu", "showCrudMenu", "showRemoveIcon", "index", "showPanelHeader"], outputs: ["onMenuIconClick", "onRemoveIconClick"] }, { 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: "component", type: InputOtp, selector: "p-inputOtp, p-inputotp, p-input-otp", inputs: ["invalid", "disabled", "readonly", "variant", "tabindex", "length", "styleClass", "mask", "integerOnly", "autofocus", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] }); }
2957
2966
  }
2958
2967
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: VerificationWrapperComponent, decorators: [{
2959
2968
  type: Component,
@@ -3599,7 +3608,7 @@ class ProfileAvatarComponent {
3599
3608
  }
3600
3609
  }
3601
3610
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ProfileAvatarComponent, deps: [{ token: LayoutServiceBase }, { token: AuthServiceBase }, { token: i3$2.Router }, { token: i1.TranslocoService }, { token: ConfigServiceBase }], target: i0.ɵɵFactoryTarget.Component }); }
3602
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: ProfileAvatarComponent, isStandalone: true, selector: "spiderly-profile-avatar", inputs: { isSideMenuLayout: "isSideMenuLayout", routeOnLargeProfileAvatarClick: "routeOnLargeProfileAvatarClick", showLoginButton: "showLoginButton", routeToLoginPage: "routeToLoginPage", loginButtonOutlined: "loginButtonOutlined", loginButtonSeverity: "loginButtonSeverity", loginButtonSize: "loginButtonSize", menuItems: "menuItems" }, outputs: { onLoginButtonClick: "onLoginButtonClick" }, viewQueries: [{ propertyName: "menu", first: true, predicate: ["topbarmenu"], descendants: true }, { propertyName: "topbarProfileDropdownMenuButton", first: true, predicate: ["topbarprofiledropdownmenubutton"], descendants: true }], ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div *ngIf=\"this.currentUser != null\" #topbarmenu>\n <div\n #topbarprofiledropdownmenubutton\n (click)=\"layoutService.showProfileDropdownSidebar()\"\n >\n <p-avatar\n *ngIf=\"showProfileIcon\"\n [label]=\"avatarLabel\"\n [style]=\"{\n 'background-color': 'var(--p-primary-color)',\n color: '#fff',\n cursor: 'pointer',\n width: '34px',\n height: '34px',\n 'font-size': '21px',\n }\"\n />\n </div>\n <div #topbarprofiledropdownmenu (document:click)=\"onDocumentClick($event)\">\n <div\n *ngIf=\"layoutService.state.profileDropdownSidebarVisible\"\n style=\"\n width: 280px;\n position: absolute;\n padding: 15px;\n background: var(--p-content-background);\n \"\n class=\"card\"\n [ngClass]=\"{\n 'side-menu-profile-dialog': isSideMenuLayout,\n 'top-menu-profile-dialog': !isSideMenuLayout,\n }\"\n >\n <div\n style=\"\n display: flex;\n flex-direction: column;\n justify-content: center;\n text-align: center;\n gap: 10px;\n \"\n >\n <p-avatar\n [label]=\"avatarLabel\"\n size=\"xlarge\"\n [style]=\"{\n 'background-color': 'var(--p-primary-color)',\n color: '#fff',\n margin: 'auto',\n cursor: routeOnLargeProfileAvatarClick ? 'pointer' : '',\n }\"\n (click)=\"routeToUserPage()\"\n />\n <div style=\"font-size: 15px\">{{ currentUser?.email }}</div>\n </div>\n <div style=\"margin-top: 15px\">\n <div\n *ngFor=\"let item of menuItems\"\n [style]=\"item.showSeparator ? 'margin-top: 5px;' : ''\"\n >\n <div *ngIf=\"item.showSeparator\" class=\"gray-separator\"></div>\n <div\n (click)=\"item.onClick()\"\n class=\"hover-card\"\n style=\"\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 5px;\n \"\n >\n <i\n class=\"pi pi-fw {{ item.icon }}\"\n style=\"font-size: 16px; position: relative\"\n ></i>\n <div style=\"font-size: 15px\">{{ item.label }}</div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div *ngIf=\"this.currentUser == null && this.showLoginButton\">\n <spiderly-button\n [label]=\"t('Login')\"\n (onClick)=\"loginButtonClick()\"\n icon=\"pi pi-sign-in\"\n [outlined]=\"loginButtonOutlined\"\n [severity]=\"loginButtonSeverity\"\n [size]=\"loginButtonSize\"\n ></spiderly-button>\n </div>\n</ng-container>\n", styles: [".side-menu-profile-dialog{right:26px;top:60px}.top-menu-profile-dialog{right:0;top:40px}\n"], dependencies: [{ 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: "ngmodule", type: RouterModule }, { kind: "ngmodule", type: AvatarModule }, { kind: "component", type: i7.Avatar, selector: "p-avatar", inputs: ["label", "icon", "image", "size", "shape", "style", "styleClass", "ariaLabel", "ariaLabelledBy"], outputs: ["onImageError"] }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] }); }
3611
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: ProfileAvatarComponent, isStandalone: true, selector: "spiderly-profile-avatar", inputs: { isSideMenuLayout: "isSideMenuLayout", routeOnLargeProfileAvatarClick: "routeOnLargeProfileAvatarClick", showLoginButton: "showLoginButton", routeToLoginPage: "routeToLoginPage", loginButtonOutlined: "loginButtonOutlined", loginButtonSeverity: "loginButtonSeverity", loginButtonSize: "loginButtonSize", menuItems: "menuItems" }, outputs: { onLoginButtonClick: "onLoginButtonClick" }, viewQueries: [{ propertyName: "menu", first: true, predicate: ["topbarmenu"], descendants: true }, { propertyName: "topbarProfileDropdownMenuButton", first: true, predicate: ["topbarprofiledropdownmenubutton"], descendants: true }], ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div *ngIf=\"this.currentUser != null\" #topbarmenu>\n <div\n #topbarprofiledropdownmenubutton\n (click)=\"layoutService.showProfileDropdownSidebar()\"\n >\n <p-avatar\n *ngIf=\"showProfileIcon\"\n [label]=\"avatarLabel\"\n [style]=\"{\n 'background-color': 'var(--p-primary-color)',\n color: '#fff',\n cursor: 'pointer',\n width: '34px',\n height: '34px',\n 'font-size': '21px',\n }\"\n />\n </div>\n <div #topbarprofiledropdownmenu (document:click)=\"onDocumentClick($event)\">\n <div\n *ngIf=\"layoutService.state.profileDropdownSidebarVisible\"\n style=\"\n width: 280px;\n position: absolute;\n padding: 15px;\n background: var(--p-content-background);\n \"\n class=\"card\"\n [ngClass]=\"{\n 'side-menu-profile-dialog': isSideMenuLayout,\n 'top-menu-profile-dialog': !isSideMenuLayout,\n }\"\n >\n <div\n style=\"\n display: flex;\n flex-direction: column;\n justify-content: center;\n text-align: center;\n gap: 10px;\n \"\n >\n <p-avatar\n [label]=\"avatarLabel\"\n size=\"xlarge\"\n [style]=\"{\n 'background-color': 'var(--p-primary-color)',\n color: '#fff',\n margin: 'auto',\n cursor: routeOnLargeProfileAvatarClick ? 'pointer' : '',\n }\"\n (click)=\"routeToUserPage()\"\n />\n <div style=\"font-size: 15px\">{{ currentUser?.email }}</div>\n </div>\n <div style=\"margin-top: 15px\">\n <div\n *ngFor=\"let item of menuItems\"\n [style]=\"item.showSeparator ? 'margin-top: 5px;' : ''\"\n >\n <div *ngIf=\"item.showSeparator\" class=\"gray-separator\"></div>\n <div\n (click)=\"item.onClick()\"\n class=\"hover-card\"\n style=\"\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 5px;\n \"\n >\n <i\n class=\"pi pi-fw {{ item.icon }}\"\n style=\"font-size: 16px; position: relative\"\n ></i>\n <div style=\"font-size: 15px\">{{ item.label }}</div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div *ngIf=\"this.currentUser == null && this.showLoginButton\">\n <spiderly-button\n [label]=\"t('Login')\"\n (onClick)=\"loginButtonClick()\"\n icon=\"pi pi-sign-in\"\n [outlined]=\"loginButtonOutlined\"\n [severity]=\"loginButtonSeverity\"\n [size]=\"loginButtonSize\"\n ></spiderly-button>\n </div>\n</ng-container>\n", styles: [".side-menu-profile-dialog{right:26px;top:60px}.top-menu-profile-dialog{right:0;top:40px}\n"], dependencies: [{ 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: "ngmodule", type: RouterModule }, { kind: "ngmodule", type: AvatarModule }, { kind: "component", type: i7$1.Avatar, selector: "p-avatar", inputs: ["label", "icon", "image", "size", "shape", "style", "styleClass", "ariaLabel", "ariaLabelledBy"], outputs: ["onImageError"] }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] }); }
3603
3612
  }
3604
3613
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ProfileAvatarComponent, decorators: [{
3605
3614
  type: Component,
@@ -3705,7 +3714,7 @@ class TopBarComponent {
3705
3714
  }
3706
3715
  }
3707
3716
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: TopBarComponent, deps: [{ token: AuthServiceBase }, { token: ConfigServiceBase }], target: i0.ɵɵFactoryTarget.Component }); }
3708
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: TopBarComponent, isStandalone: true, selector: "spiderly-topbar", inputs: { menu: "menu", showHoverBgOnRootItems: "showHoverBgOnRootItems" }, ngImport: i0, template: "<div class=\"topbar-wrapper\">\n <nav\n class=\"topbar {{\n showHoverBgOnRootItems ? '' : 'show-hover-bg-on-root-items'\n }}\"\n >\n <p-menubar [model]=\"menu\" [style]=\"{ border: '0' }\">\n <ng-template #start>\n <img\n style=\"margin-left: 11px; cursor: pointer\"\n width=\"45\"\n height=\"45\"\n routerLink=\"/\"\n [src]=\"logoPath\"\n alt=\"Logo\"\n />\n </ng-template>\n <ng-template #end>\n <div\n style=\"\n margin-right: 11px;\n display: flex;\n gap: 10px;\n align-items: center;\n position: relative;\n \"\n >\n <div>\n <ng-content select=\"[ACTIONS]\"></ng-content>\n </div>\n <spiderly-profile-avatar\n [isSideMenuLayout]=\"false\"\n ></spiderly-profile-avatar>\n </div>\n </ng-template>\n </p-menubar>\n </nav>\n</div>\n", styles: [".topbar-wrapper{background-color:var(--p-content-background);width:100%;border-bottom:1px solid var(--p-content-border-color);box-shadow:0 4px 6px #0000001a;position:sticky;top:0;left:0;z-index:1000}.topbar{max-width:1100px;margin:auto}::ng-deep .show-hover-bg-on-root-items .p-menubar-root-list>.p-menubar-item>.p-menubar-item-content{background-color:transparent!important}::ng-deep .p-menubar-item-link{background-color:transparent!important;padding:10px!important;font-size:15px!important}::ng-deep .p-menubar-item-link .pi{font-size:15px}::ng-deep .p-menubar-button{width:35px!important;height:35px!important}::ng-deep .p-menubar-button .p-icon{width:16px;height:16px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i3$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: MenubarModule }, { kind: "component", type: i4$a.Menubar, selector: "p-menubar", inputs: ["model", "style", "styleClass", "autoZIndex", "baseZIndex", "autoDisplay", "autoHide", "breakpoint", "autoHideDelay", "id", "ariaLabel", "ariaLabelledBy"], outputs: ["onFocus", "onBlur"] }, { kind: "ngmodule", type: AvatarModule }, { kind: "component", type: ProfileAvatarComponent, selector: "spiderly-profile-avatar", inputs: ["isSideMenuLayout", "routeOnLargeProfileAvatarClick", "showLoginButton", "routeToLoginPage", "loginButtonOutlined", "loginButtonSeverity", "loginButtonSize", "menuItems"], outputs: ["onLoginButtonClick"] }] }); }
3717
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: TopBarComponent, isStandalone: true, selector: "spiderly-topbar", inputs: { menu: "menu", showHoverBgOnRootItems: "showHoverBgOnRootItems" }, ngImport: i0, template: "<div class=\"topbar-wrapper\">\n <nav\n class=\"topbar {{\n showHoverBgOnRootItems ? '' : 'show-hover-bg-on-root-items'\n }}\"\n >\n <p-menubar [model]=\"menu\" [style]=\"{ border: '0' }\">\n <ng-template #start>\n <img\n style=\"margin-left: 11px; cursor: pointer\"\n width=\"45\"\n height=\"45\"\n routerLink=\"/\"\n [src]=\"logoPath\"\n alt=\"Logo\"\n />\n </ng-template>\n <ng-template #end>\n <div\n style=\"\n margin-right: 11px;\n display: flex;\n gap: 10px;\n align-items: center;\n position: relative;\n \"\n >\n <div>\n <ng-content select=\"[ACTIONS]\"></ng-content>\n </div>\n <spiderly-profile-avatar\n [isSideMenuLayout]=\"false\"\n ></spiderly-profile-avatar>\n </div>\n </ng-template>\n </p-menubar>\n </nav>\n</div>\n", styles: [".topbar-wrapper{background-color:var(--p-content-background);width:100%;border-bottom:1px solid var(--p-content-border-color);box-shadow:0 4px 6px #0000001a;position:sticky;top:0;left:0;z-index:1000}.topbar{max-width:1100px;margin:auto}::ng-deep .show-hover-bg-on-root-items .p-menubar-root-list>.p-menubar-item>.p-menubar-item-content{background-color:transparent!important}::ng-deep .p-menubar-item-link{background-color:transparent!important;padding:10px!important;font-size:15px!important}::ng-deep .p-menubar-item-link .pi{font-size:15px}::ng-deep .p-menubar-button{width:35px!important;height:35px!important}::ng-deep .p-menubar-button .p-icon{width:16px;height:16px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i3$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: MenubarModule }, { kind: "component", type: i4$9.Menubar, selector: "p-menubar", inputs: ["model", "style", "styleClass", "autoZIndex", "baseZIndex", "autoDisplay", "autoHide", "breakpoint", "autoHideDelay", "id", "ariaLabel", "ariaLabelledBy"], outputs: ["onFocus", "onBlur"] }, { kind: "ngmodule", type: AvatarModule }, { kind: "component", type: ProfileAvatarComponent, selector: "spiderly-profile-avatar", inputs: ["isSideMenuLayout", "routeOnLargeProfileAvatarClick", "showLoginButton", "routeToLoginPage", "loginButtonOutlined", "loginButtonSeverity", "loginButtonSize", "menuItems"], outputs: ["onLoginButtonClick"] }] }); }
3709
3718
  }
3710
3719
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: TopBarComponent, decorators: [{
3711
3720
  type: Component,
@@ -3878,7 +3887,7 @@ class NotFoundComponent {
3878
3887
  this.logoPath = this.config.logoPath;
3879
3888
  }
3880
3889
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: NotFoundComponent, deps: [{ token: ConfigServiceBase }], target: i0.ɵɵFactoryTarget.Component }); }
3881
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: NotFoundComponent, isStandalone: true, selector: "not-found", ngImport: i0, template: "<div class=\"flex min-h-screen overflow-hidden\" style=\"padding: 20px\">\n <div class=\"flex flex-col w-full\">\n <div\n class=\"w-full sm:w-[30rem]\"\n style=\"\n margin: auto;\n border-radius: 50px;\n padding: 0.3rem;\n background: linear-gradient(\n 180deg,\n var(--p-primary-color) 10%,\n rgba(33, 150, 243, 0) 30%\n );\n \"\n >\n <div class=\"surface-card py-12 px-8 sm:px-12\" style=\"border-radius: 45px\">\n <div class=\"text-center\">\n <img\n [src]=\"logoPath\"\n alt=\"{{ companyName }} Logo\"\n title=\"{{ companyName }} Logo\"\n height=\"60\"\n class=\"mb-6\"\n />\n </div>\n <h1 class=\"mb-6\">\n <div class=\"font-bold text-5xl lg:text-6xl spiderly-gradient-title\">\n 404\n </div>\n <div class=\"font-bold text-5xl lg:text-6xl\">Page Not Found</div>\n </h1>\n <div class=\"text-gray-600 text-lg mb-8\">\n The page you're looking for doesn't exist or has been moved. Let's get\n you back on track.\n </div>\n <div>\n <p-button [routerLink]=\"['/']\" label=\"Go to Homepage\"></p-button>\n </div>\n </div>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i3$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i12.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "fluid", "buttonProps"], outputs: ["onClick", "onFocus", "onBlur"] }] }); }
3890
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: NotFoundComponent, isStandalone: true, selector: "not-found", ngImport: i0, template: "<div class=\"flex min-h-screen overflow-hidden\" style=\"padding: 20px\">\n <div class=\"flex flex-col w-full\">\n <div\n class=\"w-full sm:w-[30rem]\"\n style=\"\n margin: auto;\n border-radius: 50px;\n padding: 0.3rem;\n background: linear-gradient(\n 180deg,\n var(--p-primary-color) 10%,\n rgba(33, 150, 243, 0) 30%\n );\n \"\n >\n <div class=\"surface-card py-12 px-8 sm:px-12\" style=\"border-radius: 45px\">\n <div class=\"text-center\">\n <img\n [src]=\"logoPath\"\n alt=\"{{ companyName }} Logo\"\n title=\"{{ companyName }} Logo\"\n height=\"60\"\n class=\"mb-6\"\n />\n </div>\n <h1 class=\"mb-6\">\n <div class=\"font-bold text-5xl lg:text-6xl spiderly-gradient-title\">\n 404\n </div>\n <div class=\"font-bold text-5xl lg:text-6xl\">Page Not Found</div>\n </h1>\n <div class=\"text-gray-600 text-lg mb-8\">\n The page you're looking for doesn't exist or has been moved. Let's get\n you back on track.\n </div>\n <div>\n <p-button [routerLink]=\"['/']\" label=\"Go to Homepage\"></p-button>\n </div>\n </div>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i3$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3$3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "fluid", "buttonProps"], outputs: ["onClick", "onFocus", "onBlur"] }] }); }
3882
3891
  }
3883
3892
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: NotFoundComponent, decorators: [{
3884
3893
  type: Component,
@@ -3961,15 +3970,747 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
3961
3970
  }]
3962
3971
  }] });
3963
3972
 
3964
- var MatchModeCodes;
3965
- (function (MatchModeCodes) {
3966
- MatchModeCodes["StartsWith"] = "startsWith";
3967
- MatchModeCodes["Contains"] = "contains";
3968
- MatchModeCodes["Equals"] = "equals";
3969
- MatchModeCodes["LessThan"] = "lessThan";
3970
- MatchModeCodes["GreaterThan"] = "greaterThan";
3971
- MatchModeCodes["In"] = "in";
3972
- })(MatchModeCodes || (MatchModeCodes = {}));
3973
+ /**
3974
+ * Puts a native `title` on an element while its text is clipped — measured when the pointer
3975
+ * arrives, which is the only moment a native title can surface.
3976
+ *
3977
+ * The data table clamps a default cell to one line (see the `.cell-text` rule), which hides the
3978
+ * value and, before this, offered nothing to read it with — the reason PACMS hand-added `[title]`
3979
+ * on four cells of one grid. Wrapping the column is a choice someone makes; this covers the
3980
+ * default nobody touched.
3981
+ *
3982
+ * Native `title`, never `pTooltip`: it needs a deliberate hover rather than firing as the cursor
3983
+ * crosses a dense list, which is what got the SKU tooltip removed.
3984
+ *
3985
+ * **Measured on hover, not watched.** The first shape ran `ngAfterViewChecked` on every instance
3986
+ * every change-detection cycle (reading `textContent`, which allocates a fresh string each call)
3987
+ * and gave each instance its OWN `ResizeObserver`. On a 100-row grid nineteen columns wide that is
3988
+ * ~1900 hooks per cycle and ~1900 observers in Chrome's per-frame loop — all to decide something
3989
+ * that can only ever be seen one cell at a time. Hover is exactly-in-time, costs nothing until it
3990
+ * happens, and needs no separate handling for a column drag or a resized window: the next hover
3991
+ * re-measures. Nothing is lost for a screen reader either — CSS clipping never hid the text from
3992
+ * the accessibility tree, so the title was only ever a sighted-hover affordance.
3993
+ */
3994
+ class SpiderlyOverflowTitleDirective {
3995
+ constructor() {
3996
+ this.el = inject(ElementRef);
3997
+ this.zone = inject(NgZone);
3998
+ this.onPointerEnter = () => this.sync();
3999
+ // Outside the zone: it sets an attribute and touches no bound state, so waking change
4000
+ // detection on every cell the cursor crosses would be pure cost.
4001
+ this.zone.runOutsideAngular(() => this.el.nativeElement.addEventListener('pointerenter', this.onPointerEnter));
4002
+ }
4003
+ ngOnDestroy() {
4004
+ this.el.nativeElement.removeEventListener('pointerenter', this.onPointerEnter);
4005
+ }
4006
+ sync() {
4007
+ const el = this.el.nativeElement;
4008
+ // A one-pixel slack: sub-pixel text metrics make scrollWidth exceed clientWidth by fractions
4009
+ // on text that visibly fits, which would title half the grid.
4010
+ if (el.scrollWidth - el.clientWidth > 1) {
4011
+ el.setAttribute('title', el.textContent ?? '');
4012
+ }
4013
+ else {
4014
+ el.removeAttribute('title');
4015
+ }
4016
+ }
4017
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyOverflowTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
4018
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyOverflowTitleDirective, isStandalone: true, selector: "[spiderlyOverflowTitle]", ngImport: i0 }); }
4019
+ }
4020
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyOverflowTitleDirective, decorators: [{
4021
+ type: Directive,
4022
+ args: [{
4023
+ selector: '[spiderlyOverflowTitle]',
4024
+ }]
4025
+ }], ctorParameters: () => [] });
4026
+
4027
+ /**
4028
+ * Renders the editor for ONE filter of a `spiderly-data-table`'s bar, replacing the control the
4029
+ * bar would have drawn for that filter's kind. Bind it to the filter's id; filters with no
4030
+ * matching template keep the built-in control, so a table opts in one filter at a time.
4031
+ *
4032
+ * This is the narrow job the directive has. Placing a filter ANYWHERE else — a drawer, a modal,
4033
+ * a header cell — needs no directive at all: `store.get(id)` returns the same handle and depends
4034
+ * on nothing in the component tree, which is why the store is the consumer's to own.
4035
+ */
4036
+ class SpiderlyFilterTemplateDirective {
4037
+ constructor(template) {
4038
+ this.template = template;
4039
+ }
4040
+ /**
4041
+ * Without this the `let-` variable is `any` at every consumer, however well the context is
4042
+ * declared — a directive's injected `TemplateRef<C>` never reaches the template that declares
4043
+ * it. Same mechanism as `SpiderlyCellTemplateDirective`.
4044
+ */
4045
+ static ngTemplateContextGuard(dir, ctx) {
4046
+ return true;
4047
+ }
4048
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyFilterTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); }
4049
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyFilterTemplateDirective, isStandalone: true, selector: "ng-template[spiderlyFilterTemplate]", inputs: { filterId: ["spiderlyFilterTemplate", "filterId"] }, ngImport: i0 }); }
4050
+ }
4051
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyFilterTemplateDirective, decorators: [{
4052
+ type: Directive,
4053
+ args: [{
4054
+ selector: 'ng-template[spiderlyFilterTemplate]',
4055
+ }]
4056
+ }], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { filterId: [{
4057
+ type: Input,
4058
+ args: ['spiderlyFilterTemplate']
4059
+ }] } });
4060
+
4061
+ /** One option's word for one value — the honest raw value when no option matches (failed lookup). */
4062
+ function spellOptionValue(options, value) {
4063
+ return String(options?.find((option) => option.value === value)?.label ?? value);
4064
+ }
4065
+ /**
4066
+ * Lowercased and stripped of diacritics for matching. NFD decomposition handles č/ć/š/ž, but NOT
4067
+ * đ — it is its own letter (U+0111), not a d carrying a mark, so it decomposes to nothing and has
4068
+ * to be mapped by hand.
4069
+ *
4070
+ * It maps to "dj", which is what the rest of this workspace already does (pa-cms's
4071
+ * `normalizePlaceQuery` and `toAscii`, the backend's `PlaceNameNormalizer`) and what someone
4072
+ * without a Serbian layout actually types. Mapping it to "d" — as this first shipped — folds
4073
+ * "Đorđe" to "dorde", so typing "djordje" finds nothing: the exact case the comment claimed to
4074
+ * fix, and one no spec covered because the test used "drzava", which has no đ in it.
4075
+ */
4076
+ function foldForSearch(value) {
4077
+ return value
4078
+ .normalize('NFD')
4079
+ .replace(/[\u0300-\u036f]/g, '')
4080
+ .replace(/\u0111/g, 'dj')
4081
+ .replace(/\u0110/g, 'Dj')
4082
+ .toLowerCase()
4083
+ .trim();
4084
+ }
4085
+ /**
4086
+ * The visible home of every applied constraint. This is what licenses a hidden column keeping its
4087
+ * filter: the header is no longer the only place a filter can be seen, so hiding a column stops
4088
+ * meaning "silently drop what it was filtering by".
4089
+ *
4090
+ * Everything drawn here comes from `applied()`, never from a draft — a chip over a half-typed
4091
+ * value would repeat the mistake the header's filter icon shipped, claiming the grid is narrowed
4092
+ * while it still shows every row.
4093
+ */
4094
+ class SpiderlyFilterBarComponent {
4095
+ constructor() {
4096
+ this.filters = input.required();
4097
+ /** The current query's row count, supplied by whoever owns the query. Null hides the counter. */
4098
+ this.totalRecords = input(null);
4099
+ /**
4100
+ * What the grid is ordered by, in priority order and already LABELLED — the bar knows nothing
4101
+ * about columns, so whoever owns the sort resolves the names.
4102
+ */
4103
+ this.sort = input([]);
4104
+ /** Asked for, never done here — see the button's comment. */
4105
+ this.clearAll = output();
4106
+ this.transloco = inject(TranslocoService);
4107
+ /** For the chip's date wording — the same locale mechanism the table's cells format with. */
4108
+ this.locale = inject(LOCALE_ID);
4109
+ /**
4110
+ * Controls a consumer projected for specific filters, collected by whoever hosts this bar. A
4111
+ * filter with no template keeps the control the bar draws for its kind, so a table opts in one
4112
+ * filter at a time.
4113
+ */
4114
+ this.controlTemplates = input(undefined);
4115
+ this.isAddOpen = signal(false);
4116
+ /** Cleared when the popover closes, so reopening never starts inside someone's old query. */
4117
+ this.addSearch = signal('');
4118
+ /** The filter whose control is open. One at a time: the bar edits, it is not a form. */
4119
+ this.editing = signal(null);
4120
+ /**
4121
+ * `addable` narrowed by the search box. Matching is unaccented and case-folded because that is
4122
+ * how the label gets typed: "drzava" for "Država" on any keyboard without a Serbian layout. The
4123
+ * backend's own product search is unaccented for the same reason.
4124
+ */
4125
+ this.offered = computed(() => {
4126
+ const needle = foldForSearch(this.addSearch());
4127
+ if (!needle)
4128
+ return this.addable();
4129
+ return this.addable().filter((option) => foldForSearch(option.label).includes(needle));
4130
+ });
4131
+ /**
4132
+ * What "+ Filter" offers: every declared filter that is not already on the bar. Sourced from the
4133
+ * DEFINITIONS, so a filter reaches this list whether or not it has a column, and whether or not
4134
+ * that column is visible. That is the whole reason the bar exists.
4135
+ *
4136
+ * `offered: false` filters stay out — they have a dedicated control of their own on the page
4137
+ * (PACMS's order search box), so the generic entry point would give one question two homes.
4138
+ * Their chips still render: the applied list above reads no such flag.
4139
+ */
4140
+ this.addable = computed(() => {
4141
+ const onBar = new Set(this.filters().applied().map((chip) => chip.id));
4142
+ return Object.entries(this.filters().definitions)
4143
+ .filter(([id, definition]) => !onBar.has(id) && definition.offered !== false)
4144
+ .map(([id, definition]) => ({ id, ...definition }));
4145
+ });
4146
+ /**
4147
+ * The open editor's operator options, already translated — p-select takes labels, not keys.
4148
+ *
4149
+ * A `computed`, not a method in the binding: both inputs are fixed for the life of an open
4150
+ * editor, and PrimeNG's `Select.options` setter deep-compares whatever it is handed — so a
4151
+ * freshly mapped array per change-detection pass paid for a recursive structural compare to
4152
+ * conclude nothing had changed.
4153
+ */
4154
+ this.operatorChoices = computed(() => {
4155
+ const handle = this.editing();
4156
+ if (!handle)
4157
+ return [];
4158
+ return handle.operators.map((option) => ({
4159
+ label: this.transloco.translate(option.labelKey),
4160
+ value: option.value,
4161
+ }));
4162
+ });
4163
+ }
4164
+ /** Opens a filter's control. Where it renders, and why not a popover: the template comment. */
4165
+ startEditing(id) {
4166
+ this.isAddOpen.set(false);
4167
+ this.editing.set(this.filters().get(id));
4168
+ }
4169
+ /** The raw-string controls only. Nothing reaches the bar or the query until `apply`. */
4170
+ draft(handle, event) {
4171
+ this.draftValue(handle, this.coerce(handle.kind, event.target.value));
4172
+ }
4173
+ controlTemplateFor(handle) {
4174
+ const match = this.controlTemplates()?.find((candidate) => candidate.filterId === handle.id);
4175
+ return match?.template ?? null;
4176
+ }
4177
+ /**
4178
+ * The ONE write path, whatever the control. Every PrimeNG control here hands back a value of
4179
+ * the right type already — a Date from the datepicker, a boolean from the checkbox, the whole
4180
+ * array from the multiselect — so only the raw string ones need coercing, and only they go
4181
+ * through `draft`.
4182
+ */
4183
+ draftValue(handle, value) {
4184
+ handle.set({
4185
+ operator: handle.operator() ?? handle.defaultOperator,
4186
+ value,
4187
+ });
4188
+ }
4189
+ /**
4190
+ * Changing the direction re-commits nothing on its own — it rewrites the draft, so the operator
4191
+ * survives until Apply like the value does.
4192
+ */
4193
+ pickOperator(handle, operator) {
4194
+ handle.set({ operator, value: handle.value() });
4195
+ }
4196
+ /**
4197
+ * A DOM control hands back a string; the store holds the value type the filter declared.
4198
+ *
4199
+ * The empty case is the trap and it is not symmetric: `Number('')` is 0, so an emptied number
4200
+ * box would apply a filter for zero and draw a chip over a control the operator had just
4201
+ * cleared. `NaN` is folded in for the same reason — the store's `isBlank` cannot recognise
4202
+ * either of them as "nothing was entered", because by then they are perfectly good numbers.
4203
+ */
4204
+ coerce(kind, raw) {
4205
+ if (kind !== 'number')
4206
+ return raw;
4207
+ if (raw.trim() === '')
4208
+ return null;
4209
+ const parsed = Number(raw);
4210
+ return Number.isNaN(parsed) ? null : parsed;
4211
+ }
4212
+ apply(handle) {
4213
+ handle.commit();
4214
+ this.editing.set(null);
4215
+ }
4216
+ /**
4217
+ * `In` is the multi-valued operator, so its chip reads as a list — in the OPTIONS' words, not
4218
+ * the wire's: "Status is one of 2, 3" narrates ids nobody chose by number. The lookup is live
4219
+ * off the definition rather than snapshotted at commit, because option lists can be filled
4220
+ * asynchronously (PACMS's admin/backend deploy race): a chip restored before the lookup answers
4221
+ * upgrades from ids to labels the moment the options land, and a failed lookup degrades to the
4222
+ * honest raw value.
4223
+ */
4224
+ chipValue(chip) {
4225
+ // `String(date)` is the full JS toString — a GMT offset and a timezone name on a chip an
4226
+ // operator scans. Same formatDate + LOCALE_ID mechanism the table's cells use; mediumDate,
4227
+ // because shortDate's two-digit year reads badly on a chip claiming a boundary.
4228
+ if (chip.value instanceof Date) {
4229
+ return formatDate(chip.value, 'mediumDate', this.locale);
4230
+ }
4231
+ const options = this.filters().definitions[chip.id]?.options;
4232
+ return Array.isArray(chip.value)
4233
+ ? chip.value.map((value) => spellOptionValue(options, value)).join(', ')
4234
+ : spellOptionValue(options, chip.value);
4235
+ }
4236
+ 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: `
4238
+ <ng-container *transloco="let t">
4239
+ <div class="filter-bar" data-testid="filter-bar">
4240
+ @for (chip of filters().applied(); track chip.id) {
4241
+ <span class="filter-chip" data-testid="filter-chip">
4242
+ <!-- The chip body reopens the filter. Once applied it is no longer offered under
4243
+ "+ Filter", so without this the only way to narrow Elektromont to Elektro is to
4244
+ delete the filter and rebuild it. -->
4245
+ <button
4246
+ type="button"
4247
+ class="filter-chip-edit"
4248
+ data-testid="filter-chip-edit"
4249
+ (click)="startEditing(chip.id)"
4250
+ >
4251
+ <!-- &ngsp; is a space the template parser keeps. Angular collapses whitespace
4252
+ between elements by default, which glued these three into
4253
+ "FirmacontainsElektromont" — how a screen reader reads the chip and how it
4254
+ lands when copied, not merely how the spec saw it. The flex gap only spaced
4255
+ them visually. -->
4256
+ <span class="filter-chip-label">{{ chip.label }}</span>&ngsp;
4257
+ <span class="filter-chip-operator">{{
4258
+ t(chip.operatorPhraseKey)
4259
+ }}</span>&ngsp;
4260
+ <span class="filter-chip-value">{{
4261
+ chip.kind === 'boolean'
4262
+ ? chip.value
4263
+ ? t('Yes')
4264
+ : t('No')
4265
+ : chipValue(chip)
4266
+ }}</span>
4267
+ </button>
4268
+ <button
4269
+ type="button"
4270
+ class="filter-chip-remove"
4271
+ data-testid="filter-chip-remove"
4272
+ [attr.aria-label]="t('RemoveFilter')"
4273
+ (click)="filters().reset(chip.id)"
4274
+ >
4275
+ &times;
4276
+ </button>
4277
+ </span>
4278
+ }
4279
+
4280
+ @if (sort().length) {
4281
+ <!-- Read-only, and that is the scope on purpose. Multi-sort is invisible today (sort
4282
+ icons scattered over headers, nothing saying which is primary), so saying it in one
4283
+ place is the whole gain. An x here would have to mean "back to the DEFAULT sort"
4284
+ rather than "no sort", because applyDefaultSortIfUnsorted puts the default straight
4285
+ back — an affordance that lies is worse than none. -->
4286
+ <span class="sort-chip" data-testid="sort-chip">
4287
+ <span class="sort-chip-label">{{ t('SortedBy') }}</span>
4288
+ @for (key of sort(); track key.label; let last = $last) {
4289
+ <span class="sort-chip-key"
4290
+ >{{ key.label }} {{ key.descending ? '↓' : '↑' }}{{
4291
+ last ? '' : ','
4292
+ }}</span
4293
+ >
4294
+ }
4295
+ </span>
4296
+ }
4297
+
4298
+ @if (totalRecords() !== null) {
4299
+ <!-- What came back, next to the chips that say what was asked. An empty grid under a
4300
+ filter is otherwise a mystery. The CURRENT query's count only: the unfiltered total
4301
+ would cost a second request the paginator does not offer. -->
4302
+ <span class="filter-bar-count" data-testid="filter-bar-count">{{
4303
+ t('ResultCount', { count: totalRecords() })
4304
+ }}</span>
4305
+ }
4306
+
4307
+ @if (addable().length) {
4308
+ <!-- A popover, not an inline list. Seven filters already fill the toolbar row inline,
4309
+ and the orders grid declares nineteen — they would wrap into three lines and shove
4310
+ the table down the page. Shopify's Filters component opens its "more filters" the
4311
+ same way. The teleport into document.body costs the specs a container lookup; that
4312
+ cost belongs to the tests, not to the operator. -->
4313
+ <button
4314
+ pButton
4315
+ type="button"
4316
+ class="p-button-text p-button-sm filter-add"
4317
+ icon="pi pi-plus"
4318
+ [label]="t('AddFilter')"
4319
+ data-testid="add-filter"
4320
+ [attr.aria-expanded]="isAddOpen()"
4321
+ (click)="addMenu.toggle($event); isAddOpen.set(!isAddOpen())"
4322
+ ></button>
4323
+ }
4324
+
4325
+ @if (filters().applied().length) {
4326
+ <!-- The bar owns clearing now: the toolbar's button sat a row away from the chips it
4327
+ cleared, which is two affordances for one job. Emits rather than clearing the store
4328
+ itself, because whoever owns the query also owns the persisted state and the sort
4329
+ that a full clear has to reach. -->
4330
+ <button
4331
+ pButton
4332
+ type="button"
4333
+ class="p-button-text p-button-sm filter-bar-clear"
4334
+ icon="pi pi-filter-slash"
4335
+ [label]="t('ClearFilters')"
4336
+ data-testid="filter-bar-clear"
4337
+ (click)="clearAll.emit()"
4338
+ ></button>
4339
+ }
4340
+
4341
+ <p-popover #addMenu (onHide)="isAddOpen.set(false); addSearch.set('')">
4342
+ <div class="filter-add-menu" role="menu">
4343
+ <input
4344
+ pInputText
4345
+ pSize="small"
4346
+ type="search"
4347
+ class="filter-add-search"
4348
+ data-testid="add-filter-search"
4349
+ [value]="addSearch()"
4350
+ [attr.aria-label]="t('AddFilter')"
4351
+ [placeholder]="t('Search')"
4352
+ (input)="addSearch.set($any($event.target).value)"
4353
+ />
4354
+
4355
+ @for (option of offered(); track option.id) {
4356
+ <button
4357
+ type="button"
4358
+ role="menuitem"
4359
+ class="filter-add-option"
4360
+ data-testid="add-filter-option"
4361
+ (click)="startEditing(option.id); addMenu.hide()"
4362
+ >
4363
+ {{ option.label }}
4364
+ </button>
4365
+ }
4366
+ </div>
4367
+ </p-popover>
4368
+
4369
+ </div>
4370
+
4371
+ <!-- Its OWN ROW, directly under the chips. It used to render at the end of the bar, where
4372
+ the clear button's auto left margin pushed everything after it to the far right, so the
4373
+ control for the second filter opened across the screen from the click (Filip, on
4374
+ /tags). A popover anchored to the click was tried first and abandoned: PrimeNG's
4375
+ ng-content slot does not re-project content once the editor's @if has destroyed it, so
4376
+ reopening gave an empty container with overlayVisible true. A fixed row is predictable,
4377
+ which is the property the complaint was actually about.
4378
+ NO BACKTICKS IN THIS TEMPLATE: it is a JS template literal and one terminates it, with
4379
+ the error landing lines away. This is the second time. -->
4380
+ @if (editing(); as handle) {
4381
+ <div class="filter-editor" data-testid="filter-editor">
4382
+ <span class="filter-editor-label">{{ handle.label }}</span>
4383
+
4384
+ @if (handle.operators.length > 1) {
4385
+ <p-select
4386
+ size="small"
4387
+ styleClass="filter-editor-operator"
4388
+ data-testid="filter-editor-operator"
4389
+ [options]="operatorChoices()"
4390
+ optionLabel="label"
4391
+ optionValue="value"
4392
+ [ngModel]="handle.operator() ?? handle.defaultOperator"
4393
+ (onChange)="pickOperator(handle, $event.value)"
4394
+ ></p-select>
4395
+ }
4396
+
4397
+ @if (controlTemplateFor(handle); as projected) {
4398
+ <!-- The consumer's own control for this filter. Read the QueryList live rather than
4399
+ indexing it into a Map: a QueryList already updates itself, and a template
4400
+ declared inside a @for or an @if would go stale in a cached index. Same
4401
+ reasoning as getCellTemplate. -->
4402
+ <ng-container
4403
+ *ngTemplateOutlet="projected; context: { $implicit: handle }"
4404
+ ></ng-container>
4405
+ } @else if (handle.options) {
4406
+ <p-multiSelect
4407
+ size="small"
4408
+ styleClass="filter-editor-value"
4409
+ data-testid="filter-editor-value"
4410
+ [options]="handle.options"
4411
+ optionLabel="label"
4412
+ optionValue="value"
4413
+ [placeholder]="t('All')"
4414
+ [ngModel]="handle.value()"
4415
+ (onChange)="draftValue(handle, $event.value)"
4416
+ ></p-multiSelect>
4417
+ } @else if (handle.kind === 'boolean') {
4418
+ <p-checkbox
4419
+ styleClass="filter-editor-value"
4420
+ data-testid="filter-editor-value"
4421
+ [binary]="true"
4422
+ [ngModel]="handle.value() === true"
4423
+ (onChange)="draftValue(handle, $event.checked)"
4424
+ ></p-checkbox>
4425
+ } @else if (handle.kind === 'date') {
4426
+ <p-datepicker
4427
+ size="small"
4428
+ styleClass="filter-editor-value"
4429
+ data-testid="filter-editor-value"
4430
+ [ngModel]="handle.value()"
4431
+ (onSelect)="draftValue(handle, $event)"
4432
+ ></p-datepicker>
4433
+ } @else if (handle.kind === 'text' || handle.kind === 'number') {
4434
+ <!-- A DOM control's raw value is always a string. It is coerced on the way into
4435
+ the store (see coerce), which is the one place the value type can be got
4436
+ right. Backticks are forbidden in this template: it is a JS template literal
4437
+ and one terminates it, with the error landing lines away. -->
4438
+ <input
4439
+ pInputText
4440
+ pSize="small"
4441
+ [type]="handle.kind === 'number' ? 'number' : 'text'"
4442
+ class="filter-editor-value"
4443
+ data-testid="filter-editor-value"
4444
+ [value]="handle.value() ?? ''"
4445
+ (input)="draft(handle, $event)"
4446
+ />
4447
+ } @else {
4448
+ <!-- No control for this kind yet. A text box would write the wrong VALUE TYPE into
4449
+ the store, which nothing checks at runtime — the operator is checked, the value
4450
+ is not. Visibly missing beats silently wrong. -->
4451
+ <span data-testid="filter-editor-unsupported">{{
4452
+ t('FilterControlNotAvailable')
4453
+ }}</span>
4454
+ }
4455
+
4456
+ <button
4457
+ pButton
4458
+ type="button"
4459
+ class="p-button-sm filter-editor-apply"
4460
+ [label]="t('Apply')"
4461
+ data-testid="filter-editor-apply"
4462
+ (click)="apply(handle)"
4463
+ ></button>
4464
+ </div>
4465
+ }
4466
+ </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"] }] }); }
4468
+ }
4469
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyFilterBarComponent, decorators: [{
4470
+ type: Component,
4471
+ args: [{ selector: 'spiderly-filter-bar', imports: [
4472
+ CommonModule,
4473
+ FormsModule,
4474
+ TranslocoDirective,
4475
+ ButtonModule,
4476
+ CheckboxModule,
4477
+ DatePickerModule,
4478
+ InputTextModule,
4479
+ MultiSelectModule,
4480
+ PopoverModule,
4481
+ SelectModule,
4482
+ ], template: `
4483
+ <ng-container *transloco="let t">
4484
+ <div class="filter-bar" data-testid="filter-bar">
4485
+ @for (chip of filters().applied(); track chip.id) {
4486
+ <span class="filter-chip" data-testid="filter-chip">
4487
+ <!-- The chip body reopens the filter. Once applied it is no longer offered under
4488
+ "+ Filter", so without this the only way to narrow Elektromont to Elektro is to
4489
+ delete the filter and rebuild it. -->
4490
+ <button
4491
+ type="button"
4492
+ class="filter-chip-edit"
4493
+ data-testid="filter-chip-edit"
4494
+ (click)="startEditing(chip.id)"
4495
+ >
4496
+ <!-- &ngsp; is a space the template parser keeps. Angular collapses whitespace
4497
+ between elements by default, which glued these three into
4498
+ "FirmacontainsElektromont" — how a screen reader reads the chip and how it
4499
+ lands when copied, not merely how the spec saw it. The flex gap only spaced
4500
+ them visually. -->
4501
+ <span class="filter-chip-label">{{ chip.label }}</span>&ngsp;
4502
+ <span class="filter-chip-operator">{{
4503
+ t(chip.operatorPhraseKey)
4504
+ }}</span>&ngsp;
4505
+ <span class="filter-chip-value">{{
4506
+ chip.kind === 'boolean'
4507
+ ? chip.value
4508
+ ? t('Yes')
4509
+ : t('No')
4510
+ : chipValue(chip)
4511
+ }}</span>
4512
+ </button>
4513
+ <button
4514
+ type="button"
4515
+ class="filter-chip-remove"
4516
+ data-testid="filter-chip-remove"
4517
+ [attr.aria-label]="t('RemoveFilter')"
4518
+ (click)="filters().reset(chip.id)"
4519
+ >
4520
+ &times;
4521
+ </button>
4522
+ </span>
4523
+ }
4524
+
4525
+ @if (sort().length) {
4526
+ <!-- Read-only, and that is the scope on purpose. Multi-sort is invisible today (sort
4527
+ icons scattered over headers, nothing saying which is primary), so saying it in one
4528
+ place is the whole gain. An x here would have to mean "back to the DEFAULT sort"
4529
+ rather than "no sort", because applyDefaultSortIfUnsorted puts the default straight
4530
+ back — an affordance that lies is worse than none. -->
4531
+ <span class="sort-chip" data-testid="sort-chip">
4532
+ <span class="sort-chip-label">{{ t('SortedBy') }}</span>
4533
+ @for (key of sort(); track key.label; let last = $last) {
4534
+ <span class="sort-chip-key"
4535
+ >{{ key.label }} {{ key.descending ? '↓' : '↑' }}{{
4536
+ last ? '' : ','
4537
+ }}</span
4538
+ >
4539
+ }
4540
+ </span>
4541
+ }
4542
+
4543
+ @if (totalRecords() !== null) {
4544
+ <!-- What came back, next to the chips that say what was asked. An empty grid under a
4545
+ filter is otherwise a mystery. The CURRENT query's count only: the unfiltered total
4546
+ would cost a second request the paginator does not offer. -->
4547
+ <span class="filter-bar-count" data-testid="filter-bar-count">{{
4548
+ t('ResultCount', { count: totalRecords() })
4549
+ }}</span>
4550
+ }
4551
+
4552
+ @if (addable().length) {
4553
+ <!-- A popover, not an inline list. Seven filters already fill the toolbar row inline,
4554
+ and the orders grid declares nineteen — they would wrap into three lines and shove
4555
+ the table down the page. Shopify's Filters component opens its "more filters" the
4556
+ same way. The teleport into document.body costs the specs a container lookup; that
4557
+ cost belongs to the tests, not to the operator. -->
4558
+ <button
4559
+ pButton
4560
+ type="button"
4561
+ class="p-button-text p-button-sm filter-add"
4562
+ icon="pi pi-plus"
4563
+ [label]="t('AddFilter')"
4564
+ data-testid="add-filter"
4565
+ [attr.aria-expanded]="isAddOpen()"
4566
+ (click)="addMenu.toggle($event); isAddOpen.set(!isAddOpen())"
4567
+ ></button>
4568
+ }
4569
+
4570
+ @if (filters().applied().length) {
4571
+ <!-- The bar owns clearing now: the toolbar's button sat a row away from the chips it
4572
+ cleared, which is two affordances for one job. Emits rather than clearing the store
4573
+ itself, because whoever owns the query also owns the persisted state and the sort
4574
+ that a full clear has to reach. -->
4575
+ <button
4576
+ pButton
4577
+ type="button"
4578
+ class="p-button-text p-button-sm filter-bar-clear"
4579
+ icon="pi pi-filter-slash"
4580
+ [label]="t('ClearFilters')"
4581
+ data-testid="filter-bar-clear"
4582
+ (click)="clearAll.emit()"
4583
+ ></button>
4584
+ }
4585
+
4586
+ <p-popover #addMenu (onHide)="isAddOpen.set(false); addSearch.set('')">
4587
+ <div class="filter-add-menu" role="menu">
4588
+ <input
4589
+ pInputText
4590
+ pSize="small"
4591
+ type="search"
4592
+ class="filter-add-search"
4593
+ data-testid="add-filter-search"
4594
+ [value]="addSearch()"
4595
+ [attr.aria-label]="t('AddFilter')"
4596
+ [placeholder]="t('Search')"
4597
+ (input)="addSearch.set($any($event.target).value)"
4598
+ />
4599
+
4600
+ @for (option of offered(); track option.id) {
4601
+ <button
4602
+ type="button"
4603
+ role="menuitem"
4604
+ class="filter-add-option"
4605
+ data-testid="add-filter-option"
4606
+ (click)="startEditing(option.id); addMenu.hide()"
4607
+ >
4608
+ {{ option.label }}
4609
+ </button>
4610
+ }
4611
+ </div>
4612
+ </p-popover>
4613
+
4614
+ </div>
4615
+
4616
+ <!-- Its OWN ROW, directly under the chips. It used to render at the end of the bar, where
4617
+ the clear button's auto left margin pushed everything after it to the far right, so the
4618
+ control for the second filter opened across the screen from the click (Filip, on
4619
+ /tags). A popover anchored to the click was tried first and abandoned: PrimeNG's
4620
+ ng-content slot does not re-project content once the editor's @if has destroyed it, so
4621
+ reopening gave an empty container with overlayVisible true. A fixed row is predictable,
4622
+ which is the property the complaint was actually about.
4623
+ NO BACKTICKS IN THIS TEMPLATE: it is a JS template literal and one terminates it, with
4624
+ the error landing lines away. This is the second time. -->
4625
+ @if (editing(); as handle) {
4626
+ <div class="filter-editor" data-testid="filter-editor">
4627
+ <span class="filter-editor-label">{{ handle.label }}</span>
4628
+
4629
+ @if (handle.operators.length > 1) {
4630
+ <p-select
4631
+ size="small"
4632
+ styleClass="filter-editor-operator"
4633
+ data-testid="filter-editor-operator"
4634
+ [options]="operatorChoices()"
4635
+ optionLabel="label"
4636
+ optionValue="value"
4637
+ [ngModel]="handle.operator() ?? handle.defaultOperator"
4638
+ (onChange)="pickOperator(handle, $event.value)"
4639
+ ></p-select>
4640
+ }
4641
+
4642
+ @if (controlTemplateFor(handle); as projected) {
4643
+ <!-- The consumer's own control for this filter. Read the QueryList live rather than
4644
+ indexing it into a Map: a QueryList already updates itself, and a template
4645
+ declared inside a @for or an @if would go stale in a cached index. Same
4646
+ reasoning as getCellTemplate. -->
4647
+ <ng-container
4648
+ *ngTemplateOutlet="projected; context: { $implicit: handle }"
4649
+ ></ng-container>
4650
+ } @else if (handle.options) {
4651
+ <p-multiSelect
4652
+ size="small"
4653
+ styleClass="filter-editor-value"
4654
+ data-testid="filter-editor-value"
4655
+ [options]="handle.options"
4656
+ optionLabel="label"
4657
+ optionValue="value"
4658
+ [placeholder]="t('All')"
4659
+ [ngModel]="handle.value()"
4660
+ (onChange)="draftValue(handle, $event.value)"
4661
+ ></p-multiSelect>
4662
+ } @else if (handle.kind === 'boolean') {
4663
+ <p-checkbox
4664
+ styleClass="filter-editor-value"
4665
+ data-testid="filter-editor-value"
4666
+ [binary]="true"
4667
+ [ngModel]="handle.value() === true"
4668
+ (onChange)="draftValue(handle, $event.checked)"
4669
+ ></p-checkbox>
4670
+ } @else if (handle.kind === 'date') {
4671
+ <p-datepicker
4672
+ size="small"
4673
+ styleClass="filter-editor-value"
4674
+ data-testid="filter-editor-value"
4675
+ [ngModel]="handle.value()"
4676
+ (onSelect)="draftValue(handle, $event)"
4677
+ ></p-datepicker>
4678
+ } @else if (handle.kind === 'text' || handle.kind === 'number') {
4679
+ <!-- A DOM control's raw value is always a string. It is coerced on the way into
4680
+ the store (see coerce), which is the one place the value type can be got
4681
+ right. Backticks are forbidden in this template: it is a JS template literal
4682
+ and one terminates it, with the error landing lines away. -->
4683
+ <input
4684
+ pInputText
4685
+ pSize="small"
4686
+ [type]="handle.kind === 'number' ? 'number' : 'text'"
4687
+ class="filter-editor-value"
4688
+ data-testid="filter-editor-value"
4689
+ [value]="handle.value() ?? ''"
4690
+ (input)="draft(handle, $event)"
4691
+ />
4692
+ } @else {
4693
+ <!-- No control for this kind yet. A text box would write the wrong VALUE TYPE into
4694
+ the store, which nothing checks at runtime — the operator is checked, the value
4695
+ is not. Visibly missing beats silently wrong. -->
4696
+ <span data-testid="filter-editor-unsupported">{{
4697
+ t('FilterControlNotAvailable')
4698
+ }}</span>
4699
+ }
4700
+
4701
+ <button
4702
+ pButton
4703
+ type="button"
4704
+ class="p-button-sm filter-editor-apply"
4705
+ [label]="t('Apply')"
4706
+ data-testid="filter-editor-apply"
4707
+ (click)="apply(handle)"
4708
+ ></button>
4709
+ </div>
4710
+ }
4711
+ </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"] }]
4713
+ }] });
3973
4714
 
3974
4715
  class SpiderlyDeleteConfirmationComponent {
3975
4716
  constructor(ref, config) {
@@ -4002,13 +4743,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
4002
4743
  }], ctorParameters: () => [{ type: i1$5.DynamicDialogRef }, { type: i1$5.DynamicDialogConfig }] });
4003
4744
 
4004
4745
  /**
4005
- * Default column width per filter type, in rem. Sized for the HEADER — the filter input plus its
4006
- * match-mode dropdown — so a column of short values reserves more than its content needs; that is
4007
- * deliberate, and {@link Column.width} is the per-column override.
4746
+ * Default column width per value type, in rem — generous on purpose (these numbers date from when
4747
+ * the header carried a filter input, and nobody has re-derived them from values yet), and
4748
+ * {@link Column.width} is the per-column override.
4008
4749
  *
4009
4750
  * A table, not a switch, so the mapping is exhaustive: a new `filterType` fails the build here
4010
4751
  * instead of silently inheriting the actions-column reservation below.
4011
4752
  */
4753
+ /** One rem in pixels. Both the share arithmetic and the px-to-share conversion need it. */
4754
+ function rootFontSizePx() {
4755
+ return parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
4756
+ }
4757
+ /** Order matters: `[a, b]` and `[b, a]` ask the server for different orderings. */
4758
+ function sortMetaEquals(a, b) {
4759
+ return (a.length === b.length &&
4760
+ a.every((m, i) => m.field === b[i].field && m.order === b[i].order));
4761
+ }
4012
4762
  const DEFAULT_COLUMN_WIDTH_REM = {
4013
4763
  text: 12,
4014
4764
  date: 10,
@@ -4019,6 +4769,59 @@ const DEFAULT_COLUMN_WIDTH_REM = {
4019
4769
  blob: 5,
4020
4770
  };
4021
4771
  class SpiderlyDataTableComponent {
4772
+ /**
4773
+ * Whether this column is pinned against the left edge. Only the FIRST column, and only when it
4774
+ * is `lockVisible`: a sticky column in the middle of the grid pins the wrong thing and leaves a
4775
+ * hole where it used to be. Once the grid scrolls sideways a row otherwise loses its identity
4776
+ * — "Intesa, plaćeno, 12.400" with no idea whose order it is — and NN/g's Data Tables is
4777
+ * explicit that the leftmost header column must lock in place. `lockVisible` already names
4778
+ * exactly that column in every table, so this needs no new API (CLAUDE.md -> decision 8).
4779
+ */
4780
+ isColumnFrozen(col) {
4781
+ return col.lockVisible === true && this.visibleCols[0] === col;
4782
+ }
4783
+ /**
4784
+ * The rightmost pinned cell, which carries the seam's shadow. It is the identity column when
4785
+ * there is one and the checkbox column otherwise — a fact only this side knows, which is why
4786
+ * the SCSS asking for it as `:last-of-type` drew nothing at all.
4787
+ */
4788
+ isLastFrozenColumn(col) {
4789
+ return this.isColumnFrozen(col);
4790
+ }
4791
+ get isSelectionColumnLastFrozen() {
4792
+ return !this.visibleCols.some((col) => this.isColumnFrozen(col));
4793
+ }
4794
+ /** A frozen column starts after the checkbox column, when there is one. */
4795
+ frozenColumnOffset() {
4796
+ return this.selectionMode === 'multiple' ? `${this.frozenOffsetPx}px` : '0';
4797
+ }
4798
+ /**
4799
+ * Tracks the selection column's rendered width. A ResizeObserver rather than a one-off read:
4800
+ * every column drag and every window resize re-splits the shares, so the offset moves without
4801
+ * anything about the selection column itself changing.
4802
+ */
4803
+ observeSelectionWidth() {
4804
+ const cell = this.selectionHeader()?.nativeElement;
4805
+ if (!cell || this.selectionWidthObserver)
4806
+ return;
4807
+ const measure = () => {
4808
+ const width = cell.offsetWidth;
4809
+ if (width === this.frozenOffsetPx)
4810
+ return;
4811
+ this.zone.run(() => (this.frozenOffsetPx = width));
4812
+ };
4813
+ if (typeof ResizeObserver !== 'undefined') {
4814
+ this.zone.runOutsideAngular(() => {
4815
+ this.selectionWidthObserver = new ResizeObserver(measure);
4816
+ this.selectionWidthObserver.observe(cell);
4817
+ });
4818
+ }
4819
+ // Measured once immediately as well, on a microtask. The observer's first callback arrives an
4820
+ // animation frame later, and until it does the identity column sits at left:0 — on top of the
4821
+ // checkbox column it is supposed to start after. The microtask keeps the write out of the CD
4822
+ // pass that just finished, which doing it inline here would break with an NG0100.
4823
+ queueMicrotask(measure);
4824
+ }
4022
4825
  constructor(router, dialogService, route, messageService, translocoService, configService, errorHandler, locale) {
4023
4826
  this.router = router;
4024
4827
  this.dialogService = dialogService;
@@ -4029,9 +4832,38 @@ class SpiderlyDataTableComponent {
4029
4832
  this.errorHandler = errorHandler;
4030
4833
  this.locale = locale;
4031
4834
  this.destroy$ = new Subject();
4835
+ this.injector = inject(Injector);
4836
+ this.zone = inject(NgZone);
4837
+ /**
4838
+ * The selection column's declared share. Under the fixed layout this is a RATIO, not a width.
4839
+ *
4840
+ * 4rem, down from 6: the original reserved room for a five-digit select-all counter sitting at
4841
+ * body text size beside the box, which is the widest thing it can ever hold and not what it
4842
+ * holds on any ordinary screen. The counter is a smaller step now (see the SCSS), so the same
4843
+ * five digits fit in two thirds of the space.
4844
+ */
4845
+ this.selectionColumnWidth = '4rem';
4846
+ /**
4847
+ * The selection column's MEASURED width, which is what the frozen identity column offsets
4848
+ * itself by. Declaring `6rem` on both looked like the one-telling answer and is wrong: widths
4849
+ * are shares, so that column renders at whatever proportion of the table it gets — 148px for a
4850
+ * declared 6rem in one spec — and the identity column would sit 52px inside it, overlapping the
4851
+ * checkboxes the moment anyone scrolled. The spec caught it; nothing about the page would have.
4852
+ */
4853
+ this.frozenOffsetPx = 0;
4854
+ /** Aborts an in-flight column resize's document listeners, from `onUp` or from teardown. */
4855
+ this.resizeAbort = null;
4032
4856
  this.tableIcon = 'pi pi-list';
4033
4857
  /** Paginator page-size choices; merge rules and persistence: CLAUDE.md → "Rows-per-page". */
4034
4858
  this.rowsPerPageOptions = [10, 25, 50, 100];
4859
+ /**
4860
+ * The view currently selected. Seeded from the first view in `ngOnInit` — then overridden by
4861
+ * `restoreActiveView`, so a reload lands where the operator was — and never left null on a
4862
+ * table that has views: `viewScope` keys the layout off it, so an unseeded table writes its
4863
+ * width and order to the SAME key an unmigrated table uses — orphaned the moment the operator
4864
+ * clicks the first tab — and no tab reads as selected until they do.
4865
+ */
4866
+ this.activeViewId = null;
4035
4867
  /** Whether the paginator is shown. Pass only when `hasLazyLoad === false`. Defaults to `true`. */
4036
4868
  this.showPaginator = true;
4037
4869
  /** Whether the table is wrapped in a card container. Defaults to `false`. */
@@ -4064,9 +4896,6 @@ class SpiderlyDataTableComponent {
4064
4896
  this.rangeAnchorId = null;
4065
4897
  this.pendingShift = null;
4066
4898
  this.onIsAllSelectedChange = new EventEmitter();
4067
- this.matchModeDateOptions = [];
4068
- this.matchModeNumberOptions = [];
4069
- this.matchModeTextOptions = [];
4070
4899
  /** Whether the "Add" button is shown. Defaults to `true`. */
4071
4900
  this.showAddButton = true;
4072
4901
  /** Whether the "Export to Excel" button is shown. Defaults to `true`. */
@@ -4104,33 +4933,54 @@ class SpiderlyDataTableComponent {
4104
4933
  */
4105
4934
  this.columnVisibilityOverrides = {};
4106
4935
  /**
4107
- * Columns revealed by the load-time reconciliation. Kept apart from
4108
- * `columnVisibilityOverrides` so persisting a later unrelated toggle can't promote a
4109
- * transient safety reveal into a durable choice.
4936
+ * Per-column wrap, by `field`. A SEPARATE key from the visibility overrides on purpose: folding
4937
+ * both into one object would invalidate every layout already in an operator's localStorage, and
4938
+ * this is the key the width and order overrides will join (decision 5).
4939
+ */
4940
+ this.columnWrap = {};
4941
+ /** Data-column fields in the operator's order. Empty means "as declared". */
4942
+ this.columnOrder = [];
4943
+ /**
4944
+ * Per-column width overrides, as SHARES in the same unit `getColWidth` emits — never pixels.
4945
+ * Under the fixed layout the browser splits surplus in proportion to these, so a pixel would
4946
+ * pin the column and stop it answering the window: the operator on a 1280 laptop and the one
4947
+ * on a 2560 monitor would get the same frozen column (decision 4).
4948
+ */
4949
+ this.columnWidths = {};
4950
+ /** The column the header menu is currently open for, and its header cell. */
4951
+ this.menuColumn = null;
4952
+ this.menuHeaderCell = null;
4953
+ this.draggedColumn = null;
4954
+ /** False while a gesture that began on the menu chevron or the resize grip is in flight. */
4955
+ this.headerDragArmed = true;
4956
+ /**
4957
+ * A ViewChild rather than a template reference passed in from the header. The popover lives in
4958
+ * the CAPTION template and the button that opens it in the HEADER one, and a reference variable
4959
+ * does not cross an ng-template boundary — it arrived as undefined, and the click did nothing
4960
+ * at all.
4110
4961
  */
4111
- this.revealedByConstraint = new Set();
4962
+ this.columnMenu = viewChild.required('columnMenu');
4963
+ this.filterBar = viewChild(SpiderlyFilterBarComponent);
4964
+ this.selectionHeader = viewChild('selectionHeader');
4112
4965
  /**
4113
- * The filter keys the rows on screen were actually narrowed by. Kept apart from
4114
- * `table.filters` because those are not the same question: PrimeNG's `onModelChange`
4115
- * writes each keystroke of a text/numeric filter straight into the meta and only calls
4116
- * `_filter()` for the auto-applying types, so `table.filters` holds PENDING edits too.
4117
- * Reading it for the header icon marked a column filtered the moment the operator typed
4118
- * a character, before Apply — claiming the grid was narrowed when it was not.
4966
+ * What the grid is ordered by, resolved to COLUMN NAMES for the bar. Read off PrimeNG's live
4967
+ * meta rather than mirrored into a field, so it cannot fall out of step with the sort the table
4968
+ * is actually applying. A field with no column left (hidden, or removed in a later release)
4969
+ * falls back to its own name rather than vanishing from the list.
4119
4970
  */
4120
- this.appliedFilterKeys = new Set();
4121
- this.clientFilterCount = 0;
4122
4971
  /**
4123
- * Memo for the resolution below. Two jobs: the narrowing maps to a NEW array and a
4124
- * template binding must not hand PrimeNG a fresh reference every CD pass, and both
4125
- * halves (offered list, default mode) must come from ONE computation so they cannot
4126
- * disagree. Keyed on the declared array's identity, so reassigning `col.matchModes`
4127
- * recomputes — note PrimeNG itself reads `matchModeOptions` only in its own `ngOnInit`,
4128
- * so a post-init change still never reaches the rendered dropdown.
4972
+ * What the grid is ordered by, resolved to COLUMN NAMES for the bar.
4973
+ *
4974
+ * A field refreshed per fetch, not a getter: it is bound to the bar's `sort`, which is a SIGNAL
4975
+ * input, so a freshly mapped array failed `Object.is` on every change-detection pass and marked
4976
+ * the bar's consumers dirty forever — for a value that changes once per request. Same reason
4977
+ * `tableStyle` is a field.
4129
4978
  */
4130
- this.matchModeResolutions = new WeakMap();
4979
+ this.sortKeys = [];
4131
4980
  }
4132
4981
  ngAfterViewInit() {
4133
4982
  this.setupRemovableSort();
4983
+ this.observeSelectionWidth();
4134
4984
  }
4135
4985
  /**
4136
4986
  * The template that renders this column's cells, or null to use the built-in rendering.
@@ -4176,9 +5026,12 @@ class SpiderlyDataTableComponent {
4176
5026
  this.table.saveState();
4177
5027
  }
4178
5028
  this.table.anchorRowIndex = null;
4179
- return;
4180
5029
  }
4181
- originalSort(event);
5030
+ else {
5031
+ originalSort(event);
5032
+ }
5033
+ // Every explicit header-sort gesture, un-sort included, records the view's override.
5034
+ this.persistActiveViewSort();
4182
5035
  };
4183
5036
  }
4184
5037
  /**
@@ -4204,63 +5057,27 @@ class SpiderlyDataTableComponent {
4204
5057
  }
4205
5058
  }
4206
5059
  }
4207
- ngOnChanges() {
5060
+ ngOnChanges(changes) {
4208
5061
  // Only meaningful once ngOnInit has resolved the defaults; before that it is a no-op re-run.
4209
5062
  if (this.rows != null)
4210
5063
  this.mergeActivePageSizesIntoOptions();
5064
+ if (changes['views'])
5065
+ this.adoptStoredView();
4211
5066
  }
4212
5067
  ngOnInit() {
5068
+ // Fail loud, not half-broken: the store commits through the paginated request, and a
5069
+ // client-side (form-array) table has none — armed anyway, the first commit would call the
5070
+ // absent getPaginatedListObservableMethod inside the requery effect and strand the pending
5071
+ // overlay forever. Client-side store predicates are unbuilt (spiderly#407 territory).
5072
+ if (this.filters && !this.hasLazyLoad) {
5073
+ 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.');
5074
+ }
4213
5075
  if (this.rows == null)
4214
5076
  this.rows = this.configService.defaultPageSize;
5077
+ this.activeViewId ??= this.views?.[0]?.id ?? null;
4215
5078
  if (this.deleteListFromTableObservableMethod && !this.selectionMode) {
4216
5079
  this.selectionMode = 'multiple';
4217
5080
  }
4218
- this.matchModeDateOptions = [
4219
- {
4220
- label: this.translocoService.translate('OnDate'),
4221
- value: MatchModeCodes.Equals,
4222
- },
4223
- {
4224
- label: this.translocoService.translate('DatesBefore'),
4225
- value: MatchModeCodes.LessThan,
4226
- },
4227
- {
4228
- label: this.translocoService.translate('DatesAfter'),
4229
- value: MatchModeCodes.GreaterThan,
4230
- },
4231
- ];
4232
- // Only the three the generated paginator implements for strings
4233
- // (PaginatedResultGenerator.GetCaseForString). Declared rather than left to PrimeNG,
4234
- // whose own text list adds notContains/endsWith/notEquals — modes the backend answers
4235
- // with InvalidMatchMode, i.e. a 400 on every load once the user picks one.
4236
- this.matchModeTextOptions = [
4237
- {
4238
- label: this.translocoService.translate('StartsWith'),
4239
- value: MatchModeCodes.StartsWith,
4240
- },
4241
- {
4242
- label: this.translocoService.translate('Contains'),
4243
- value: MatchModeCodes.Contains,
4244
- },
4245
- {
4246
- label: this.translocoService.translate('Equals'),
4247
- value: MatchModeCodes.Equals,
4248
- },
4249
- ];
4250
- this.matchModeNumberOptions = [
4251
- {
4252
- label: this.translocoService.translate('Equals'),
4253
- value: MatchModeCodes.Equals,
4254
- },
4255
- {
4256
- label: this.translocoService.translate('LessThan'),
4257
- value: MatchModeCodes.LessThan,
4258
- },
4259
- {
4260
- label: this.translocoService.translate('MoreThan'),
4261
- value: MatchModeCodes.GreaterThan,
4262
- },
4263
- ];
4264
5081
  if (this.hasLazyLoad) {
4265
5082
  const baseKey = this.stateKey ?? `spiderly-table:${this.router.url}`;
4266
5083
  this.resolvedStateKey =
@@ -4272,20 +5089,31 @@ class SpiderlyDataTableComponent {
4272
5089
  this.clientLoad();
4273
5090
  }
4274
5091
  this.mergeActivePageSizesIntoOptions();
5092
+ // AFTER `resolvedStateKey` is derived, which is what every restore below keys off — placed
5093
+ // above it they read a null key and silently restore nothing. The view restore runs FIRST,
5094
+ // because the other three read the `viewScope` it decides. And all of it before the effect
5095
+ // is armed, so the first request already carries the filters and the effect's skipped first
5096
+ // run sees the state it will be watching.
5097
+ const restoredView = this.restoreActiveView();
4275
5098
  this.restoreColumnVisibility();
4276
- this.reconcileVisibilityWithPersistedConstraints();
4277
- this.reconcilePersistedMatchModes();
4278
- // Restored filters are applied by definition — mark them before the first paint rather
4279
- // than waiting for the first response, which is the reload case the icon exists for.
4280
- this.snapshotAppliedFilters(this.persistedTableState()?.filters);
5099
+ this.restoreColumnLayout();
5100
+ this.enterViewFilters(restoredView, 'refresh');
5101
+ if (this.filters)
5102
+ this.requeryOnAppliedFilters();
4281
5103
  this.chooserCols = this.cols.filter(SpiderlyDataTableComponent.isDataColumn);
4282
5104
  this.refreshVisibleCols();
4283
5105
  // Bound to p-table's [multiSortMeta] so the FIRST request already carries the right
4284
- // sort. Persisted user sort wins over the declared default; we read it ourselves
4285
- // because PrimeNG's restoreState() runs on the first [value] change — after the
4286
- // initial lazy emit has already left.
5106
+ // sort. The view's own sort wins; the persisted table-global sort sits deliberately
5107
+ // BETWEEN it and the table default, so a view declaring nothing keeps the ordering the
5108
+ // operator last chose. We read it all ourselves because PrimeNG's restoreState() runs on
5109
+ // the first [value] change — after the initial lazy emit has already left — and is then
5110
+ // told to agree (see `alignPersistedTableStateSort`).
5111
+ const tableState = this.persistedTableState();
4287
5112
  this.initialMultiSortMeta =
4288
- this.persistedMultiSortMeta() ?? this.defaultMultiSortMeta();
5113
+ this.viewSort() ??
5114
+ this.sanitizedSortMeta(tableState?.multiSortMeta) ??
5115
+ this.tableDefaultMultiSortMeta();
5116
+ this.alignPersistedTableStateSort(tableState);
4289
5117
  }
4290
5118
  /** A data column shows values for a `field`; anything else (the actions column) always renders. */
4291
5119
  static isDataColumn(col) {
@@ -4297,20 +5125,110 @@ class SpiderlyDataTableComponent {
4297
5125
  * are a transient working set — different natural lifetimes.
4298
5126
  */
4299
5127
  get columnsStateKey() {
4300
- return this.resolvedStateKey ? `${this.resolvedStateKey}:columns` : null;
5128
+ return this.resolvedStateKey
5129
+ ? `${this.resolvedStateKey}${this.viewScope}:columns`
5130
+ : null;
4301
5131
  }
4302
- /** Actions columns always render; data columns follow reveal/override, then declared default. */
5132
+ /** Actions columns always render; data columns follow the override, then declared default. */
4303
5133
  isColumnVisible(col) {
4304
5134
  if (!SpiderlyDataTableComponent.isDataColumn(col))
4305
5135
  return true;
4306
5136
  if (col.lockVisible)
4307
5137
  return true; // pinned — wins over any (possibly stale) override
4308
- if (this.revealedByConstraint.has(col.field))
4309
- return true;
4310
5138
  return this.columnVisibilityOverrides[col.field] ?? col.visible !== false;
4311
5139
  }
5140
+ /**
5141
+ * `cols` in the operator's order. A field the stored order has never seen — a column added in a
5142
+ * later release — sorts to the END rather than displacing a layout someone built by hand
5143
+ * (CLAUDE.md -> "Operator-owned view", decision 7); the sort is stable, so those keep their
5144
+ * declared order among themselves. Actions columns carry no field and are always last, which is
5145
+ * where every consumer already declares them.
5146
+ */
5147
+ orderedCols() {
5148
+ if (this.columnOrder.length === 0)
5149
+ return this.cols;
5150
+ const rank = new Map(this.columnOrder.map((field, index) => [field, index]));
5151
+ return [...this.cols].sort((a, b) => (rank.get(a.field) ?? Number.MAX_SAFE_INTEGER) -
5152
+ (rank.get(b.field) ?? Number.MAX_SAFE_INTEGER));
5153
+ }
5154
+ /**
5155
+ * Whether this column can move one place in `direction` (-1 left, 1 right). A locked column is
5156
+ * the row's anchor — and, once the left edge freezes, the thing a horizontal scroll keeps in
5157
+ * view — so it does not move and nothing moves in front of it.
5158
+ */
5159
+ canMoveColumn(col, direction) {
5160
+ if (col.lockVisible || !col.field)
5161
+ return false;
5162
+ const order = this.dataColumnsInOrder();
5163
+ const from = order.indexOf(col);
5164
+ const to = from + direction;
5165
+ if (from < 0 || to < 0 || to >= order.length)
5166
+ return false;
5167
+ return !order[to].lockVisible;
5168
+ }
5169
+ moveMenuColumn(direction) {
5170
+ const col = this.menuColumn;
5171
+ this.columnMenu().hide();
5172
+ if (!col || !this.canMoveColumn(col, direction))
5173
+ return;
5174
+ const order = this.dataColumnsInOrder();
5175
+ this.placeColumn(order, col, order.indexOf(col) + direction);
5176
+ }
5177
+ /** The move itself, shared by the menu and the drag — two entry points, one rule. */
5178
+ placeColumn(order, col, to) {
5179
+ order.splice(to, 0, ...order.splice(order.indexOf(col), 1));
5180
+ this.columnOrder = order.map((entry) => entry.field);
5181
+ this.persistColumnLayout();
5182
+ this.refreshVisibleCols();
5183
+ }
5184
+ dataColumnsInOrder() {
5185
+ return this.orderedCols().filter(SpiderlyDataTableComponent.isDataColumn);
5186
+ }
5187
+ /**
5188
+ * Header drag, ours rather than PrimeNG's `pReorderableColumn`, for two reasons: its
5189
+ * `onMouseDown` arms the th for anything that is not an INPUT, a TEXTAREA or its OWN resizer —
5190
+ * so our menu chevron and our resize grip would each start a column drag, and reaching for a
5191
+ * width would silently reorder the grid — and its `onColumnDrop` reorders blind, with no notion
5192
+ * of a locked column (CLAUDE.md -> decision 6).
5193
+ *
5194
+ * A shortcut for neighbours, never the mechanism: HTML5 dnd has no edge auto-scroll, so a column
5195
+ * scrolled off the right is unreachable this way, and there is no keyboard path at all. The menu
5196
+ * carries both.
5197
+ *
5198
+ * This half is the arming rule; the drop rule is `canDropOn` below.
5199
+ */
5200
+ onHeaderMouseDown(event) {
5201
+ const target = event.target;
5202
+ this.headerDragArmed = !target.closest('.column-menu-button, .column-resizer');
5203
+ }
5204
+ onHeaderDragStart(col, event) {
5205
+ if (col.lockVisible || !this.headerDragArmed) {
5206
+ event.preventDefault();
5207
+ return;
5208
+ }
5209
+ this.draggedColumn = col;
5210
+ event.dataTransfer?.setData('text/plain', col.field ?? '');
5211
+ }
5212
+ onHeaderDragOver(col, event) {
5213
+ if (this.canDropOn(col))
5214
+ event.preventDefault();
5215
+ }
5216
+ onHeaderDrop(col, event) {
5217
+ event.preventDefault();
5218
+ const dragged = this.draggedColumn;
5219
+ this.draggedColumn = null;
5220
+ if (!dragged || dragged === col || !this.canDropOn(col))
5221
+ return;
5222
+ const order = this.dataColumnsInOrder();
5223
+ this.placeColumn(order, dragged, order.indexOf(col));
5224
+ }
5225
+ /** Nothing lands on, or in front of, the locked column. Enforced here as well as in the menu:
5226
+ * a drop is a separate entry point, and the rule is about the grid rather than about a button. */
5227
+ canDropOn(col) {
5228
+ return !col.lockVisible && SpiderlyDataTableComponent.isDataColumn(col);
5229
+ }
4312
5230
  refreshVisibleCols() {
4313
- this.visibleCols = this.cols.filter((col) => this.isColumnVisible(col));
5231
+ this.visibleCols = this.orderedCols().filter((col) => this.isColumnVisible(col));
4314
5232
  this.visibleDataColsCount = this.visibleCols.filter(SpiderlyDataTableComponent.isDataColumn).length;
4315
5233
  }
4316
5234
  /**
@@ -4325,7 +5243,6 @@ class SpiderlyDataTableComponent {
4325
5243
  return this.visibleDataColsCount > 1;
4326
5244
  }
4327
5245
  toggleColumn(col, visible) {
4328
- this.revealedByConstraint.delete(col.field); // an explicit choice supersedes a safety reveal
4329
5246
  if (visible === (col.visible !== false)) {
4330
5247
  delete this.columnVisibilityOverrides[col.field]; // back at the declared default
4331
5248
  }
@@ -4335,146 +5252,337 @@ class SpiderlyDataTableComponent {
4335
5252
  this.persistColumnVisibility();
4336
5253
  this.refreshVisibleCols();
4337
5254
  if (!visible)
4338
- this.clearHiddenColumnConstraints([col]);
5255
+ this.dropHiddenColumnSort([col]);
4339
5256
  }
4340
- /** Restores every column to its declared default and forgets the stored override. */
4341
- resetColumnVisibility() {
5257
+ /**
5258
+ * Restores every column to its declared default and forgets the stored overrides — visibility,
5259
+ * wrap, order and width alike. It undid visibility only until the header menu shipped three
5260
+ * more gestures beside it, and a layout with no way back is worse than one with no knobs.
5261
+ *
5262
+ * On a STORE table, hiding — here or from the menu — never touches filters or sort: the chip
5263
+ * bar names every applied filter and the sort chip names the sort, hidden column or not
5264
+ * (decision 2b). A storeless table keeps the sort half of the legacy invariant instead — see
5265
+ * `dropHiddenColumnSort`.
5266
+ */
5267
+ resetColumnLayout() {
5268
+ this.columnWrap = {};
5269
+ this.columnOrder = [];
5270
+ this.columnWidths = {};
5271
+ this.persistColumnLayout();
4342
5272
  const wasVisible = this.visibleCols.filter(SpiderlyDataTableComponent.isDataColumn);
4343
- this.revealedByConstraint.clear();
4344
5273
  this.columnVisibilityOverrides = {};
4345
5274
  this.persistColumnVisibility();
4346
5275
  this.refreshVisibleCols();
4347
5276
  // Columns the reset just hid follow the same rule as a manual hide.
4348
- this.clearHiddenColumnConstraints(wasVisible.filter((col) => !this.isColumnVisible(col)));
5277
+ this.dropHiddenColumnSort(wasVisible.filter((col) => !this.isColumnVisible(col)));
4349
5278
  }
4350
5279
  /**
4351
- * A hidden column contributes nothing to filtering or sorting: the header is the only
4352
- * filter surface, so a kept constraint would restrict the data invisibly. Reloads (once)
4353
- * only when a constraint was actually cleared — plain hides don't need a server round-trip.
5280
+ * The sort half of the legacy "hidden contributes nothing" invariant, kept ONLY for storeless
5281
+ * tables (the generated M2M details grids, until spiderly#407 scaffolds their stores): they
5282
+ * have no sort chip, so a hidden column's sort would order the grid with nothing on screen
5283
+ * naming it and no control left to clear it. The filter half needs no heir — `lazyLoad`
5284
+ * overwrites the outgoing filters unconditionally. On a store table this is a no-op:
5285
+ * decision 2b's chips are the visible surface, hidden column or not.
4354
5286
  */
4355
- clearHiddenColumnConstraints(cols) {
4356
- if (!this.table)
5287
+ dropHiddenColumnSort(cols) {
5288
+ if (!this.table || this.filters)
4357
5289
  return;
4358
- let cleared = false;
4359
- for (const col of cols) {
4360
- if (this.columnHasConstraint(this.table, col)) {
4361
- delete this.table.filters[this.filterKey(col)];
4362
- cleared = true;
4363
- }
4364
- if (this.table._multiSortMeta?.some((m) => m.field === col.field)) {
4365
- this.table._multiSortMeta = this.table._multiSortMeta.filter((m) => m.field !== col.field);
4366
- this.table.tableService.onSort(this.table._multiSortMeta);
4367
- cleared = true;
4368
- }
4369
- }
4370
- if (cleared)
4371
- this.table._filter(); // re-emits the lazy load and saves the cleaned state
4372
- }
4373
- /** Whether filter metadata (single or array) carries a real constraint, not just an empty slot. */
4374
- static isActiveFilterMeta(meta) {
4375
- return (Array.isArray(meta) ? meta : [meta]).some((m) => m?.value != null &&
4376
- m.value !== '' &&
4377
- (!Array.isArray(m.value) || m.value.length > 0));
5290
+ const sorted = cols.filter((col) => this.table._multiSortMeta?.some((m) => m.field === col.field));
5291
+ if (!sorted.length)
5292
+ return;
5293
+ this.table._multiSortMeta = this.table._multiSortMeta.filter((m) => !sorted.some((col) => col.field === m.field));
5294
+ this.table.tableService.onSort(this.table._multiSortMeta);
5295
+ this.table._filter(); // re-emits the lazy load and saves the cleaned state
4378
5296
  }
4379
5297
  /**
4380
- * The one home of the filter-state key rule (sort meta is keyed by `field` instead).
4381
- * Instance rather than static so the template's `[field]` binding can share it.
5298
+ * Layout keys carry the ACTIVE VIEW, which is what makes a view more than a saved filter: a
5299
+ * picking view and a payments view want different COLUMNS, not just different rows. Global
5300
+ * layout would re-create the original complaint one level up — the operator still re-picking
5301
+ * columns every time the job changes.
5302
+ *
5303
+ * A table with no views has no segment, so the twenty-six tables that have not migrated keep
5304
+ * reading exactly the keys they already wrote.
4382
5305
  */
4383
- filterKey(col) {
4384
- return col.filterField ?? col.field;
5306
+ get viewScope() {
5307
+ return this.activeViewId ? `:${this.activeViewId}` : '';
4385
5308
  }
4386
5309
  /**
4387
- * Whether the rows on screen are narrowed by this column — feeds the projected
4388
- * `filtericon` template. Why not the template's `let-hasFilter`, and why applied rather
4389
- * than pending state: CLAUDE.md → "Active-filter header icon".
5310
+ * Where the applied filters live. Follows `stateStorage` rather than the layout's always-local
5311
+ * rule, because that is where they lived before the bar took them off the header: PrimeNG's
5312
+ * stateful table persisted them for free, and nothing replaced it — a refresh dropped every
5313
+ * filter (Filip, on /tags). A filter is a question you are in the middle of asking, not a
5314
+ * durable preference like a column's width.
4390
5315
  */
4391
- isColumnFiltered(col) {
4392
- return this.appliedFilterKeys.has(this.filterKey(col));
5316
+ get filtersStateKey() {
5317
+ return this.resolvedStateKey
5318
+ ? `${this.resolvedStateKey}${this.viewScope}:filters`
5319
+ : null;
4393
5320
  }
4394
- /** Whether the column carries a constraint at all, applied or still being typed. */
4395
- columnHasConstraint(table, col) {
4396
- return SpiderlyDataTableComponent.isActiveFilterMeta(table.filters?.[this.filterKey(col)]);
5321
+ get filterStorage() {
5322
+ return this.stateStorage === 'local' ? localStorage : sessionStorage;
4397
5323
  }
4398
5324
  /**
4399
- * Records what a just-applied filter set narrows by. Called from every path that commits
4400
- * one: `(onFilter)` (Apply, auto-apply, a per-column Clear), `lazyLoad` (which also covers
4401
- * `table.clear()` — it re-queries WITHOUT emitting onFilter), the caption's Clear filters,
4402
- * and once from `ngOnInit` off persisted state, so a restored filter is marked on first
4403
- * paint rather than after the first response.
5325
+ * Where the active view id lives. Deliberately NOT view-scoped — it is the input `viewScope`
5326
+ * derives from — and it follows `stateStorage` like the filters rather than the layout's
5327
+ * always-local rule, because the id and the filters it scopes are one question: a durable id
5328
+ * over session-scoped filters would restore a tab with nothing under it.
4404
5329
  */
4405
- snapshotAppliedFilters(filters) {
4406
- const applied = new Set();
4407
- for (const [field, meta] of Object.entries(filters ?? {})) {
4408
- if (SpiderlyDataTableComponent.isActiveFilterMeta(meta))
4409
- applied.add(field);
4410
- }
4411
- this.appliedFilterKeys = applied;
4412
- }
4413
- persistColumnVisibility() {
4414
- if (!this.columnsStateKey)
4415
- return;
4416
- if (Object.keys(this.columnVisibilityOverrides).length === 0) {
4417
- localStorage.removeItem(this.columnsStateKey);
4418
- }
4419
- else {
4420
- writeStoredJson(localStorage, this.columnsStateKey, this.columnVisibilityOverrides);
4421
- }
5330
+ get viewStateKey() {
5331
+ return this.resolvedStateKey ? `${this.resolvedStateKey}:view` : null;
4422
5332
  }
4423
- restoreColumnVisibility() {
4424
- if (!this.columnsStateKey)
5333
+ /** Only `selectView` writes; the `ngOnInit` seed does not — absence means "the default". */
5334
+ persistActiveView() {
5335
+ if (!this.viewStateKey || !this.activeViewId)
4425
5336
  return;
4426
- this.columnVisibilityOverrides =
4427
- readStoredJson(localStorage, this.columnsStateKey) ?? {};
5337
+ writeStoredJson(this.filterStorage, this.viewStateKey, this.activeViewId);
4428
5338
  }
4429
5339
  /**
4430
- * PrimeNG's persisted table state (filters/sort/pagination), read directly from storage
4431
- * because we need it before PrimeNG's restoreState() runs on the first [value] change.
5340
+ * Puts a reload back on the view the operator was using. Every persisted key carries the view
5341
+ * segment, so before this existed the seed decided what got restored: F5 landed on the first
5342
+ * view and read `…:all:filters` — the tab snapped to "Sve" and the bar came back empty while
5343
+ * the operator's filters sat intact under the old view's segment (Filip, on /porudzbine).
5344
+ *
5345
+ * An id naming no current view keeps the seed AND the stored key: "vanished by deploy" and
5346
+ * "not yet arrived" are indistinguishable here (order-list's catalog-licensed "Za pakovanje"
5347
+ * is literally both), so `adoptStoredView` re-reads the key on every `[views]` change and the
5348
+ * key self-heals on the next explicit click.
4432
5349
  */
4433
- persistedTableState() {
4434
- if (!this.resolvedStateKey)
5350
+ restoreActiveView() {
5351
+ if (!this.viewStateKey || !this.views?.length)
4435
5352
  return null;
4436
- return readStoredJson(this.stateStorage === 'local' ? localStorage : sessionStorage, this.resolvedStateKey);
5353
+ const storedId = readStoredJson(this.filterStorage, this.viewStateKey);
5354
+ if (typeof storedId !== 'string')
5355
+ return null;
5356
+ const view = this.viewById(storedId) ?? null;
5357
+ if (view)
5358
+ this.activeViewId = view.id;
5359
+ return view;
4437
5360
  }
4438
5361
  /**
4439
- * Guards the "hidden contributes nothing" invariant against state this component didn't
4440
- * write (older blobs, hand-edited storage): a hidden column the persisted table state
4441
- * still filters or sorts by is revealed rather than constraining the data invisibly.
4442
- * In-memory only — once the constraint is gone, the user's stored choice reapplies.
5362
+ * The second half of `restoreActiveView`: a stored view that did not exist at init is adopted
5363
+ * when a later `[views]` change brings it — through `selectView`, so it gets the same
5364
+ * stored-wins/transient/layout/requery semantics a click would have had. There is no parked
5365
+ * copy of the stored id — the key itself is re-read, and because every explicit pick
5366
+ * overwrites it, the operator's own choice wins by construction. Accepted cost of adopting
5367
+ * late instead of holding the first request: a brief flash of the seeded view's rows and a
5368
+ * second request when the late view lands.
4443
5369
  */
4444
- reconcileVisibilityWithPersistedConstraints() {
4445
- const state = this.persistedTableState();
4446
- if (!state)
5370
+ adoptStoredView() {
5371
+ if (!this.viewStateKey)
4447
5372
  return;
4448
- const constrained = new Set();
4449
- for (const [field, meta] of Object.entries(state.filters ?? {})) {
4450
- if (SpiderlyDataTableComponent.isActiveFilterMeta(meta))
4451
- constrained.add(field);
4452
- }
4453
- for (const sortMeta of state.multiSortMeta ?? []) {
4454
- if (sortMeta?.field)
4455
- constrained.add(sortMeta.field);
4456
- }
4457
- for (const col of this.cols) {
4458
- if (!SpiderlyDataTableComponent.isDataColumn(col))
4459
- continue;
4460
- if (this.isColumnVisible(col))
4461
- continue;
4462
- if (constrained.has(this.filterKey(col)) || constrained.has(col.field)) {
4463
- this.revealedByConstraint.add(col.field);
4464
- }
5373
+ const storedId = readStoredJson(this.filterStorage, this.viewStateKey);
5374
+ if (typeof storedId !== 'string' || storedId === this.activeViewId)
5375
+ return;
5376
+ const view = this.viewById(storedId);
5377
+ if (view)
5378
+ this.selectView(view);
5379
+ }
5380
+ viewById(id) {
5381
+ return this.views?.find((candidate) => candidate.id === id);
5382
+ }
5383
+ persistAppliedFilters() {
5384
+ if (!this.filtersStateKey || !this.filters)
5385
+ return;
5386
+ const snapshot = this.filters.snapshot();
5387
+ if (Object.keys(snapshot).length === 0) {
5388
+ this.filterStorage.removeItem(this.filtersStateKey);
5389
+ }
5390
+ else {
5391
+ writeStoredJson(this.filterStorage, this.filtersStateKey, snapshot);
4465
5392
  }
4466
5393
  }
5394
+ /**
5395
+ * Applies the right filters for entering `view` — the transient rule's one home. A TRANSIENT
5396
+ * view's `apply` is a function of NOW, so it always re-derives; restoring would put
5397
+ * yesterday's date under a tab claiming "today" (`persistAppliedFilters` has no transient
5398
+ * guard, so storage holds the LAST answer). A non-transient view's own stored answer wins
5399
+ * over its `apply`. The semantics argument is the one deliberate difference between the
5400
+ * callers: a 'click' re-asks the view's question when nothing is stored, a 'refresh' returns
5401
+ * the empty state the operator left — they cleared it, and F5 must give the cleared state
5402
+ * back. On a table with no views, `view` is null and this is a plain restore of the
5403
+ * `persistAppliedFilters` snapshot.
5404
+ */
5405
+ enterViewFilters(view, semantics) {
5406
+ if (!this.filters)
5407
+ return;
5408
+ const stored = !view?.transient && this.filtersStateKey
5409
+ ? readStoredJson(this.filterStorage, this.filtersStateKey)
5410
+ : null;
5411
+ if (stored)
5412
+ this.filters.restore(stored);
5413
+ else if (view?.transient || semantics === 'click')
5414
+ view?.apply?.(this.filters);
5415
+ }
5416
+ get layoutStateKey() {
5417
+ return this.resolvedStateKey
5418
+ ? `${this.resolvedStateKey}${this.viewScope}:layout`
5419
+ : null;
5420
+ }
5421
+ /** One key for the whole layout — wrap now, widths next (decision 5). */
5422
+ persistColumnLayout() {
5423
+ if (!this.layoutStateKey)
5424
+ return;
5425
+ const layout = {
5426
+ wrap: this.columnWrap,
5427
+ order: this.columnOrder,
5428
+ widths: this.columnWidths,
5429
+ };
5430
+ const isDefault = Object.keys(this.columnWrap).length === 0 &&
5431
+ this.columnOrder.length === 0 &&
5432
+ Object.keys(this.columnWidths).length === 0;
5433
+ if (isDefault)
5434
+ localStorage.removeItem(this.layoutStateKey);
5435
+ else
5436
+ writeStoredJson(localStorage, this.layoutStateKey, layout);
5437
+ }
5438
+ restoreColumnLayout() {
5439
+ if (!this.layoutStateKey)
5440
+ return;
5441
+ const layout = readStoredJson(localStorage, this.layoutStateKey);
5442
+ this.columnWrap = layout?.wrap ?? {};
5443
+ this.columnOrder = layout?.order ?? [];
5444
+ this.columnWidths = layout?.widths ?? {};
5445
+ }
5446
+ persistColumnVisibility() {
5447
+ if (!this.columnsStateKey)
5448
+ return;
5449
+ if (Object.keys(this.columnVisibilityOverrides).length === 0) {
5450
+ localStorage.removeItem(this.columnsStateKey);
5451
+ }
5452
+ else {
5453
+ writeStoredJson(localStorage, this.columnsStateKey, this.columnVisibilityOverrides);
5454
+ }
5455
+ }
5456
+ restoreColumnVisibility() {
5457
+ if (!this.columnsStateKey)
5458
+ return;
5459
+ this.columnVisibilityOverrides =
5460
+ readStoredJson(localStorage, this.columnsStateKey) ?? {};
5461
+ }
5462
+ /**
5463
+ * PrimeNG's persisted table state (filters/sort/pagination), read directly from storage
5464
+ * because we need it before PrimeNG's restoreState() runs on the first [value] change.
5465
+ */
5466
+ persistedTableState() {
5467
+ if (!this.resolvedStateKey)
5468
+ return null;
5469
+ return readStoredJson(this.stateStorage === 'local' ? localStorage : sessionStorage, this.resolvedStateKey);
5470
+ }
5471
+ activeView() {
5472
+ return this.activeViewId ? this.viewById(this.activeViewId) : undefined;
5473
+ }
5474
+ /**
5475
+ * Where the operator's per-view sort override lives. Null on a table without views — those
5476
+ * keep the pure PrimeNG sort persistence they always had — and null on a STORELESS table:
5477
+ * there "hidden contributes nothing" retires a hidden column's sort (`dropHiddenColumnSort`)
5478
+ * with no persist call at the hide site, so an override restored blind at the next init
5479
+ * would re-impose a sort no chip can name. Store tables — where every viewed consumer
5480
+ * lives — have the chip. Follows `stateStorage` like the filters and the view id: the sort
5481
+ * is part of the same question they scope.
5482
+ */
5483
+ get viewSortStateKey() {
5484
+ if (!this.views?.length || !this.filters)
5485
+ return null;
5486
+ return this.resolvedStateKey
5487
+ ? `${this.resolvedStateKey}${this.viewScope}:sort`
5488
+ : null;
5489
+ }
5490
+ /** The operator's stored sort for the active view, sanitized like any persisted sort. */
5491
+ persistedViewSort() {
5492
+ if (!this.viewSortStateKey)
5493
+ return null;
5494
+ const stored = readStoredJson(this.filterStorage, this.viewSortStateKey);
5495
+ return Array.isArray(stored) ? this.sanitizedSortMeta(stored) : null;
5496
+ }
5497
+ /** The sort this view asks for: the operator's stored override, else its declared sort. */
5498
+ viewSort() {
5499
+ return this.persistedViewSort() ?? this.viewDeclaredMultiSortMeta();
5500
+ }
5501
+ /**
5502
+ * Records the current sort as the active view's override — called from the EXPLICIT sort
5503
+ * gestures only (header click, menu sort, Clear filters), never from a load: a sort merely
5504
+ * inherited at init must not crystallize into an override, or a later change to the view's
5505
+ * declared sort would stop flowing to operators who never touched sort — the same
5506
+ * flows-through rule `columnVisibilityOverrides` documents. A sort equal to the declared
5507
+ * default is stored as ABSENCE for the same reason.
5508
+ */
5509
+ persistActiveViewSort() {
5510
+ const key = this.viewSortStateKey;
5511
+ if (!key)
5512
+ return;
5513
+ const current = this.table._multiSortMeta ?? [];
5514
+ if (sortMetaEquals(current, this.defaultMultiSortMeta() ?? [])) {
5515
+ this.filterStorage.removeItem(key);
5516
+ }
5517
+ else {
5518
+ writeStoredJson(this.filterStorage, key, current);
5519
+ }
5520
+ }
5521
+ /**
5522
+ * Puts the entering view's sort on the grid — stored override first, then the declared sort;
5523
+ * a view stating no preference keeps the ordering already there (the pre-decision-10-sort
5524
+ * behavior, and the least surprising for the tab an operator never sorted). Broadcast only —
5525
+ * the caller owns the refetch, because which request carries the new sort differs between the
5526
+ * store and storeless paths.
5527
+ */
5528
+ applyViewSort() {
5529
+ const target = this.viewSort();
5530
+ if (!target)
5531
+ return false;
5532
+ if (sortMetaEquals(target, this.table._multiSortMeta ?? []))
5533
+ return false;
5534
+ this.broadcastSort(target);
5535
+ return true;
5536
+ }
5537
+ /**
5538
+ * Rewrites the sort inside PrimeNG's persisted state to the one `ngOnInit` resolved. The two
5539
+ * can disagree only on a viewed table — PrimeNG's state is table-global while the resolved
5540
+ * sort is the view's — and PrimeNG's restoreState() applies its copy on the first [value]
5541
+ * change, AFTER the initial request already left with ours: without this line the grid's
5542
+ * arrows and every later request silently fall back to the other view's sort. Handed the
5543
+ * already-parsed state: `ngOnInit` reads the blob once for the whole init pass.
5544
+ */
5545
+ alignPersistedTableStateSort(state) {
5546
+ if (!this.views?.length || !this.resolvedStateKey || !state)
5547
+ return;
5548
+ const resolved = this.initialMultiSortMeta ?? [];
5549
+ if (sortMetaEquals(state.multiSortMeta ?? [], resolved))
5550
+ return;
5551
+ state.multiSortMeta = resolved.length ? resolved : undefined;
5552
+ writeStoredJson(this.filterStorage, this.resolvedStateKey, state);
5553
+ }
4467
5554
  //#endregion
4468
5555
  /**
4469
- * The declared default as PrimeNG sort meta, or null when none is declared — or when its
4470
- * column is hidden ("hidden contributes nothing"): the table then falls back to the
4471
- * backend's implicit Id DESC, exactly as if no default were declared.
5556
+ * The DECLARED default in the current context: the active view's own sort when it declares
5557
+ * one, else the table-level `defaultSortField`. Every "fall back to the declared default"
5558
+ * site — tri-state un-sort, Clear filters, the unsorted-load safety net — reads this, so
5559
+ * un-sorting inside a view lands on the view's ordering, not the table's.
4472
5560
  */
4473
5561
  defaultMultiSortMeta() {
5562
+ return this.viewDeclaredMultiSortMeta() ?? this.tableDefaultMultiSortMeta();
5563
+ }
5564
+ /**
5565
+ * The active view's declared sort, sanitized: a non-sortable field would answer every load
5566
+ * with a 400 (`keepSortableMeta`'s rationale), so it is dropped rather than sent; a view
5567
+ * whose declaration sanitizes to nothing falls through to the table default.
5568
+ */
5569
+ viewDeclaredMultiSortMeta() {
5570
+ return this.sanitizedSortMeta(this.activeView()?.sort);
5571
+ }
5572
+ /**
5573
+ * The table-level declared default as PrimeNG sort meta, or null when none is declared. On a
5574
+ * store table it applies even while its column is hidden — the sort chip names the ordering
5575
+ * whether or not the column is on screen (decision 2b). A STORELESS table has no chip, so a
5576
+ * hidden default would order the grid with nothing on screen naming it; there the default
5577
+ * dies with the column, exactly as the legacy invariant had it (see `dropHiddenColumnSort`).
5578
+ */
5579
+ tableDefaultMultiSortMeta() {
4474
5580
  if (!this.defaultSortField)
4475
5581
  return null;
4476
5582
  const defaultSortCol = this.cols?.find((col) => col.field === this.defaultSortField);
4477
- if (defaultSortCol && !this.isColumnVisible(defaultSortCol))
5583
+ if (defaultSortCol &&
5584
+ !this.filters &&
5585
+ !this.isColumnVisible(defaultSortCol))
4478
5586
  return null;
4479
5587
  // A declared default on a non-sortable column is a consumer mistake the backend answers
4480
5588
  // with a 400 on every load; fall back to its implicit Id DESC instead (see keepSortableMeta).
@@ -4482,9 +5590,13 @@ class SpiderlyDataTableComponent {
4482
5590
  return null;
4483
5591
  return [{ field: this.defaultSortField, order: this.defaultSortOrder }];
4484
5592
  }
4485
- persistedMultiSortMeta() {
4486
- const state = this.persistedTableState();
4487
- const sortable = this.keepSortableMeta(state?.multiSortMeta);
5593
+ /**
5594
+ * `keepSortableMeta` with "sanitized to nothing" surfaced as null — the rule every sort
5595
+ * SOURCE shares ("a sort that sanitizes to nothing is no sort"), so the resolution chains
5596
+ * can `??` past an empty answer instead of stopping on an empty array.
5597
+ */
5598
+ sanitizedSortMeta(multiSortMeta) {
5599
+ const sortable = this.keepSortableMeta(multiSortMeta);
4488
5600
  return sortable.length ? sortable : null;
4489
5601
  }
4490
5602
  /**
@@ -4502,47 +5614,6 @@ class SpiderlyDataTableComponent {
4502
5614
  return col == null || this.isColumnSortable(col);
4503
5615
  });
4504
5616
  }
4505
- /**
4506
- * The filter twin of `keepSortableMeta` — persisted state outlives the rule that produced
4507
- * it, here a match mode stored before the column declared `matchModes`. It has to REWRITE
4508
- * storage rather than filter a value on the way past: `ColumnFilter.ngOnInit` skips
4509
- * `initFieldFilterConstraint()` whenever the field already carries a constraint, so our
4510
- * `[matchMode]` never applies to a restored one. Left alone, the column keeps filtering by
4511
- * a mode it no longer offers while its match-mode `<p-select>` renders blank (the stored
4512
- * value is not among the options). Runs before PrimeNG's `restoreState()`, which reads this
4513
- * same key on the first `[value]` change.
4514
- *
4515
- * Deliberately narrow: it touches only constraints whose mode the declaring column no
4516
- * longer offers, so every other byte of the persisted blob survives.
4517
- */
4518
- reconcilePersistedMatchModes() {
4519
- if (!this.resolvedStateKey)
4520
- return;
4521
- const state = this.persistedTableState();
4522
- if (!state?.filters)
4523
- return;
4524
- let changed = false;
4525
- for (const col of this.cols ?? []) {
4526
- if (!col.matchModes?.length)
4527
- continue;
4528
- const offered = this.getColMatchModeOptions(col);
4529
- const meta = state.filters[this.filterKey(col)];
4530
- if (!offered?.length || !Array.isArray(meta))
4531
- continue;
4532
- for (const constraint of meta) {
4533
- if (constraint?.matchMode == null)
4534
- continue;
4535
- if (offered.some((option) => option.value === constraint.matchMode))
4536
- continue;
4537
- constraint.matchMode = this.getColMatchMode(col);
4538
- changed = true;
4539
- }
4540
- }
4541
- if (!changed)
4542
- return;
4543
- const storage = this.stateStorage === 'local' ? localStorage : sessionStorage;
4544
- storage.setItem(this.resolvedStateKey, JSON.stringify(state));
4545
- }
4546
5617
  // Safety net: any lazy load still leaving unsorted (Clear filters — PrimeNG's clear()
4547
5618
  // nulls the sort and emits in one call — or stale persisted state) falls back to the
4548
5619
  // declared default. Patching the event before it becomes the backend Filter avoids a
@@ -4556,10 +5627,13 @@ class SpiderlyDataTableComponent {
4556
5627
  if (defaultSort == null)
4557
5628
  return;
4558
5629
  event.multiSortMeta = defaultSort;
4559
- if (this.table) {
4560
- this.table._multiSortMeta = defaultSort;
4561
- this.table.tableService.onSort(defaultSort);
4562
- }
5630
+ if (this.table)
5631
+ this.broadcastSort(defaultSort);
5632
+ }
5633
+ /** Puts `meta` on the grid and tells the header icons — no fetch; the caller owns that. */
5634
+ broadcastSort(meta) {
5635
+ this.table._multiSortMeta = meta;
5636
+ this.table.tableService.onSort(meta);
4563
5637
  }
4564
5638
  onPageChange(container) {
4565
5639
  scrollElementIntoViewIfAboveViewport(container);
@@ -4585,10 +5659,16 @@ class SpiderlyDataTableComponent {
4585
5659
  this.loading = true;
4586
5660
  this.applyDefaultSortIfUnsorted(event);
4587
5661
  this.lastLazyLoadEvent = event;
5662
+ this.refreshSortKeys();
4588
5663
  this.rangeAnchorId = null;
4589
- this.snapshotAppliedFilters(event.filters);
4590
5664
  let tableFilter = event;
4591
5665
  tableFilter.additionalFilterIdLong = this.additionalFilterIdLong;
5666
+ // The store is the ONLY source of what narrows the grid, on every table. Unconditional on
5667
+ // purpose: with the header filters gone, the one remaining writer of PrimeNG's own
5668
+ // `event.filters` is a stale persisted blob from the header-filter era replayed by
5669
+ // `restoreState` — and letting that through would narrow a storeless table invisibly, with
5670
+ // no chip and no button left to clear it.
5671
+ tableFilter.filters = this.filters?.toFilterPayload() ?? {};
4592
5672
  this.onLazyLoad.next(tableFilter);
4593
5673
  // Issued NOW rather than after the list lands: it needs only `tableFilter`, so serialising
4594
5674
  // it behind the list response cost a second round trip on every page of every
@@ -4681,21 +5761,6 @@ class SpiderlyDataTableComponent {
4681
5761
  this.rowsSelectedNumber = this.selectedItemIds.length;
4682
5762
  this.setFakeIsAllSelected();
4683
5763
  }
4684
- filter(event) {
4685
- // Fires from _filter(), i.e. only once a filter is COMMITTED — never on a keystroke.
4686
- this.snapshotAppliedFilters(event.filters ?? this.table?.filters);
4687
- if (this.hasLazyLoad && this.selectionMode === 'multiple')
4688
- this.selectAll(false); // We need to do it like this because: totalRecords: 1 -> selectedRecords from earlyer selection 2 -> unselect current -> all checkbox is set to true
4689
- if (this.hasLazyLoad === false && this.selectionMode === 'multiple') {
4690
- if (this.clientFilterCount === 0) {
4691
- this.loadFormArrayItems();
4692
- this.clientFilterCount++;
4693
- }
4694
- else {
4695
- this.clientFilterCount--;
4696
- }
4697
- }
4698
- }
4699
5764
  loadFormArrayItems() {
4700
5765
  this.items = this.getFormArrayItems(this.additionalIndexes);
4701
5766
  this.rangeAnchorId = null;
@@ -4715,135 +5780,342 @@ class SpiderlyDataTableComponent {
4715
5780
  !!col.field &&
4716
5781
  !col.field.endsWith('CommaSeparated'));
4717
5782
  }
5783
+ openColumnMenu(col, event) {
5784
+ this.menuColumn = col;
5785
+ // Kept so `fitMenuColumn` can find the column's cells: it needs the header's position among
5786
+ // its siblings, which is the only thing that ties a Column to a table column in the DOM.
5787
+ this.menuHeaderCell = event.target.closest('th');
5788
+ this.columnMenu().toggle(event);
5789
+ }
4718
5790
  /**
4719
- * The column's declared {@link Column.width}, or the default for its filter type.
5791
+ * Switches to a view. Clears first, always: a view is a STATE rather than an addition, and two
5792
+ * of them composed produce a question nobody asked. The clear runs through the store directly
5793
+ * rather than through `clear(table)`, which would also wipe the sort and the persisted state a
5794
+ * view is entitled to keep.
5795
+ */
5796
+ selectView(view) {
5797
+ this.activeViewId = view.id;
5798
+ // Always writes — including the first view, so a deliberate "Sve" during a deploy race
5799
+ // sticks, and so an explicit pick beats a stored id whose view has not arrived yet
5800
+ // (`adoptStoredView` compares against this key).
5801
+ this.persistActiveView();
5802
+ // The keys just changed under us, so the layout has to be re-read for the view being entered
5803
+ // — otherwise the one being left keeps rendering until something else forces a reload.
5804
+ this.restoreColumnVisibility();
5805
+ this.restoreColumnLayout();
5806
+ this.refreshVisibleCols();
5807
+ // Sort is view state too (decision 10's sort half). Swapped BEFORE the filter transition so
5808
+ // the one request the requery effect sends carries both.
5809
+ const sortChanged = this.applyViewSort();
5810
+ if (!this.filters) {
5811
+ if (sortChanged)
5812
+ this.table.sortMultiple();
5813
+ return;
5814
+ }
5815
+ const appliedBefore = this.filters.applied();
5816
+ // The filter key moved with the view too; `enterViewFilters` holds the stored-wins and
5817
+ // transient rules.
5818
+ this.filters.clear();
5819
+ this.enterViewFilters(view, 'click');
5820
+ // The requery effect runs only when the applied set actually changed — `clear()` on an
5821
+ // already-empty store writes nothing (two views sharing one filter answer are exactly that)
5822
+ // — so a sort-only switch fetches here. `applied` is a computed: reference equality across
5823
+ // the transition means none of its sources moved.
5824
+ if (sortChanged && this.filters.applied() === appliedBefore) {
5825
+ this.refetchFromFirstPage();
5826
+ }
5827
+ }
5828
+ /**
5829
+ * The filter this column stands for. Defaults to the column's own `field`, because a store id
5830
+ * IS a backend property name — `toFilterPayload` emits it straight into `Filter.filters`. So
5831
+ * `filterId` is an escape hatch for the rare mismatch, not something every column restates:
5832
+ * all eight of the first migrated table's declarations were the field name written a second
5833
+ * time.
5834
+ */
5835
+ filterIdFor(col) {
5836
+ return col.filterId ?? col.field;
5837
+ }
5838
+ /**
5839
+ * Asks the STORE, not the column. Checking only that an id was declared let a typo through to
5840
+ * `store.get(id)`, which reads `definitions[id].label` — so a mistyped id threw on click rather
5841
+ * than greying the item out. With 195 declarations to migrate, that is the failure mode worth
5842
+ * closing here rather than in each consumer.
5843
+ */
5844
+ canFilterColumn(col) {
5845
+ const id = this.filterIdFor(col);
5846
+ return id != null && this.filters?.definitions[id] != null;
5847
+ }
5848
+ /**
5849
+ * Opens the bar's editor for this column's filter. The bar took the filter off the header and
5850
+ * the header is still where an operator reaches for it, which is twenty-seven tables' worth of
5851
+ * habit; this is the shortcut back rather than a second filter surface — it drives the same
5852
+ * editor, on the same bar, that `+ Filter` opens.
5853
+ */
5854
+ filterMenuColumn() {
5855
+ const col = this.menuColumn;
5856
+ this.columnMenu().hide();
5857
+ if (!col || !this.canFilterColumn(col))
5858
+ return;
5859
+ this.filterBar()?.startEditing(this.filterIdFor(col));
5860
+ }
5861
+ /**
5862
+ * Sorts by this column in the NAMED direction. Clicking a header cycles asc, desc and off,
5863
+ * which is fine for one column and a guess for a direction someone wants now — you click, look,
5864
+ * and click again if it went the other way. This is also the only sort path that works from a
5865
+ * keyboard.
4720
5866
  *
4721
- * A width, not a minimum: under the fixed layout {@link tableStyle} establishes, this is what
4722
- * the column is sized from, and the table shares its surplus in PROPORTION to these numbers.
4723
- * Columns declaring none would otherwise take an equal share, which throws away what the
4724
- * per-type defaults say — a boolean holds "Da"/"Ne", a text column holds a name.
5867
+ * A column the generated paginator has no sort case for answers every load with a 400, so the
5868
+ * items are disabled by the same predicate that keeps its header from being clickable.
4725
5869
  */
4726
- getColWidth(col) {
5870
+ sortMenuColumn(order) {
5871
+ const col = this.menuColumn;
5872
+ this.columnMenu().hide();
5873
+ if (!col?.field || !this.isColumnSortable(col))
5874
+ return;
5875
+ this.table._multiSortMeta = [{ field: col.field, order }];
5876
+ this.table.sortMultiple();
5877
+ this.persistActiveViewSort();
5878
+ }
5879
+ /**
5880
+ * Sizes the column to the widest thing in it. The cheaper escape from a cramped column: no
5881
+ * drag, no aim. Unlike a drag it does NOT trade with the neighbour — a minimum that grew is
5882
+ * exactly the case decision 4 lets push the table past its container and start scrolling.
5883
+ *
5884
+ * Measures the CELL CONTENT, not the cell: the clamp lives on the inner span, so a `td` reports
5885
+ * a scrollWidth that fits while the text inside it does not.
5886
+ */
5887
+ fitMenuColumn() {
5888
+ const col = this.menuColumn;
5889
+ const th = this.menuHeaderCell;
5890
+ this.columnMenu().hide();
5891
+ if (!col?.field || !th?.parentElement)
5892
+ return;
5893
+ // Walked up from the header rather than reached through PrimeNG's own ElementRef: the th is
5894
+ // the only thing that ties a Column to a table column in the DOM, and it already knows which
5895
+ // table it is in.
5896
+ const index = Array.from(th.parentElement.children).indexOf(th) + 1;
5897
+ const cells = th
5898
+ .closest('table')
5899
+ ?.querySelectorAll(`tbody tr > td:nth-child(${index})`);
5900
+ if (!cells?.length)
5901
+ return;
5902
+ // Padding comes from one CSS rule for every cell in the column, so it is read once off the
5903
+ // first — a getComputedStyle per cell was a hundred style flushes for one number.
5904
+ const styles = getComputedStyle(cells[0]);
5905
+ const padding = (parseFloat(styles.paddingLeft) || 0) +
5906
+ (parseFloat(styles.paddingRight) || 0);
5907
+ // Cells AND their descendants in one query rather than one per cell: the clamp lives on an
5908
+ // inner span, so a td reports a scrollWidth that fits while the text inside it does not, and
5909
+ // a consumer's cell template can nest the overflowing element arbitrarily deep.
5910
+ const content = th
5911
+ .closest('table')
5912
+ .querySelectorAll(`tbody tr > td:nth-child(${index}), tbody tr > td:nth-child(${index}) *`);
5913
+ let widest = th.scrollWidth;
5914
+ for (const node of Array.from(content)) {
5915
+ widest = Math.max(widest, node.scrollWidth + padding);
5916
+ }
5917
+ this.columnWidths[col.field] = this.shareThatFits(col, th, widest);
5918
+ this.persistColumnLayout();
5919
+ }
5920
+ /**
5921
+ * The share that actually yields `needed` pixels — not `needed` scaled by the column's current
5922
+ * px-per-share, which is the obvious answer and the wrong one. Shares are a PROPORTION of
5923
+ * whatever width the table has, so growing this column's share also shrinks the realized width
5924
+ * of every other column, and the naive figure lands short. The spec caught it as a cell that
5925
+ * was still clipped after being told to fit.
5926
+ *
5927
+ * While the columns fit, the table keeps the container's width and the proportion has to solve
5928
+ * needed / tableWidth = newShare / (otherShares + newShare)
5929
+ * with `otherShares` recovered from what this column currently renders at. Once `needed` is the
5930
+ * whole container the proportional regime has no answer: the table is about to overrun its
5931
+ * container and scroll (decision 4), and there a share IS its own length, so the pixels convert
5932
+ * straight through the root font size.
5933
+ */
5934
+ shareThatFits(col, th, needed) {
5935
+ const share = this.columnShare(col);
5936
+ const thPx = Math.max(th.offsetWidth, 1);
5937
+ const tableWidth = th.closest('table')?.offsetWidth ?? 0;
5938
+ if (needed < tableWidth) {
5939
+ const otherShares = (share * (tableWidth - thPx)) / thPx;
5940
+ return (needed * otherShares) / (tableWidth - needed);
5941
+ }
5942
+ return needed / rootFontSizePx();
5943
+ }
5944
+ hideMenuColumn() {
5945
+ if (this.menuColumn)
5946
+ this.toggleColumn(this.menuColumn, false);
5947
+ this.columnMenu().hide();
5948
+ }
5949
+ /**
5950
+ * Whether this column's cells give up the one-line clamp. The DEFAULT stays clamped
5951
+ * (CLAUDE.md -> "Operator-owned view", decision 9); what changes is that the operator picks
5952
+ * which column pays the row height, not whoever wrote `cols`.
5953
+ *
5954
+ * Lands on the td as `.cell-wrap`, whose inherited `--spiderly-cell-*` properties are how the
5955
+ * toggle crosses into a projected `spiderlyCellTemplate` too — the contract's one telling is
5956
+ * on that SCSS rule; a consumer clamp that hard-codes its values opts out.
5957
+ */
5958
+ isColumnWrapped(col) {
5959
+ return col.field != null && this.columnWrap[col.field] === true;
5960
+ }
5961
+ get isMenuColumnWrapped() {
5962
+ return this.menuColumn != null && this.isColumnWrapped(this.menuColumn);
5963
+ }
5964
+ toggleWrapForMenuColumn() {
5965
+ const col = this.menuColumn;
5966
+ this.columnMenu().hide();
5967
+ if (!col?.field)
5968
+ return;
5969
+ if (this.columnWrap[col.field])
5970
+ delete this.columnWrap[col.field];
5971
+ else
5972
+ this.columnWrap[col.field] = true;
5973
+ this.persistColumnLayout();
5974
+ }
5975
+ /** Exposed for the template: actions columns carry no field and get no menu. */
5976
+ isDataColumn(col) {
5977
+ return SpiderlyDataTableComponent.isDataColumn(col);
5978
+ }
5979
+ /**
5980
+ * Read off the last REQUEST's sort, not `table._multiSortMeta`. On a first load with a declared
5981
+ * default that field is still null: `applyDefaultSortIfUnsorted` writes the default onto the
5982
+ * lazy-load event, and PrimeNG fills its own meta only once someone clicks a header. Reading the
5983
+ * table there showed no sort on exactly the tables that always have one. A field with no column
5984
+ * left falls back to its own name rather than vanishing.
5985
+ */
5986
+ refreshSortKeys() {
5987
+ this.sortKeys = (this.lastLazyLoadEvent?.multiSortMeta ?? []).map((key) => ({
5988
+ label: this.cols.find((col) => col.field === key.field)?.name ?? key.field,
5989
+ descending: key.order === -1,
5990
+ }));
5991
+ }
5992
+ /** This column's share, override first, then its declared width, then the per-type default. */
5993
+ columnShare(col) {
5994
+ if (col.field && this.columnWidths[col.field] != null) {
5995
+ return this.columnWidths[col.field];
5996
+ }
4727
5997
  if (col.width != null)
4728
- return col.width;
5998
+ return this.shareOfDeclaredWidth(col);
5999
+ return this.defaultShare(col);
6000
+ }
6001
+ /**
6002
+ * A declared width expressed in SHARE units. rem IS the share unit and px converts; any other
6003
+ * unit falls back to the per-type default rather than being read as a bare number.
6004
+ *
6005
+ * `parseFloat` alone shipped here first, so `width: '150px'` resolved to share 150 against
6006
+ * neighbours at 12 — one column eating the grid on the first drag or fit. Every declared width
6007
+ * in the consumer happens to be rem today, which is exactly why nothing would have caught it.
6008
+ */
6009
+ shareOfDeclaredWidth(col) {
6010
+ const declared = col.width.trim();
6011
+ const value = parseFloat(declared);
6012
+ if (Number.isNaN(value))
6013
+ return this.defaultShare(col);
6014
+ if (declared.endsWith('rem'))
6015
+ return value;
6016
+ if (declared.endsWith('px'))
6017
+ return value / rootFontSizePx();
6018
+ return this.defaultShare(col);
6019
+ }
6020
+ defaultShare(col) {
4729
6021
  if (col.filterType)
4730
- return `${DEFAULT_COLUMN_WIDTH_REM[col.filterType]}rem`;
6022
+ return DEFAULT_COLUMN_WIDTH_REM[col.filterType];
4731
6023
  // What is left declares no filterType: an actions column. It cannot shrink to fit any more,
4732
6024
  // so the icons need a reservation — each sits in a flex row whose gap is set beside them in
4733
6025
  // the template, inside the cell's own padding. 2.5, not 2.2, so the arithmetic stays exact in
4734
6026
  // binary and the string never reads `8.6000000001rem`.
4735
- return `${2 + (col.actions?.length ?? 0) * 2.5}rem`;
4736
- }
4737
- getColMatchModeOptions(col) {
4738
- return this.resolveMatchModes(col).options;
4739
- }
4740
- getColMatchMode(col) {
4741
- return this.resolveMatchModes(col).defaultMode;
4742
- }
4743
- resolveMatchModes(col) {
4744
- const cached = this.matchModeResolutions.get(col);
4745
- if (cached && cached.source === col.matchModes)
4746
- return cached.resolution;
4747
- const resolution = this.computeMatchModes(col);
4748
- this.matchModeResolutions.set(col, {
4749
- source: col.matchModes,
4750
- resolution,
4751
- });
4752
- return resolution;
6027
+ return 2 + (col.actions?.length ?? 0) * 2.5;
4753
6028
  }
4754
6029
  /**
4755
- * Applies `Column.matchModes`, and refuses to half-apply it. Both ways a declaration can
4756
- * be wrong are consumer mistakes that PrimeNG would otherwise turn into a broken filter
4757
- * rather than an error: it reads `matchModeOptions || <type defaults>`, so handing it
4758
- * `[]` renders an EMPTY dropdown (an empty array is truthy) while the default mode still
4759
- * seeds the constraint, and handing it `null` silently restores PrimeNG's own list. So an
4760
- * unusable narrowing falls back to the full list and says so, instead of shipping a
4761
- * dropdown the user cannot pick from.
6030
+ * Trades share between the column being dragged and the one on its right, so the table keeps
6031
+ * its total: widening a column never starts a horizontal scroll by itself, which only happens
6032
+ * once the SUM of the minimums stops fitting. The px delta is converted through the column's
6033
+ * own rendered width, which is the only place the share-to-pixel scale is actually known.
4762
6034
  */
4763
- computeMatchModes(col) {
4764
- const options = this.matchModeOptionsForType(col.filterType);
4765
- const typeDefault = this.defaultMatchModeForType(col.filterType);
4766
- const declared = col.matchModes;
4767
- if (!declared?.length)
4768
- return { options, defaultMode: typeDefault };
4769
- if (!options?.length) {
4770
- console.error(`spiderly-data-table: column "${col.field}" declares matchModes, but filterType "${col.filterType}" has no match modes to narrow — ignoring.`);
4771
- return { options, defaultMode: typeDefault };
4772
- }
4773
- const narrowed = declared
4774
- .map((code) => options.find((option) => option.value === code))
4775
- .filter((option) => option != null);
4776
- if (narrowed.length !== declared.length) {
4777
- const unsupported = declared.filter((code) => !options.some((option) => option.value === code));
4778
- console.error(`spiderly-data-table: column "${col.field}" declares match mode(s) [${unsupported.join(', ')}] that filterType "${col.filterType}" does not support — ignoring them.`);
4779
- }
4780
- if (!narrowed.length)
4781
- return { options, defaultMode: typeDefault };
4782
- return { options: narrowed, defaultMode: narrowed[0].value };
4783
- }
4784
- matchModeOptionsForType(filterType) {
4785
- switch (filterType) {
4786
- case 'text':
4787
- return this.matchModeTextOptions;
4788
- case 'date':
4789
- return this.matchModeDateOptions;
4790
- case 'multiselect':
4791
- return null;
4792
- case 'boolean':
4793
- return null;
4794
- case 'numeric':
4795
- return this.matchModeNumberOptions;
4796
- default:
4797
- return null;
4798
- }
4799
- }
4800
- defaultMatchModeForType(filterType) {
4801
- switch (filterType) {
4802
- case 'text':
4803
- return MatchModeCodes.Contains;
4804
- case 'date':
4805
- return MatchModeCodes.Equals;
4806
- case 'multiselect':
4807
- return MatchModeCodes.In;
4808
- case 'boolean':
4809
- return MatchModeCodes.Equals;
4810
- case 'numeric':
4811
- return MatchModeCodes.Equals;
4812
- default:
4813
- return null;
4814
- }
4815
- }
4816
- isDropOrMulti(filterType) {
4817
- if (filterType == 'dropdown' || filterType == 'multiselect') {
4818
- return true;
4819
- }
4820
- else {
4821
- return false;
4822
- }
6035
+ startColumnResize(col, event, th) {
6036
+ event.preventDefault();
6037
+ event.stopPropagation();
6038
+ const neighbour = this.visibleCols[this.visibleCols.indexOf(col) + 1];
6039
+ if (!col.field || !neighbour?.field)
6040
+ return;
6041
+ const controller = new AbortController();
6042
+ this.resizeAbort?.abort();
6043
+ this.resizeAbort = controller;
6044
+ const startX = event.clientX;
6045
+ const startShare = this.columnShare(col);
6046
+ const neighbourShare = this.columnShare(neighbour);
6047
+ const sharePerPx = startShare / Math.max(th.offsetWidth, 1);
6048
+ // A column that reaches zero share disappears with no way back from the header it lost.
6049
+ const floor = 2;
6050
+ const neighbourCell = th.nextElementSibling;
6051
+ let applied = 0;
6052
+ const onMove = (move) => {
6053
+ const delta = (move.clientX - startX) * sharePerPx;
6054
+ applied = Math.max(Math.min(delta, neighbourShare - floor), floor - startShare);
6055
+ // The preview is written straight to the two header cells. Under the fixed layout their
6056
+ // widths are what lay the grid out, so this IS the live drag — and it costs no change
6057
+ // detection, where routing it through `columnWidths` ran a full-grid pass per mouse move
6058
+ // (~60/sec) to move two cells. The bindings overwrite these on the next pass anyway.
6059
+ th.style.width = `${startShare + applied}rem`;
6060
+ if (neighbourCell) {
6061
+ neighbourCell.style.width = `${neighbourShare - applied}rem`;
6062
+ }
6063
+ };
6064
+ const onUp = () => {
6065
+ controller.abort();
6066
+ this.resizeAbort = null;
6067
+ this.zone.run(() => {
6068
+ this.columnWidths[col.field] = startShare + applied;
6069
+ this.columnWidths[neighbour.field] = neighbourShare - applied;
6070
+ this.persistColumnLayout();
6071
+ });
6072
+ // Sorting hangs off CLICK, which stopPropagation on mousedown never touched — so every
6073
+ // resize also reordered the grid. Swallowing the click on the grip is not enough either:
6074
+ // on a drag that ends away from it, the click fires on their common ancestor, the th.
6075
+ // One capturing listener, once, on the document.
6076
+ const swallow = (click) => {
6077
+ click.stopPropagation();
6078
+ click.preventDefault();
6079
+ };
6080
+ document.addEventListener('click', swallow, {
6081
+ capture: true,
6082
+ once: true,
6083
+ });
6084
+ // Removed on the next tick rather than left to `once`. The click that follows a mouseup
6085
+ // fires synchronously, so this always catches it — while a drag that ends with no click at
6086
+ // all (the pointer leaves the window, the operator navigates away) would otherwise leave a
6087
+ // listener sitting on the document to eat someone's next click entirely.
6088
+ setTimeout(() => document.removeEventListener('click', swallow, { capture: true }));
6089
+ };
6090
+ // One controller for the pair, aborted by `onUp` AND by ngOnDestroy. Left to `removeEventListener`
6091
+ // inside `onUp` alone, navigating away mid-drag stranded `mousemove` on the document — patched
6092
+ // by zone.js, so it ticked the app on every mouse move for the rest of the session, with its
6093
+ // closure pinning the dead component and the page of rows it captured.
6094
+ this.zone.runOutsideAngular(() => {
6095
+ document.addEventListener('mousemove', onMove, {
6096
+ signal: controller.signal,
6097
+ });
6098
+ document.addEventListener('mouseup', onUp, { signal: controller.signal });
6099
+ });
4823
6100
  }
4824
6101
  /**
4825
- * Whether the filter type applies on every value change, which hides the menu's Apply
4826
- * button — an Apply there would promise a pending state that cannot exist. Boolean
4827
- * auto-applies through PrimeNG's own `onModelChange`; date and dropdown/multiselect
4828
- * through the filter templates THIS component projects, which call `filterCallback`
4829
- * directly (PrimeNG's built-in element, and with it its `onModelChange`, is only
4830
- * rendered when no template is projected — so the date path is ours, not PrimeNG's).
4831
- * With showApplyButton off, PrimeNG also auto-applies match-mode, operator and
4832
- * constraint-removal changes, so the whole menu stays consistent. Typed input
4833
- * (text/numeric) is deliberately NOT here — it commits on Enter/Apply, since applying
4834
- * per keystroke would fire a lazy load per key. Listing the auto types (not the typed
4835
- * ones) is the safe polarity: an unlisted future type keeps its Apply button rather
4836
- * than silently losing its commit.
6102
+ * The column's declared {@link Column.width}, or the default for its filter type.
4837
6103
  *
4838
- * Known gap this does not close, and it predates the flag: the projected date template
4839
- * binds `[ngModel]` one-way and commits on `(onSelect)`, which the datepicker raises for
4840
- * a calendar pick but not for `onUserInput` — so a date TYPED into the field never
4841
- * reaches the constraint at all, with or without an Apply button.
6104
+ * A width, not a minimum: under the fixed layout {@link tableStyle} establishes, this is what
6105
+ * the column is sized from, and the table shares its surplus in PROPORTION to these numbers.
6106
+ * Columns declaring none would otherwise take an equal share, which throws away what the
6107
+ * per-type defaults say — a boolean holds "Da"/"Ne", a text column holds a name.
4842
6108
  */
4843
- filterAppliesOnChange(filterType) {
4844
- return (filterType === 'boolean' ||
4845
- filterType === 'date' ||
4846
- this.isDropOrMulti(filterType));
6109
+ getColWidth(col) {
6110
+ const override = col.field ? this.columnWidths[col.field] : undefined;
6111
+ if (override != null)
6112
+ return `${override}rem`;
6113
+ // A declared width passes through VERBATIM so a consumer keeps % and px; every other branch is
6114
+ // a share and renders in rem. Both halves resolve through `columnShare` otherwise, because
6115
+ // this chain and that one were written twice and had already drifted apart on the px case.
6116
+ if (col.width != null)
6117
+ return col.width;
6118
+ return `${this.defaultShare(col)}rem`;
4847
6119
  }
4848
6120
  /*
4849
6121
  * Navigate to details page based on rowId and rowNavigationPath.
@@ -4943,6 +6215,8 @@ class SpiderlyDataTableComponent {
4943
6215
  ngOnDestroy() {
4944
6216
  this.destroy$.next();
4945
6217
  this.destroy$.complete();
6218
+ this.selectionWidthObserver?.disconnect();
6219
+ this.resizeAbort?.abort();
4946
6220
  }
4947
6221
  get showSelectAllCheckbox() {
4948
6222
  return (this.selectionMode === 'multiple' &&
@@ -4956,6 +6230,45 @@ class SpiderlyDataTableComponent {
4956
6230
  this.isAllSelected = null;
4957
6231
  this.fakeIsAllSelected = false;
4958
6232
  }
6233
+ /**
6234
+ * A COMMITTED filter re-queries; a draft does not. Re-fetching per keystroke would be the same
6235
+ * lie the chip bar exists to prevent, only paid for in requests rather than in credibility.
6236
+ *
6237
+ * Back to page one first, and off FRESH metadata rather than `reload()`: that one replays the
6238
+ * cached lazy-load event, whose `first` was copied when the event was built, so a filter applied
6239
+ * from page 3 would keep asking the server to skip 50 rows a narrower result set does not have.
6240
+ * The grid comes back empty for a value that is definitely in it.
6241
+ *
6242
+ * The first run is PrimeNG's: it fires the initial lazy load itself, and re-querying here would
6243
+ * make every table load twice.
6244
+ */
6245
+ requeryOnAppliedFilters() {
6246
+ let isFirstRun = true;
6247
+ effect(() => {
6248
+ this.filters.applied();
6249
+ if (isFirstRun) {
6250
+ isFirstRun = false;
6251
+ return;
6252
+ }
6253
+ this.persistAppliedFilters();
6254
+ this.refetchFromFirstPage();
6255
+ }, { injector: this.injector });
6256
+ }
6257
+ /**
6258
+ * Back to page one, state saved, one fetch off FRESH metadata — the shared tail of the
6259
+ * requery effect and `selectView`'s sort-only switch. The save is load-bearing: PrimeNG
6260
+ * saves state only from its own interactions, so without it the pre-change page offset
6261
+ * survives in storage and the NEXT visit replays it against a narrowed result set — the
6262
+ * backend answers data=[] for an offset the filtered total no longer reaches, and the grid
6263
+ * renders empty under chips claiming matches.
6264
+ */
6265
+ refetchFromFirstPage() {
6266
+ this.table.first = 0;
6267
+ this.table.firstChange.emit(0);
6268
+ if (this.table.isStateful())
6269
+ this.table.saveState();
6270
+ this.lazyLoad(this.table.createLazyLoadMetadata());
6271
+ }
4959
6272
  reload() {
4960
6273
  // Nothing to replay before the table's own initial load, which is already on its way.
4961
6274
  if (this.lastLazyLoadEvent == null)
@@ -5015,9 +6328,8 @@ class SpiderlyDataTableComponent {
5015
6328
  const local = typeof raw === 'string' ? parseDateOnlyLocal(raw) : null;
5016
6329
  return formatDate(local ?? raw, 'shortDate', this.locale);
5017
6330
  case 'multiselect': {
5018
- // Translate through the column's own option list — the same list its filter dropdown
5019
- // already renders labels from. Without this the cell shows the stored value, so an enum
5020
- // column reads `1` while its filter offers "Info", and an FK column reads the id.
6331
+ // Translate through the column's own option list. Without this the cell shows the stored
6332
+ // value, so an enum column reads `1` instead of "Info", and an FK column reads the id.
5021
6333
  //
5022
6334
  // Match on `code`, not `value`: that is the property PrimengOption carries (see the note
5023
6335
  // on the class). The raw value is the fallback rather than an empty cell, because the
@@ -5056,12 +6368,19 @@ class SpiderlyDataTableComponent {
5056
6368
  filter.additionalFilterIdLong = this.additionalFilterIdLong;
5057
6369
  exportListToExcel(this.exportListToExcelObservableMethod, filter);
5058
6370
  }
6371
+ /**
6372
+ * Clears everything that narrows the grid. With a store supplied it has to reach BOTH — the
6373
+ * store holds the constraints and PrimeNG holds the persisted state and sort — or the bar goes
6374
+ * empty while a reload brings the old filters back.
6375
+ */
5059
6376
  clear(table) {
6377
+ this.filters?.clear();
5060
6378
  table.clear();
5061
6379
  table.clearState();
5062
- // clear() re-queries without emitting (onFilter), so the icons would keep their fill
5063
- // on a client-side table; on a lazy one lazyLoad already covers it.
5064
- this.snapshotAppliedFilters(table.filters);
6380
+ // `table.clear()` landed the grid on the declared default (`applyDefaultSortIfUnsorted`),
6381
+ // so the view's sort override must go with it — or F5 would bring back the sort Clear
6382
+ // visibly removed.
6383
+ this.persistActiveViewSort();
5065
6384
  }
5066
6385
  //#region Selection
5067
6386
  setFakeIsAllSelected() {
@@ -5235,7 +6554,7 @@ class SpiderlyDataTableComponent {
5235
6554
  }
5236
6555
  }
5237
6556
  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 }); }
5238
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.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", 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 }], viewQueries: [{ 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 (onFilter)=\"filter($event)\"\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 [label]=\"t('ClearFilters')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-filter-slash\"\n (click)=\"clear(dt)\"\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 </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 style=\"width: 6rem\" *ngIf=\"selectionMode == 'multiple'\">\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 ({{ rowsSelectedNumber }})\n </div>\n </th>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <th\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"!isColumnSortable(col)\"\n [style.width]=\"getColWidth(col)\"\n >\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 </span>\n <!-- hideOnClear: Apply is what closes this menu in PrimeNG (applyFilter =\n _filter + hide), so wherever Apply is hidden Clear is the only button\n left and would otherwise clear-and-linger. Committing a VALUE still\n leaves the menu open deliberately \u2014 a multiselect gets ticked several\n times per visit; dismissal is the popover's own (outside click, Esc). -->\n <p-columnFilter\n *ngIf=\"col.filterType != null && col.filterType !== 'blob'\"\n [type]=\"col.filterType\"\n [field]=\"filterKey(col)\"\n display=\"menu\"\n [placeholder]=\"col.filterPlaceholder\"\n [showOperator]=\"false\"\n [showMatchModes]=\"col.showMatchModes\"\n [showAddButton]=\"col.showAddButton\"\n [showApplyButton]=\"!filterAppliesOnChange(col.filterType)\"\n [hideOnClear]=\"true\"\n [matchModeOptions]=\"getColMatchModeOptions(col)\"\n [matchMode]=\"getColMatchMode(col)\"\n >\n <!-- Marks a filtered column \u2014 PrimeNG's own <FilterIcon> cannot\n (CLAUDE.md \u2192 \"Active-filter header icon\"). Colour: the SCSS. -->\n <ng-template pTemplate=\"filtericon\">\n <i\n [class]=\"\n isColumnFiltered(col) ? 'pi pi-filter-fill' : 'pi pi-filter'\n \"\n ></i>\n </ng-template>\n <ng-template\n *ngIf=\"isDropOrMulti(col.filterType)\"\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-multiSelect\n [ngModel]=\"value\"\n [options]=\"col.dropdownOrMultiselectValues\"\n [placeholder]=\"t('All')\"\n (onChange)=\"filter($event.value)\"\n optionLabel=\"label\"\n optionValue=\"code\"\n [style]=\"{ width: '240px' }\"\n >\n <ng-template let-item pTemplate=\"item\">\n <div class=\"p-multiselect-representative-option\">\n <span class=\"ml-2\">{{ item.label }}</span>\n </div>\n </ng-template>\n </p-multiSelect>\n </ng-template>\n <ng-template\n *ngIf=\"col.filterType == 'date'\"\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-datepicker\n [ngModel]=\"value\"\n [showTime]=\"col.showTime\"\n (onSelect)=\"filter($event)\"\n ></p-datepicker>\n </ng-template>\n </p-columnFilter>\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 class=\"cell-text\">{{ displayValue }}</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\"\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 (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 *ngIf=\"col.editable\" class=\"row-interactive\">\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)=\"resetColumnVisibility()\"\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}.pi-filter-fill{color:var(--p-primary-color)}@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{margin-top:4px;padding:0;border:none;background:none;color:var(--p-primary-color);cursor:pointer;align-self:flex-start;font-size:.875rem}.column-chooser-reset:hover{text-decoration:underline}: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 td.selection-cell{-webkit-user-select:none;user-select:none}:host .cell-text{display:block;overflow:hidden;text-overflow:ellipsis;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: "component", type: i10.ColumnFilter, selector: "p-columnFilter", inputs: ["field", "type", "display", "showMenu", "matchMode", "operator", "showOperator", "showClearButton", "showApplyButton", "showMatchModes", "showAddButton", "hideOnClear", "placeholder", "matchModeOptions", "maxConstraints", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "locale", "localeMatcher", "currency", "currencyDisplay", "useGrouping", "showButtons", "ariaLabel", "filterButtonProps"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i12.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "fluid", "label", "icon", "buttonProps"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i4$5.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: i14.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { 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: 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: 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"] }] }); }
6557
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.2.13", type: SpiderlyDataTableComponent, isStandalone: true, selector: "spiderly-data-table", inputs: { tableTitle: "tableTitle", tableIcon: "tableIcon", items: "items", rows: "rows", rowsPerPageOptions: "rowsPerPageOptions", cols: "cols", filters: "filters", views: "views", showPaginator: "showPaginator", showCardWrapper: "showCardWrapper", readonly: "readonly", idField: "idField", getPaginatedListObservableMethod: "getPaginatedListObservableMethod", exportListToExcelObservableMethod: "exportListToExcelObservableMethod", deleteItemFromTableObservableMethod: "deleteItemFromTableObservableMethod", deleteListFromTableObservableMethod: "deleteListFromTableObservableMethod", newlySelectedItems: "newlySelectedItems", unselectedItems: "unselectedItems", selectionMode: "selectionMode", selectedLazyLoadObservableMethod: "selectedLazyLoadObservableMethod", additionalFilterIdLong: "additionalFilterIdLong", showAddButton: "showAddButton", showExportToExcelButton: "showExportToExcelButton", showReloadTableButton: "showReloadTableButton", getFormArrayItems: "getFormArrayItems", hasLazyLoad: "hasLazyLoad", stateKey: "stateKey", stateStorage: "stateStorage", defaultSortField: "defaultSortField", defaultSortOrder: "defaultSortOrder", getAlreadySelectedItemIds: "getAlreadySelectedItemIds", getAlreadySelectedItems: "getAlreadySelectedItems", getFormControl: "getFormControl", additionalIndexes: "additionalIndexes", navigateOnRowClick: "navigateOnRowClick", rowNavigationPath: "rowNavigationPath" }, outputs: { onTotalRecordsChange: "onTotalRecordsChange", onLazyLoad: "onLazyLoad", onIsAllSelectedChange: "onIsAllSelectedChange", onRowSelect: "onRowSelect", onRowUnselect: "onRowUnselect" }, queries: [{ propertyName: "actionsTemplate", first: true, predicate: SpiderlyDataTableActionsDirective, descendants: true, read: TemplateRef }, { propertyName: "cellTemplates", predicate: SpiderlyCellTemplateDirective }, { propertyName: "filterTemplates", predicate: SpiderlyFilterTemplateDirective }], viewQueries: [{ propertyName: "columnMenu", first: true, predicate: ["columnMenu"], descendants: true, isSignal: true }, { propertyName: "filterBar", first: true, predicate: SpiderlyFilterBarComponent, descendants: true, isSignal: true }, { propertyName: "selectionHeader", first: true, predicate: ["selectionHeader"], descendants: true, isSignal: true }, { propertyName: "table", first: true, predicate: ["dt"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div\n #tableContainer\n class=\"spiderly-table-container\"\n [attr.aria-busy]=\"isPending\"\n [ngClass]=\"{\n 'card responsive-card-padding overflow-auto': showCardWrapper\n }\"\n >\n <p-table\n #dt\n [value]=\"items\"\n [rows]=\"rows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [rowHover]=\"true\"\n [paginator]=\"showPaginator\"\n responsiveLayout=\"scroll\"\n [lazy]=\"hasLazyLoad\"\n (onLazyLoad)=\"lazyLoad($event)\"\n (onPage)=\"onPageChange(tableContainer)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"isPending\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n sortMode=\"multiple\"\n [multiSortMeta]=\"initialMultiSortMeta\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\n [tableStyle]=\"tableStyle\"\n >\n <ng-template pTemplate=\"caption\">\n <div class=\"table-header overflow-auto\">\n <div style=\"display: flex; align-items: center; gap: 8px\">\n <i class=\"{{ tableIcon }}\" style=\"font-size: 20px\"></i>\n <div style=\"margin: 0px; font-size: 17.5px\">{{ tableTitle }}</div>\n </div>\n <div style=\"display: flex; gap: 8px\">\n <ng-container\n *ngIf=\"actionsTemplate\"\n [ngTemplateOutlet]=\"actionsTemplate\"\n ></ng-container>\n <button\n pButton\n *ngIf=\"hasLazyLoad\"\n data-testid=\"column-chooser-button\"\n [label]=\"t('Columns')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-sliders-h\"\n (click)=\"columnChooser.toggle($event)\"\n ></button>\n <button\n pButton\n *ngIf=\"showExportToExcelButton\"\n [label]=\"t('ExportToExcel')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-download\"\n (click)=\"exportListToExcel()\"\n ></button>\n <button\n pButton\n *ngIf=\"showReloadTableButton\"\n [label]=\"t('Reload')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-refresh\"\n (click)=\"reload()\"\n ></button>\n <button\n pButton\n *ngIf=\"deleteListFromTableObservableMethod && rowsSelectedNumber > 0\"\n [label]=\"t('DeleteSelected') + ' (' + rowsSelectedNumber + ')'\"\n class=\"p-button-danger\"\n style=\"flex: none\"\n icon=\"pi pi-trash\"\n (click)=\"deleteSelectedObjects()\"\n ></button>\n </div>\n </div>\n <!-- ONE menu for every column, with the target held in `menuColumn`. Rendering its items\n inside an @if keyed on the column would put them inside the popover's projected\n content, and PrimeNG does not re-project content an @if has destroyed \u2014 the filter\n bar's editor lost a whole afternoon to exactly that. -->\n <p-popover #columnMenu>\n <div class=\"column-menu\" role=\"menu\">\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-hide\"\n [disabled]=\"!menuColumn || !canToggleColumn(menuColumn)\"\n (click)=\"hideMenuColumn()\"\n >\n <i class=\"pi pi-eye-slash\"></i>\n <span class=\"column-menu-item-label\">{{ t('HideColumn') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-filter\"\n [disabled]=\"!menuColumn || !canFilterColumn(menuColumn)\"\n (click)=\"filterMenuColumn()\"\n >\n <i class=\"pi pi-filter\"></i>\n <span class=\"column-menu-item-label\">{{ t('Filter') }}\u2026</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-asc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(1)\"\n >\n <i class=\"pi pi-sort-amount-up-alt\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortAscending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-desc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(-1)\"\n >\n <i class=\"pi pi-sort-amount-down\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortDescending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-left\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, -1)\"\n (click)=\"moveMenuColumn(-1)\"\n >\n <i class=\"pi pi-arrow-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveLeft') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-right\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, 1)\"\n (click)=\"moveMenuColumn(1)\"\n >\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveRight') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-fit\"\n (click)=\"fitMenuColumn()\"\n >\n <i class=\"pi pi-arrows-h\"></i>\n <span class=\"column-menu-item-label\">{{ t('FitWidth') }}</span>\n </button>\n <!-- menuitemcheckbox, not menuitem: this one TOGGLES, and a label reading the same\n in both states leaves the operator to find out by clicking \u2014 which on a wide\n column means re-laying the grid out to learn what it was already set to. The\n check is the state; the label stays put, which is what the role is for.\n Disabled for an editable column: its cell holds an input, not clamped text, so\n the toggle could only persist a preference that changes nothing. -->\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-wrap\"\n [disabled]=\"!menuColumn || menuColumn.editable === true\"\n [attr.aria-checked]=\"isMenuColumnWrapped\"\n (click)=\"toggleWrapForMenuColumn()\"\n >\n <i class=\"pi pi-align-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('WrapText') }}</span>\n <i *ngIf=\"isMenuColumnWrapped\" class=\"pi pi-check\"></i>\n </button>\n </div>\n </p-popover>\n\n <div class=\"table-views\" *ngIf=\"views?.length\" role=\"tablist\">\n <button\n *ngFor=\"let view of views\"\n type=\"button\"\n role=\"tab\"\n class=\"table-view\"\n data-testid=\"table-view\"\n [class.table-view-active]=\"activeViewId === view.id\"\n [attr.aria-selected]=\"activeViewId === view.id\"\n (click)=\"selectView(view)\"\n >\n {{ view.label }}\n </button>\n </div>\n\n <spiderly-filter-bar\n *ngIf=\"filters\"\n [filters]=\"filters\"\n [totalRecords]=\"totalRecords\"\n [sort]=\"sortKeys\"\n [controlTemplates]=\"filterTemplates\"\n (clearAll)=\"clear(dt)\"\n ></spiderly-filter-bar>\n </ng-template>\n <ng-template pTemplate=\"header\">\n <tr>\n <!-- Sized for the checkbox plus the counter beside it, which shows `totalRecords` on a\n select-all and so reaches five digits on a large table. Not `0rem` (see CLAUDE.md \u2192\n Column widths), and not routed through getColWidth: this column has no `Column`. -->\n <th\n #selectionHeader\n class=\"frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.width]=\"selectionColumnWidth\"\n [style.left]=\"'0'\"\n *ngIf=\"selectionMode == 'multiple'\"\n >\n <div style=\"display: flex; gap: 8px\">\n <p-checkbox\n *ngIf=\"showSelectAllCheckbox\"\n [disabled]=\"readonly\"\n (onChange)=\"selectAll($event.checked)\"\n [(ngModel)]=\"fakeIsAllSelected\"\n [binary]=\"true\"\n ></p-checkbox>\n <span class=\"selection-count\">({{ rowsSelectedNumber }})</span>\n </div>\n </th>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <th\n #headerCell\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"!isColumnSortable(col)\"\n [style.width]=\"getColWidth(col)\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n [attr.draggable]=\"hasLazyLoad && !col.lockVisible ? true : null\"\n (mousedown)=\"onHeaderMouseDown($event)\"\n (dragstart)=\"onHeaderDragStart(col, $event)\"\n (dragover)=\"onHeaderDragOver(col, $event)\"\n (drop)=\"onHeaderDrop(col, $event)\"\n >\n <!-- Ours, not PrimeNG's: enabling [resizableColumns] makes it persist widths by\n th:nth-child INDEX into our own stateKey, which slides every stored width onto\n the wrong column the first time the chooser hides one (CLAUDE.md -> decision\n 5). We take the gesture and keep the state. -->\n <span\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n class=\"column-resizer\"\n data-testid=\"column-resizer\"\n (mousedown)=\"startColumnResize(col, $event, headerCell)\"\n ></span>\n <div\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n \"\n >\n <span style=\"display: flex; align-items: center; gap: 4px\">\n {{ col.name }}\n <p-sortIcon\n *ngIf=\"isColumnSortable(col)\"\n [field]=\"col.field!\"\n ></p-sortIcon>\n <!-- stopPropagation is load-bearing: the th carries pSortableColumn, so a click\n that reaches it sorts the grid. Opening a menu must not reorder the table on\n the way. -->\n <button\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n type=\"button\"\n class=\"column-menu-button\"\n data-testid=\"column-menu\"\n [attr.aria-label]=\"t('Columns')\"\n (click)=\"$event.stopPropagation(); openColumnMenu(col, $event)\"\n >\n <i class=\"pi pi-chevron-down\"></i>\n </button>\n </span>\n </div>\n </th>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template\n pTemplate=\"body\"\n let-rowData\n let-index=\"rowIndex\"\n let-editing=\"editing\"\n >\n <!-- The cell rendering every column gets unless it projects a spiderlyCellTemplate. Declared\n once here rather than inside the column loop, and fed the same context the projected\n template gets, so getRowData is evaluated exactly once per cell either way. -->\n <ng-template #defaultCell let-col=\"col\" let-displayValue=\"displayValue\">\n <ng-container *ngIf=\"col.filterType === 'blob'\">\n <img width=\"45\" [src]=\"displayValue\" alt=\"\" />\n </ng-container>\n <ng-container *ngIf=\"col.filterType !== 'blob'\">\n <span spiderlyOverflowTitle class=\"cell-text\">{{\n displayValue\n }}</span>\n </ng-container>\n </ng-template>\n <tr\n [class.clickable]=\"navigateOnRowClick\"\n (click)=\"onRowClick(rowData, $event)\"\n >\n <!-- row-interactive: a selection gesture must never double as row navigation (onRowClick). -->\n <td\n *ngIf=\"selectionMode == 'multiple'\"\n class=\"selection-cell row-interactive frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.left]=\"'0'\"\n (mousedown)=\"onSelectionCellMouseDown($event, rowData[idField])\"\n >\n <p-checkbox\n [disabled]=\"readonly\"\n (onChange)=\"selectRow(rowData[idField], rowData.index)\"\n [ngModel]=\"isRowSelected(rowData[idField])\"\n [binary]=\"true\"\n ></p-checkbox>\n </td>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <td\n [style]=\"getStyleForBodyColumn(col)\"\n [class.clickable]=\"col.onCellClick\"\n [class.row-interactive]=\"col.onCellClick\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [class.cell-wrap]=\"isColumnWrapped(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n (click)=\"onCellClick(col, rowData, $event)\"\n *ngIf=\"!col.editable\"\n >\n <!-- getColWidth reserves this column's width from `actions.length`; this gap is\n the geometry it reserves for, so changing it belongs in the same edit. -->\n <div\n style=\"\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 18px;\n \"\n >\n <ng-container\n *ngFor=\"let action of col.actions; trackBy: actionTrackByFn\"\n >\n <span\n [pTooltip]=\"action.name\"\n [class]=\"getClassForAction(action) + ' row-interactive'\"\n [style]=\"getStyleForAction(action)\"\n (click)=\"getMethodForAction(action, rowData, $event)\"\n ></span>\n </ng-container>\n </div>\n <ng-container\n [ngTemplateOutlet]=\"getCellTemplate(col) ?? defaultCell\"\n [ngTemplateOutletContext]=\"{\n $implicit: rowData,\n value: rowData[col.field],\n displayValue: getRowData(rowData, col),\n col: col\n }\"\n ></ng-container>\n </td>\n <td\n *ngIf=\"col.editable\"\n class=\"row-interactive\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n >\n <spiderly-number\n [control]=\"getFormArrayControlByIndex(col.field, rowData.index)\"\n [showLabel]=\"false\"\n ></spiderly-number>\n </td>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n <tr>\n <td\n [attr.colspan]=\"\n visibleCols.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"NoRecordsFound\") }}\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template pTemplate=\"paginatorright\">\n <div style=\"display: flex; justify-content: end; gap: 10px\">\n <spiderly-button\n *ngIf=\"showAddButton\"\n [label]=\"t('AddNew')\"\n icon=\"pi pi-plus\"\n (onClick)=\"navigateToDetails(0)\"\n ></spiderly-button>\n </div>\n </ng-template>\n </p-table>\n <p-popover #columnChooser>\n <div class=\"column-chooser\">\n <label\n *ngFor=\"let col of chooserCols; trackBy: colTrackByFn\"\n data-testid=\"column-chooser-option\"\n class=\"column-chooser-option\"\n >\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isColumnVisible(col)\"\n [disabled]=\"!canToggleColumn(col)\"\n (onChange)=\"toggleColumn(col, $event.checked)\"\n ></p-checkbox>\n <span>{{ col.name }}</span>\n </label>\n <button\n type=\"button\"\n data-testid=\"column-chooser-reset\"\n class=\"column-chooser-reset\"\n (click)=\"resetColumnLayout()\"\n >\n {{ t('ResetToDefault') }}\n </button>\n </div>\n </p-popover>\n </div>\n</ng-container>\n", styles: [".table-header{display:flex;justify-content:space-between;align-items:center}@media (max-width: 640px){.table-header{display:flex;flex-direction:column;align-items:start;gap:14px}}.spiderly-table .p-paginator{padding:1rem}@media (min-width: 1400px){.spiderly-table .p-paginator-left-content{position:absolute;padding:14px;left:0}}@media (min-width: 1400px){.spiderly-table .p-paginator-right-content{position:absolute;padding:14px;right:0}}.column-chooser{display:flex;flex-direction:column;gap:8px}.column-chooser-option{display:flex;align-items:center;gap:8px;cursor:pointer}.column-chooser-reset{border:none;background:none;color:inherit;font:inherit;cursor:pointer;padding:0;margin-top:4px;color:var(--p-primary-color);align-self:flex-start;font-size:.875rem}.column-chooser-reset:hover{text-decoration:underline}.column-menu{display:flex;flex-direction:column;min-width:11rem}.column-menu-item{display:flex;align-items:center;gap:8px;border:none;background:none;color:inherit;font:inherit;cursor:pointer;text-align:left;padding:6px 10px;border-radius:var(--p-content-border-radius)}.column-menu-item .column-menu-item-label{flex:1}.column-menu-item:hover:not(:disabled){background:var(--p-content-hover-background)}.column-menu-item:disabled{color:var(--p-text-muted-color);cursor:default}:host .spiderly-table-container{scroll-margin-top:calc(var(--spiderly-viewport-top-inset, 0px) + 1rem)}:host ::ng-deep .clickable{cursor:pointer}:host ::ng-deep td.clickable:hover{text-decoration:underline}:host .column-menu-button{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:0 2px;opacity:0;font-size:.75rem}:host th:hover .column-menu-button,:host .column-menu-button:focus-visible{opacity:1}:host .table-views{display:flex;flex-wrap:wrap;gap:4px;padding-top:8px}:host .table-view{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:4px 10px;border-radius:var(--p-content-border-radius)}:host .table-view:hover{background:var(--p-content-hover-background)}:host .table-view-active{color:var(--p-primary-color);font-weight:600;background:var(--p-content-hover-background)}:host .frozen-column{position:sticky;z-index:1}:host td.frozen-column{background:var(--p-datatable-row-background, var(--p-content-background));transition:background-color var(--p-transition-duration, .2s)}:host tr:hover>td.frozen-column{background:var(--p-datatable-row-hover-background, inherit)}:host .frozen-column-last{box-shadow:2px 0 4px -2px #00000026}:host th{position:relative}:host .column-resizer{position:absolute;top:0;right:0;width:8px;height:100%;cursor:col-resize;-webkit-user-select:none;user-select:none}:host .selection-count{font-size:.75rem;color:var(--p-text-muted-color)}:host td.selection-cell{-webkit-user-select:none;user-select:none}:host td.cell-wrap{--spiderly-cell-overflow: visible;--spiderly-cell-white-space: normal}:host .cell-text{display:block;overflow:var(--spiderly-cell-overflow, hidden);text-overflow:ellipsis;white-space:var(--spiderly-cell-white-space, nowrap)}:host ::ng-deep .p-datatable-mask{background:color-mix(in srgb,var(--p-content-background, #ffffff) 65%,transparent);color:var(--p-primary-color, #334155);align-items:flex-start;padding-top:6rem}:host ::ng-deep .p-datatable-thead{position:unset!important}:host ::ng-deep .p-datatable-header{border-radius:6px 6px 0 0}:host ::ng-deep .p-datatable{border-radius:var(--p-content-border-radius);border:1px solid var(--p-datatable-border-color)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: SpiderlyControlsModule }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "component", type: SpiderlyNumberComponent, selector: "spiderly-number", inputs: ["prefix", "showButtons", "decimal", "maxFractionDigits"] }, { kind: "ngmodule", type: TableModule }, { kind: "component", type: i10.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "style", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "scrollDirection", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "responsive", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "autoLayout", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "virtualRowHeight", "selectAll"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "directive", type: i1$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "directive", type: i10.SortableColumn, selector: "[pSortableColumn]", inputs: ["pSortableColumn", "pSortableColumnDisabled"] }, { kind: "component", type: i10.SortIcon, selector: "p-sortIcon", inputs: ["field"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3$3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "fluid", "label", "icon", "buttonProps"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i8.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["value", "name", "disabled", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "inputStyle", "styleClass", "inputClass", "indeterminate", "size", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "component", type: SpiderlyFilterBarComponent, selector: "spiderly-filter-bar", inputs: ["filters", "totalRecords", "sort", "controlTemplates"], outputs: ["clearAll"] }, { kind: "directive", type: SpiderlyOverflowTitleDirective, selector: "[spiderlyOverflowTitle]" }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }] }); }
5239
6558
  }
5240
6559
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyDataTableComponent, decorators: [{
5241
6560
  type: Component,
@@ -5246,12 +6565,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
5246
6565
  SpiderlyControlsModule,
5247
6566
  TableModule,
5248
6567
  ButtonModule,
5249
- MultiSelectModule,
5250
6568
  PopoverModule,
5251
- DatePickerModule,
5252
6569
  CheckboxModule,
6570
+ SpiderlyFilterBarComponent,
6571
+ SpiderlyOverflowTitleDirective,
5253
6572
  TooltipModule,
5254
- ], 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 (onFilter)=\"filter($event)\"\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 [label]=\"t('ClearFilters')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-filter-slash\"\n (click)=\"clear(dt)\"\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 </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 style=\"width: 6rem\" *ngIf=\"selectionMode == 'multiple'\">\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 ({{ rowsSelectedNumber }})\n </div>\n </th>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <th\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"!isColumnSortable(col)\"\n [style.width]=\"getColWidth(col)\"\n >\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 </span>\n <!-- hideOnClear: Apply is what closes this menu in PrimeNG (applyFilter =\n _filter + hide), so wherever Apply is hidden Clear is the only button\n left and would otherwise clear-and-linger. Committing a VALUE still\n leaves the menu open deliberately \u2014 a multiselect gets ticked several\n times per visit; dismissal is the popover's own (outside click, Esc). -->\n <p-columnFilter\n *ngIf=\"col.filterType != null && col.filterType !== 'blob'\"\n [type]=\"col.filterType\"\n [field]=\"filterKey(col)\"\n display=\"menu\"\n [placeholder]=\"col.filterPlaceholder\"\n [showOperator]=\"false\"\n [showMatchModes]=\"col.showMatchModes\"\n [showAddButton]=\"col.showAddButton\"\n [showApplyButton]=\"!filterAppliesOnChange(col.filterType)\"\n [hideOnClear]=\"true\"\n [matchModeOptions]=\"getColMatchModeOptions(col)\"\n [matchMode]=\"getColMatchMode(col)\"\n >\n <!-- Marks a filtered column \u2014 PrimeNG's own <FilterIcon> cannot\n (CLAUDE.md \u2192 \"Active-filter header icon\"). Colour: the SCSS. -->\n <ng-template pTemplate=\"filtericon\">\n <i\n [class]=\"\n isColumnFiltered(col) ? 'pi pi-filter-fill' : 'pi pi-filter'\n \"\n ></i>\n </ng-template>\n <ng-template\n *ngIf=\"isDropOrMulti(col.filterType)\"\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-multiSelect\n [ngModel]=\"value\"\n [options]=\"col.dropdownOrMultiselectValues\"\n [placeholder]=\"t('All')\"\n (onChange)=\"filter($event.value)\"\n optionLabel=\"label\"\n optionValue=\"code\"\n [style]=\"{ width: '240px' }\"\n >\n <ng-template let-item pTemplate=\"item\">\n <div class=\"p-multiselect-representative-option\">\n <span class=\"ml-2\">{{ item.label }}</span>\n </div>\n </ng-template>\n </p-multiSelect>\n </ng-template>\n <ng-template\n *ngIf=\"col.filterType == 'date'\"\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-datepicker\n [ngModel]=\"value\"\n [showTime]=\"col.showTime\"\n (onSelect)=\"filter($event)\"\n ></p-datepicker>\n </ng-template>\n </p-columnFilter>\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 class=\"cell-text\">{{ displayValue }}</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\"\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 (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 *ngIf=\"col.editable\" class=\"row-interactive\">\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)=\"resetColumnVisibility()\"\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}.pi-filter-fill{color:var(--p-primary-color)}@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{margin-top:4px;padding:0;border:none;background:none;color:var(--p-primary-color);cursor:pointer;align-self:flex-start;font-size:.875rem}.column-chooser-reset:hover{text-decoration:underline}: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 td.selection-cell{-webkit-user-select:none;user-select:none}:host .cell-text{display:block;overflow:hidden;text-overflow:ellipsis;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"] }]
6573
+ ], template: "<ng-container *transloco=\"let t\">\n <div\n #tableContainer\n class=\"spiderly-table-container\"\n [attr.aria-busy]=\"isPending\"\n [ngClass]=\"{\n 'card responsive-card-padding overflow-auto': showCardWrapper\n }\"\n >\n <p-table\n #dt\n [value]=\"items\"\n [rows]=\"rows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [rowHover]=\"true\"\n [paginator]=\"showPaginator\"\n responsiveLayout=\"scroll\"\n [lazy]=\"hasLazyLoad\"\n (onLazyLoad)=\"lazyLoad($event)\"\n (onPage)=\"onPageChange(tableContainer)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"isPending\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n sortMode=\"multiple\"\n [multiSortMeta]=\"initialMultiSortMeta\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\n [tableStyle]=\"tableStyle\"\n >\n <ng-template pTemplate=\"caption\">\n <div class=\"table-header overflow-auto\">\n <div style=\"display: flex; align-items: center; gap: 8px\">\n <i class=\"{{ tableIcon }}\" style=\"font-size: 20px\"></i>\n <div style=\"margin: 0px; font-size: 17.5px\">{{ tableTitle }}</div>\n </div>\n <div style=\"display: flex; gap: 8px\">\n <ng-container\n *ngIf=\"actionsTemplate\"\n [ngTemplateOutlet]=\"actionsTemplate\"\n ></ng-container>\n <button\n pButton\n *ngIf=\"hasLazyLoad\"\n data-testid=\"column-chooser-button\"\n [label]=\"t('Columns')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-sliders-h\"\n (click)=\"columnChooser.toggle($event)\"\n ></button>\n <button\n pButton\n *ngIf=\"showExportToExcelButton\"\n [label]=\"t('ExportToExcel')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-download\"\n (click)=\"exportListToExcel()\"\n ></button>\n <button\n pButton\n *ngIf=\"showReloadTableButton\"\n [label]=\"t('Reload')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-refresh\"\n (click)=\"reload()\"\n ></button>\n <button\n pButton\n *ngIf=\"deleteListFromTableObservableMethod && rowsSelectedNumber > 0\"\n [label]=\"t('DeleteSelected') + ' (' + rowsSelectedNumber + ')'\"\n class=\"p-button-danger\"\n style=\"flex: none\"\n icon=\"pi pi-trash\"\n (click)=\"deleteSelectedObjects()\"\n ></button>\n </div>\n </div>\n <!-- ONE menu for every column, with the target held in `menuColumn`. Rendering its items\n inside an @if keyed on the column would put them inside the popover's projected\n content, and PrimeNG does not re-project content an @if has destroyed \u2014 the filter\n bar's editor lost a whole afternoon to exactly that. -->\n <p-popover #columnMenu>\n <div class=\"column-menu\" role=\"menu\">\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-hide\"\n [disabled]=\"!menuColumn || !canToggleColumn(menuColumn)\"\n (click)=\"hideMenuColumn()\"\n >\n <i class=\"pi pi-eye-slash\"></i>\n <span class=\"column-menu-item-label\">{{ t('HideColumn') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-filter\"\n [disabled]=\"!menuColumn || !canFilterColumn(menuColumn)\"\n (click)=\"filterMenuColumn()\"\n >\n <i class=\"pi pi-filter\"></i>\n <span class=\"column-menu-item-label\">{{ t('Filter') }}\u2026</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-asc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(1)\"\n >\n <i class=\"pi pi-sort-amount-up-alt\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortAscending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-desc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(-1)\"\n >\n <i class=\"pi pi-sort-amount-down\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortDescending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-left\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, -1)\"\n (click)=\"moveMenuColumn(-1)\"\n >\n <i class=\"pi pi-arrow-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveLeft') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-right\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, 1)\"\n (click)=\"moveMenuColumn(1)\"\n >\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveRight') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-fit\"\n (click)=\"fitMenuColumn()\"\n >\n <i class=\"pi pi-arrows-h\"></i>\n <span class=\"column-menu-item-label\">{{ t('FitWidth') }}</span>\n </button>\n <!-- menuitemcheckbox, not menuitem: this one TOGGLES, and a label reading the same\n in both states leaves the operator to find out by clicking \u2014 which on a wide\n column means re-laying the grid out to learn what it was already set to. The\n check is the state; the label stays put, which is what the role is for.\n Disabled for an editable column: its cell holds an input, not clamped text, so\n the toggle could only persist a preference that changes nothing. -->\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-wrap\"\n [disabled]=\"!menuColumn || menuColumn.editable === true\"\n [attr.aria-checked]=\"isMenuColumnWrapped\"\n (click)=\"toggleWrapForMenuColumn()\"\n >\n <i class=\"pi pi-align-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('WrapText') }}</span>\n <i *ngIf=\"isMenuColumnWrapped\" class=\"pi pi-check\"></i>\n </button>\n </div>\n </p-popover>\n\n <div class=\"table-views\" *ngIf=\"views?.length\" role=\"tablist\">\n <button\n *ngFor=\"let view of views\"\n type=\"button\"\n role=\"tab\"\n class=\"table-view\"\n data-testid=\"table-view\"\n [class.table-view-active]=\"activeViewId === view.id\"\n [attr.aria-selected]=\"activeViewId === view.id\"\n (click)=\"selectView(view)\"\n >\n {{ view.label }}\n </button>\n </div>\n\n <spiderly-filter-bar\n *ngIf=\"filters\"\n [filters]=\"filters\"\n [totalRecords]=\"totalRecords\"\n [sort]=\"sortKeys\"\n [controlTemplates]=\"filterTemplates\"\n (clearAll)=\"clear(dt)\"\n ></spiderly-filter-bar>\n </ng-template>\n <ng-template pTemplate=\"header\">\n <tr>\n <!-- Sized for the checkbox plus the counter beside it, which shows `totalRecords` on a\n select-all and so reaches five digits on a large table. Not `0rem` (see CLAUDE.md \u2192\n Column widths), and not routed through getColWidth: this column has no `Column`. -->\n <th\n #selectionHeader\n class=\"frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.width]=\"selectionColumnWidth\"\n [style.left]=\"'0'\"\n *ngIf=\"selectionMode == 'multiple'\"\n >\n <div style=\"display: flex; gap: 8px\">\n <p-checkbox\n *ngIf=\"showSelectAllCheckbox\"\n [disabled]=\"readonly\"\n (onChange)=\"selectAll($event.checked)\"\n [(ngModel)]=\"fakeIsAllSelected\"\n [binary]=\"true\"\n ></p-checkbox>\n <span class=\"selection-count\">({{ rowsSelectedNumber }})</span>\n </div>\n </th>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <th\n #headerCell\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"!isColumnSortable(col)\"\n [style.width]=\"getColWidth(col)\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n [attr.draggable]=\"hasLazyLoad && !col.lockVisible ? true : null\"\n (mousedown)=\"onHeaderMouseDown($event)\"\n (dragstart)=\"onHeaderDragStart(col, $event)\"\n (dragover)=\"onHeaderDragOver(col, $event)\"\n (drop)=\"onHeaderDrop(col, $event)\"\n >\n <!-- Ours, not PrimeNG's: enabling [resizableColumns] makes it persist widths by\n th:nth-child INDEX into our own stateKey, which slides every stored width onto\n the wrong column the first time the chooser hides one (CLAUDE.md -> decision\n 5). We take the gesture and keep the state. -->\n <span\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n class=\"column-resizer\"\n data-testid=\"column-resizer\"\n (mousedown)=\"startColumnResize(col, $event, headerCell)\"\n ></span>\n <div\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n \"\n >\n <span style=\"display: flex; align-items: center; gap: 4px\">\n {{ col.name }}\n <p-sortIcon\n *ngIf=\"isColumnSortable(col)\"\n [field]=\"col.field!\"\n ></p-sortIcon>\n <!-- stopPropagation is load-bearing: the th carries pSortableColumn, so a click\n that reaches it sorts the grid. Opening a menu must not reorder the table on\n the way. -->\n <button\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n type=\"button\"\n class=\"column-menu-button\"\n data-testid=\"column-menu\"\n [attr.aria-label]=\"t('Columns')\"\n (click)=\"$event.stopPropagation(); openColumnMenu(col, $event)\"\n >\n <i class=\"pi pi-chevron-down\"></i>\n </button>\n </span>\n </div>\n </th>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template\n pTemplate=\"body\"\n let-rowData\n let-index=\"rowIndex\"\n let-editing=\"editing\"\n >\n <!-- The cell rendering every column gets unless it projects a spiderlyCellTemplate. Declared\n once here rather than inside the column loop, and fed the same context the projected\n template gets, so getRowData is evaluated exactly once per cell either way. -->\n <ng-template #defaultCell let-col=\"col\" let-displayValue=\"displayValue\">\n <ng-container *ngIf=\"col.filterType === 'blob'\">\n <img width=\"45\" [src]=\"displayValue\" alt=\"\" />\n </ng-container>\n <ng-container *ngIf=\"col.filterType !== 'blob'\">\n <span spiderlyOverflowTitle class=\"cell-text\">{{\n displayValue\n }}</span>\n </ng-container>\n </ng-template>\n <tr\n [class.clickable]=\"navigateOnRowClick\"\n (click)=\"onRowClick(rowData, $event)\"\n >\n <!-- row-interactive: a selection gesture must never double as row navigation (onRowClick). -->\n <td\n *ngIf=\"selectionMode == 'multiple'\"\n class=\"selection-cell row-interactive frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.left]=\"'0'\"\n (mousedown)=\"onSelectionCellMouseDown($event, rowData[idField])\"\n >\n <p-checkbox\n [disabled]=\"readonly\"\n (onChange)=\"selectRow(rowData[idField], rowData.index)\"\n [ngModel]=\"isRowSelected(rowData[idField])\"\n [binary]=\"true\"\n ></p-checkbox>\n </td>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <td\n [style]=\"getStyleForBodyColumn(col)\"\n [class.clickable]=\"col.onCellClick\"\n [class.row-interactive]=\"col.onCellClick\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [class.cell-wrap]=\"isColumnWrapped(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n (click)=\"onCellClick(col, rowData, $event)\"\n *ngIf=\"!col.editable\"\n >\n <!-- getColWidth reserves this column's width from `actions.length`; this gap is\n the geometry it reserves for, so changing it belongs in the same edit. -->\n <div\n style=\"\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 18px;\n \"\n >\n <ng-container\n *ngFor=\"let action of col.actions; trackBy: actionTrackByFn\"\n >\n <span\n [pTooltip]=\"action.name\"\n [class]=\"getClassForAction(action) + ' row-interactive'\"\n [style]=\"getStyleForAction(action)\"\n (click)=\"getMethodForAction(action, rowData, $event)\"\n ></span>\n </ng-container>\n </div>\n <ng-container\n [ngTemplateOutlet]=\"getCellTemplate(col) ?? defaultCell\"\n [ngTemplateOutletContext]=\"{\n $implicit: rowData,\n value: rowData[col.field],\n displayValue: getRowData(rowData, col),\n col: col\n }\"\n ></ng-container>\n </td>\n <td\n *ngIf=\"col.editable\"\n class=\"row-interactive\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n >\n <spiderly-number\n [control]=\"getFormArrayControlByIndex(col.field, rowData.index)\"\n [showLabel]=\"false\"\n ></spiderly-number>\n </td>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n <tr>\n <td\n [attr.colspan]=\"\n visibleCols.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"NoRecordsFound\") }}\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template pTemplate=\"paginatorright\">\n <div style=\"display: flex; justify-content: end; gap: 10px\">\n <spiderly-button\n *ngIf=\"showAddButton\"\n [label]=\"t('AddNew')\"\n icon=\"pi pi-plus\"\n (onClick)=\"navigateToDetails(0)\"\n ></spiderly-button>\n </div>\n </ng-template>\n </p-table>\n <p-popover #columnChooser>\n <div class=\"column-chooser\">\n <label\n *ngFor=\"let col of chooserCols; trackBy: colTrackByFn\"\n data-testid=\"column-chooser-option\"\n class=\"column-chooser-option\"\n >\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isColumnVisible(col)\"\n [disabled]=\"!canToggleColumn(col)\"\n (onChange)=\"toggleColumn(col, $event.checked)\"\n ></p-checkbox>\n <span>{{ col.name }}</span>\n </label>\n <button\n type=\"button\"\n data-testid=\"column-chooser-reset\"\n class=\"column-chooser-reset\"\n (click)=\"resetColumnLayout()\"\n >\n {{ t('ResetToDefault') }}\n </button>\n </div>\n </p-popover>\n </div>\n</ng-container>\n", styles: [".table-header{display:flex;justify-content:space-between;align-items:center}@media (max-width: 640px){.table-header{display:flex;flex-direction:column;align-items:start;gap:14px}}.spiderly-table .p-paginator{padding:1rem}@media (min-width: 1400px){.spiderly-table .p-paginator-left-content{position:absolute;padding:14px;left:0}}@media (min-width: 1400px){.spiderly-table .p-paginator-right-content{position:absolute;padding:14px;right:0}}.column-chooser{display:flex;flex-direction:column;gap:8px}.column-chooser-option{display:flex;align-items:center;gap:8px;cursor:pointer}.column-chooser-reset{border:none;background:none;color:inherit;font:inherit;cursor:pointer;padding:0;margin-top:4px;color:var(--p-primary-color);align-self:flex-start;font-size:.875rem}.column-chooser-reset:hover{text-decoration:underline}.column-menu{display:flex;flex-direction:column;min-width:11rem}.column-menu-item{display:flex;align-items:center;gap:8px;border:none;background:none;color:inherit;font:inherit;cursor:pointer;text-align:left;padding:6px 10px;border-radius:var(--p-content-border-radius)}.column-menu-item .column-menu-item-label{flex:1}.column-menu-item:hover:not(:disabled){background:var(--p-content-hover-background)}.column-menu-item:disabled{color:var(--p-text-muted-color);cursor:default}:host .spiderly-table-container{scroll-margin-top:calc(var(--spiderly-viewport-top-inset, 0px) + 1rem)}:host ::ng-deep .clickable{cursor:pointer}:host ::ng-deep td.clickable:hover{text-decoration:underline}:host .column-menu-button{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:0 2px;opacity:0;font-size:.75rem}:host th:hover .column-menu-button,:host .column-menu-button:focus-visible{opacity:1}:host .table-views{display:flex;flex-wrap:wrap;gap:4px;padding-top:8px}:host .table-view{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:4px 10px;border-radius:var(--p-content-border-radius)}:host .table-view:hover{background:var(--p-content-hover-background)}:host .table-view-active{color:var(--p-primary-color);font-weight:600;background:var(--p-content-hover-background)}:host .frozen-column{position:sticky;z-index:1}:host td.frozen-column{background:var(--p-datatable-row-background, var(--p-content-background));transition:background-color var(--p-transition-duration, .2s)}:host tr:hover>td.frozen-column{background:var(--p-datatable-row-hover-background, inherit)}:host .frozen-column-last{box-shadow:2px 0 4px -2px #00000026}:host th{position:relative}:host .column-resizer{position:absolute;top:0;right:0;width:8px;height:100%;cursor:col-resize;-webkit-user-select:none;user-select:none}:host .selection-count{font-size:.75rem;color:var(--p-text-muted-color)}:host td.selection-cell{-webkit-user-select:none;user-select:none}:host td.cell-wrap{--spiderly-cell-overflow: visible;--spiderly-cell-white-space: normal}:host .cell-text{display:block;overflow:var(--spiderly-cell-overflow, hidden);text-overflow:ellipsis;white-space:var(--spiderly-cell-white-space, nowrap)}:host ::ng-deep .p-datatable-mask{background:color-mix(in srgb,var(--p-content-background, #ffffff) 65%,transparent);color:var(--p-primary-color, #334155);align-items:flex-start;padding-top:6rem}:host ::ng-deep .p-datatable-thead{position:unset!important}:host ::ng-deep .p-datatable-header{border-radius:6px 6px 0 0}:host ::ng-deep .p-datatable{border-radius:var(--p-content-border-radius);border:1px solid var(--p-datatable-border-color)}\n"] }]
5255
6574
  }], 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: [{
5256
6575
  type: Inject,
5257
6576
  args: [LOCALE_ID]
@@ -5276,6 +6595,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
5276
6595
  type: Input
5277
6596
  }], cols: [{
5278
6597
  type: Input
6598
+ }], filters: [{
6599
+ type: Input
6600
+ }], views: [{
6601
+ type: Input
5279
6602
  }], showPaginator: [{
5280
6603
  type: Input
5281
6604
  }], showCardWrapper: [{
@@ -5342,6 +6665,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
5342
6665
  type: Input
5343
6666
  }], rowNavigationPath: [{
5344
6667
  type: Input
6668
+ }], filterTemplates: [{
6669
+ type: ContentChildren,
6670
+ args: [SpiderlyFilterTemplateDirective]
5345
6671
  }] } });
5346
6672
  class Action {
5347
6673
  constructor({ name, field, icon, style, styleClass, onClick, } = {}) {
@@ -5354,15 +6680,13 @@ class Action {
5354
6680
  }
5355
6681
  }
5356
6682
  class Column {
5357
- constructor({ name, field, filterField, filterType, filterPlaceholder, showMatchModes, matchModes, showAddButton, dropdownOrMultiselectValues, actions, editable, showTime, decimalPlaces, sortable, visible, lockVisible, width, onCellClick, } = {}) {
6683
+ constructor({ name, field, filterType, filterId, showMatchModes, filterField, dropdownOrMultiselectValues, actions, editable, showTime, decimalPlaces, sortable, visible, lockVisible, width, onCellClick, } = {}) {
5358
6684
  this.name = name;
5359
6685
  this.field = field;
5360
- this.filterField = filterField;
5361
6686
  this.filterType = filterType;
5362
- this.filterPlaceholder = filterPlaceholder;
6687
+ this.filterId = filterId;
5363
6688
  this.showMatchModes = showMatchModes;
5364
- this.matchModes = matchModes;
5365
- this.showAddButton = showAddButton;
6689
+ this.filterField = filterField;
5366
6690
  this.dropdownOrMultiselectValues = dropdownOrMultiselectValues;
5367
6691
  this.actions = actions;
5368
6692
  this.editable = editable;
@@ -5389,6 +6713,461 @@ class AllClickEvent {
5389
6713
  }
5390
6714
  }
5391
6715
 
6716
+ var MatchModeCodes;
6717
+ (function (MatchModeCodes) {
6718
+ MatchModeCodes["StartsWith"] = "startsWith";
6719
+ MatchModeCodes["Contains"] = "contains";
6720
+ MatchModeCodes["Equals"] = "equals";
6721
+ MatchModeCodes["LessThan"] = "lessThan";
6722
+ MatchModeCodes["GreaterThan"] = "greaterThan";
6723
+ MatchModeCodes["In"] = "in";
6724
+ })(MatchModeCodes || (MatchModeCodes = {}));
6725
+
6726
+ /**
6727
+ * What each value type may be asked — the ONE operator table, mirroring what the generated
6728
+ * paginator implements per type (`PaginatedResultGenerator`): `In` on a string column answers
6729
+ * with InvalidMatchMode, PrimeNG's own text list adds three modes the backend 400s, and so on.
6730
+ *
6731
+ * Deliberately a LEAF module (imports nothing but the codes) so every consumer can derive from
6732
+ * it without a cycle: the store's runtime validation and offered lists (`filter-store.ts`), the
6733
+ * compile-time `FilterRule.matchMode` union (`entities/filter-rule.ts`), and the data view's
6734
+ * kind-level lists (via `operatorOptionsForKind`). These used to be hand-kept copies; edit the
6735
+ * table here and every surface follows.
6736
+ *
6737
+ * Tuple order is DISPLAY order for every operator picker (Equals, then "less/before", then
6738
+ * "more/after" — the order the header dropdowns always shipped).
6739
+ */
6740
+ const ALLOWED_OPERATORS = {
6741
+ text: [
6742
+ MatchModeCodes.StartsWith,
6743
+ MatchModeCodes.Contains,
6744
+ MatchModeCodes.Equals,
6745
+ ],
6746
+ number: [
6747
+ MatchModeCodes.Equals,
6748
+ MatchModeCodes.LessThan,
6749
+ MatchModeCodes.GreaterThan,
6750
+ MatchModeCodes.In,
6751
+ ],
6752
+ boolean: [MatchModeCodes.Equals],
6753
+ date: [
6754
+ MatchModeCodes.Equals,
6755
+ MatchModeCodes.LessThan,
6756
+ MatchModeCodes.GreaterThan,
6757
+ ],
6758
+ };
6759
+
6760
+ const OPERATOR_WORDS = {
6761
+ text: {
6762
+ [MatchModeCodes.StartsWith]: {
6763
+ pickerKey: 'StartsWith',
6764
+ phraseKey: 'FilterChipStartsWith',
6765
+ },
6766
+ [MatchModeCodes.Contains]: {
6767
+ pickerKey: 'Contains',
6768
+ phraseKey: 'FilterChipContains',
6769
+ },
6770
+ [MatchModeCodes.Equals]: {
6771
+ pickerKey: 'Equals',
6772
+ phraseKey: 'FilterChipEquals',
6773
+ },
6774
+ // No `In` for text: ALLOWED_OPERATORS.text does not carry it, so the entry was unreachable —
6775
+ // and it made `textFilter({ options })` look supported when the store throws on it.
6776
+ },
6777
+ number: {
6778
+ [MatchModeCodes.Equals]: {
6779
+ pickerKey: 'Equals',
6780
+ phraseKey: 'FilterChipEquals',
6781
+ },
6782
+ [MatchModeCodes.GreaterThan]: {
6783
+ pickerKey: 'MoreThan',
6784
+ phraseKey: 'FilterChipGreaterThan',
6785
+ },
6786
+ [MatchModeCodes.LessThan]: {
6787
+ pickerKey: 'LessThan',
6788
+ phraseKey: 'FilterChipLessThan',
6789
+ },
6790
+ // The picker never renders for a pick-list (one operator), but the CHIP always does.
6791
+ [MatchModeCodes.In]: { pickerKey: 'In', phraseKey: 'FilterChipIn' },
6792
+ },
6793
+ boolean: {
6794
+ [MatchModeCodes.Equals]: {
6795
+ pickerKey: 'Equals',
6796
+ phraseKey: 'FilterChipEquals',
6797
+ },
6798
+ },
6799
+ date: {
6800
+ [MatchModeCodes.Equals]: {
6801
+ pickerKey: 'OnDate',
6802
+ phraseKey: 'FilterChipEquals',
6803
+ },
6804
+ [MatchModeCodes.LessThan]: {
6805
+ pickerKey: 'DatesBefore',
6806
+ phraseKey: 'FilterChipBefore',
6807
+ },
6808
+ [MatchModeCodes.GreaterThan]: {
6809
+ pickerKey: 'DatesAfter',
6810
+ phraseKey: 'FilterChipAfter',
6811
+ },
6812
+ },
6813
+ };
6814
+ /**
6815
+ * `In` and options are the same fact seen from two sides: `In` needs a list of values, and the
6816
+ * editor only draws one when the filter declares options. So options mean `In` and nothing else,
6817
+ * and their absence rules `In` out — offering it on a plain number filter would hand the
6818
+ * operator a mode with no control behind it.
6819
+ *
6820
+ * Widened first: the per-kind tuples are literal, so `boolean`'s `[Equals]` makes the `In`
6821
+ * comparison a "no overlap" error rather than an empty result.
6822
+ */
6823
+ function baseOperators(kind, hasOptions) {
6824
+ const accepted = ALLOWED_OPERATORS[kind];
6825
+ return accepted.filter((value) => hasOptions ? value === MatchModeCodes.In : value !== MatchModeCodes.In);
6826
+ }
6827
+ function toOperatorOptions(kind, operators) {
6828
+ return operators.map((value) => ({
6829
+ value,
6830
+ labelKey: OPERATOR_WORDS[kind][value]?.pickerKey ?? value,
6831
+ }));
6832
+ }
6833
+ /**
6834
+ * A KIND's operator picker options, for callers with no filter behind them — today that is
6835
+ * `spiderly-data-view`, whose own filter config derives its operator lists here instead of
6836
+ * hand-keeping copies (the drift `allowed-operators.ts` exists to end). Per-FILTER narrowing
6837
+ * lives in the store (`createFilterStore` resolves it once per declaration), so this cannot be
6838
+ * handed a fake definition to satisfy — that shape shipped once and interpolated an empty
6839
+ * filter name into its own error message.
6840
+ */
6841
+ function operatorOptionsForKind(kind, hasOptions = false) {
6842
+ return toOperatorOptions(kind, baseOperators(kind, hasOptions));
6843
+ }
6844
+ /**
6845
+ * The operator applied when nobody picked one, which has to be what a person MEANS by filling in
6846
+ * the control. `Contains` for a fragment typed into a box; `GreaterThan` for a date, never
6847
+ * `Equals` — a date equality against a TIMESTAMP matches only the row written in that exact
6848
+ * second, and answers with an empty grid rather than an error (PACMS ran into this on all three
6849
+ * of its date columns, Filip 2026-08-28).
6850
+ */
6851
+ const DEFAULT_OPERATOR = {
6852
+ text: MatchModeCodes.Contains,
6853
+ number: MatchModeCodes.Equals,
6854
+ boolean: MatchModeCodes.Equals,
6855
+ date: MatchModeCodes.GreaterThan,
6856
+ };
6857
+ function textFilter(config) {
6858
+ return {
6859
+ kind: 'text',
6860
+ label: config.label,
6861
+ operators: config.operators,
6862
+ offered: config.offered,
6863
+ };
6864
+ }
6865
+ /**
6866
+ * Only a NUMBER filter accepts options, and the asymmetry is forced rather than chosen: options
6867
+ * mean `In`, and `In` is the one operator ALLOWED_OPERATORS grants to numbers alone — because the
6868
+ * generated paginator answers it with InvalidMatchMode on a string column. A text pick-list used
6869
+ * to type-check and then throw at the first `set`.
6870
+ */
6871
+ function numberFilter(config) {
6872
+ return {
6873
+ kind: 'number',
6874
+ label: config.label,
6875
+ options: config.options,
6876
+ operators: config.operators,
6877
+ offered: config.offered,
6878
+ };
6879
+ }
6880
+ function booleanFilter(config) {
6881
+ return {
6882
+ kind: 'boolean',
6883
+ label: config.label,
6884
+ operators: config.operators,
6885
+ offered: config.offered,
6886
+ };
6887
+ }
6888
+ function dateFilter(config) {
6889
+ return {
6890
+ kind: 'date',
6891
+ label: config.label,
6892
+ operators: config.operators,
6893
+ offered: config.offered,
6894
+ };
6895
+ }
6896
+ /** The shape `Date.toJSON` writes, which is the only value here JSON cannot round-trip itself. */
6897
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
6898
+ function reviveValue(value) {
6899
+ if (typeof value === 'string' && ISO_DATE.test(value))
6900
+ return new Date(value);
6901
+ if (Array.isArray(value))
6902
+ return value.map(reviveValue);
6903
+ return value;
6904
+ }
6905
+ /**
6906
+ * A blanked value is NOT a constraint. `contains ''` matches every row, so emitting one would
6907
+ * narrow nothing while the bar claims the grid is filtered.
6908
+ */
6909
+ function isBlank(value) {
6910
+ if (value === null || value === undefined)
6911
+ return true;
6912
+ if (typeof value === 'string')
6913
+ return value.trim() === '';
6914
+ if (Array.isArray(value))
6915
+ return value.length === 0;
6916
+ // A half-typed datepicker yields an Invalid Date, which JSON.stringify writes as `null`.
6917
+ if (value instanceof Date)
6918
+ return Number.isNaN(value.getTime());
6919
+ return false;
6920
+ }
6921
+ /**
6922
+ * Whether two constraint values are the same QUESTION. Identity is not it: every `set` builds a
6923
+ * fresh draft, a multiselect hands back a fresh array per change, and two Dates at one instant
6924
+ * are one constraint. This is what `commit` bails on, so a paste-over-itself or a double Apply
6925
+ * cannot spend a request — the rule PACMS's order list used to hand-roll as `lastCommittedTerm`.
6926
+ */
6927
+ function valueEquals(a, b) {
6928
+ if (a === b)
6929
+ return true;
6930
+ if (a instanceof Date && b instanceof Date)
6931
+ return a.getTime() === b.getTime();
6932
+ if (Array.isArray(a) && Array.isArray(b)) {
6933
+ return a.length === b.length && a.every((item, i) => valueEquals(item, b[i]));
6934
+ }
6935
+ return false;
6936
+ }
6937
+ function constraintEquals(a, b) {
6938
+ return a != null && a.operator === b.operator && valueEquals(a.value, b.value);
6939
+ }
6940
+ /**
6941
+ * The filter engine. It knows nothing about columns: a filter has an id, a value type and a
6942
+ * constraint, and that is the entire vocabulary.
6943
+ */
6944
+ function createFilterStore(definitions) {
6945
+ // Per-filter offered operators, resolved ONCE per declaration — so an invalid narrowing is
6946
+ // said once, not on every editor open — and re-resolved only where the inputs can actually
6947
+ // change: `setOptions`, because options' PRESENCE flips a filter between pick-list (`In`) and
6948
+ // plain operators. A per-store Map rather than the module-level WeakMap this first shipped
6949
+ // as: that memo was keyed on a definition object consumers mutate, so late-filled options
6950
+ // could freeze a filter's operators at the wrong answer.
6951
+ const resolveOffered = (id) => {
6952
+ const definition = definitions[id];
6953
+ const base = baseOperators(definition.kind, definition.options != null);
6954
+ let codes = base;
6955
+ if (definition.operators?.length) {
6956
+ const survivors = definition.operators.filter((operator) => base.includes(operator));
6957
+ // Same polarity as the column path's computeMatchModes: a bad entry is dropped and said
6958
+ // out loud, and a narrowing that leaves nothing falls back to the full list — an editor
6959
+ // with no operators would be unusable, which is worse than an unwanted one. Mostly
6960
+ // unreachable now that FilterConfig types `operators` per kind; kept for dynamically
6961
+ // built arrays.
6962
+ if (survivors.length < definition.operators.length) {
6963
+ console.error(`Filter '${definition.label}' narrows to operators its ${definition.kind} kind does not ` +
6964
+ `offer (${definition.operators.join(', ')}; offered: ${base.join(', ')}). ` +
6965
+ `Unsupported entries are ignored${survivors.length ? '' : '; falling back to the full list'}.`);
6966
+ }
6967
+ codes = survivors.length ? survivors : base;
6968
+ }
6969
+ return { codes, options: toOperatorOptions(definition.kind, codes) };
6970
+ };
6971
+ const offered = new Map();
6972
+ for (const id of Object.keys(definitions)) {
6973
+ offered.set(id, resolveOffered(id));
6974
+ }
6975
+ /**
6976
+ * The operator applied when nobody picked one. Options mean `In`, whatever the kind's default;
6977
+ * a narrowed filter defaults to its FIRST declared operator.
6978
+ */
6979
+ const defaultOperatorFor = (id) => {
6980
+ const definition = definitions[id];
6981
+ if (definition.options != null)
6982
+ return MatchModeCodes.In;
6983
+ return definition.operators?.length
6984
+ ? offered.get(id).codes[0]
6985
+ : DEFAULT_OPERATOR[definition.kind];
6986
+ };
6987
+ // Two stores, and the split is the whole point: `set` writes a DRAFT, `commit` publishes it.
6988
+ // A chip drawn off a draft would repeat the mistake the header's filter icon already shipped —
6989
+ // claiming the grid is narrowed on the first keystroke. Controls that apply on change
6990
+ // (multiselect, boolean, date) call both at once; a text box commits on Enter or blur.
6991
+ const drafts = signal(new Map());
6992
+ const writeDraft = (id, constraint) => {
6993
+ const next = new Map(drafts());
6994
+ if (constraint === null)
6995
+ next.delete(id);
6996
+ else
6997
+ next.set(id, constraint);
6998
+ drafts.set(next);
6999
+ };
7000
+ const committed = signal(new Map());
7001
+ const applied = computed(() => [...committed()].map(([id, constraint]) => ({
7002
+ id: id,
7003
+ label: definitions[id].label,
7004
+ kind: definitions[id].kind,
7005
+ operator: constraint.operator,
7006
+ operatorPhraseKey: OPERATOR_WORDS[definitions[id].kind][constraint.operator]?.phraseKey ??
7007
+ constraint.operator,
7008
+ value: constraint.value,
7009
+ })));
7010
+ const store = {
7011
+ definitions,
7012
+ applied,
7013
+ /**
7014
+ * A handle bound to one filter. This is the PLACEMENT api: it needs nothing from the component
7015
+ * tree, so the same filter can be driven from the bar, from a header cell, or from a drawer
7016
+ * rendered at app root. `value` is the DRAFT — a control shows what was typed, not what was
7017
+ * applied; `applied()` is the other question and the bar asks that one.
7018
+ */
7019
+ get(id) {
7020
+ return {
7021
+ id: id,
7022
+ label: definitions[id].label,
7023
+ kind: definitions[id].kind,
7024
+ value: computed(() => drafts().get(id)?.value),
7025
+ operator: computed(() => drafts().get(id)?.operator),
7026
+ // Getters, not snapshots: options can land AFTER a handle is taken — the bar's editor
7027
+ // holds one for the life of the popover, and PACMS fills its lookups asynchronously —
7028
+ // and a snapshot left an already-open pick-list empty until it was reopened. The
7029
+ // operator list rides along, since setOptions can flip it to `In`.
7030
+ get operators() {
7031
+ return offered.get(id).options;
7032
+ },
7033
+ get defaultOperator() {
7034
+ return defaultOperatorFor(id);
7035
+ },
7036
+ get options() {
7037
+ return definitions[id].options;
7038
+ },
7039
+ set: (constraint) => store.set(id, constraint),
7040
+ commit: () => store.commit(id),
7041
+ reset: () => store.reset(id),
7042
+ };
7043
+ },
7044
+ /**
7045
+ * The ONE seam for choices that arrive after the store is built (PACMS fills its pick-lists
7046
+ * from lookups that race a deploy). Never write `definitions[id].options` by hand: options'
7047
+ * PRESENCE is what makes a filter a pick-list, so the setter also re-resolves the offered
7048
+ * operators — a hand write would leave a late-filled filter offering `Equals` on a control
7049
+ * that emits a list.
7050
+ */
7051
+ setOptions(id, options) {
7052
+ definitions[id].options = options;
7053
+ offered.set(id, resolveOffered(id));
7054
+ },
7055
+ set(id, constraint) {
7056
+ const kind = definitions[id].kind;
7057
+ // Throws rather than logging and ignoring, which is what `computeMatchModes` does for a
7058
+ // column's OFFERED match modes. Different stakes: that one narrows a dropdown, so a bad
7059
+ // entry merely fails to show, while this one assembles a QUERY — dropping it silently would
7060
+ // leave the operator looking at unfiltered rows believing they are filtered.
7061
+ if (!ALLOWED_OPERATORS[kind].includes(constraint.operator)) {
7062
+ throw new Error(`Filter '${String(id)}' is ${kind}, which does not accept the '${constraint.operator}' ` +
7063
+ `operator (allowed: ${ALLOWED_OPERATORS[kind].join(', ')}). The generated paginator ` +
7064
+ `answers an unsupported match mode with InvalidMatchMode.`);
7065
+ }
7066
+ writeDraft(id, constraint);
7067
+ },
7068
+ /**
7069
+ * `set` + `commit` in one breath — the shape every PROGRAMMATIC write takes (a view's
7070
+ * `apply`, the sheet flow). The split API stays for controls, where draft-on-type /
7071
+ * publish-on-Apply is the point; but a bare `set` whose `commit` is forgotten (or written
7072
+ * with the wrong id — the pair repeats the string twice) fails silently: the draft is
7073
+ * written, nothing is published, and a view narrows by nothing under a labelled tab.
7074
+ */
7075
+ setAndCommit(id, constraint) {
7076
+ store.set(id, constraint);
7077
+ store.commit(id);
7078
+ },
7079
+ /**
7080
+ * Publishes the draft. A blanked draft REMOVES the constraint rather than committing an empty
7081
+ * one, so `applied()` and the payload agree: no chip, no key, no `contains ''`.
7082
+ */
7083
+ commit(id) {
7084
+ const draft = drafts().get(id);
7085
+ const current = committed();
7086
+ const shouldRemove = draft === undefined || isBlank(draft.value);
7087
+ // A new Map is a new identity even with identical contents, and everything downstream
7088
+ // reacts to identity — so a no-op commit would spend a request. The bail compares the
7089
+ // CONSTRAINT, not the draft object: every set() builds a fresh draft, and the case that
7090
+ // matters is an identical value re-committed (see valueEquals).
7091
+ if (shouldRemove ? !current.has(id) : constraintEquals(current.get(id), draft))
7092
+ return;
7093
+ const next = new Map(current);
7094
+ if (shouldRemove)
7095
+ next.delete(id);
7096
+ else
7097
+ next.set(id, draft);
7098
+ committed.set(next);
7099
+ },
7100
+ /**
7101
+ * The chip's `x`. Drops the DRAFT as well as the committed constraint: a surviving draft
7102
+ * leaves the control still showing the cleared text, and the next commit would restore a
7103
+ * filter nobody re-typed.
7104
+ */
7105
+ reset(id) {
7106
+ if (drafts().has(id))
7107
+ writeDraft(id, null);
7108
+ if (!committed().has(id))
7109
+ return;
7110
+ const next = new Map(committed());
7111
+ next.delete(id);
7112
+ committed.set(next);
7113
+ },
7114
+ /**
7115
+ * The applied set, plain enough to store. Drafts are deliberately NOT included: a half-typed
7116
+ * value is not a state anyone should come back to, and the bar's whole claim is that what it
7117
+ * shows is what narrows the grid.
7118
+ */
7119
+ snapshot() {
7120
+ const snapshot = {};
7121
+ for (const [id, constraint] of committed()) {
7122
+ snapshot[id] = {
7123
+ operator: constraint.operator,
7124
+ value: constraint.value,
7125
+ };
7126
+ }
7127
+ return snapshot;
7128
+ },
7129
+ /**
7130
+ * Replaces the applied set. The drafts are written too, so reopening a restored chip shows
7131
+ * the value it is filtering by rather than an empty control.
7132
+ *
7133
+ * Ids the store no longer declares are dropped rather than carried: a filter removed from the
7134
+ * declaration in a later release would otherwise ride every request from anyone whose storage
7135
+ * still held it, with no chip able to name it and no way to clear it from the UI.
7136
+ */
7137
+ restore(snapshot) {
7138
+ const next = new Map();
7139
+ for (const [id, constraint] of Object.entries(snapshot ?? {})) {
7140
+ if (!(id in definitions))
7141
+ continue;
7142
+ next.set(id, {
7143
+ operator: constraint.operator,
7144
+ value: reviveValue(constraint.value),
7145
+ });
7146
+ }
7147
+ drafts.set(new Map(next));
7148
+ committed.set(next);
7149
+ },
7150
+ /** The bar's "Clear all". `reset` for every filter, drafts included. */
7151
+ clear() {
7152
+ if (drafts().size > 0)
7153
+ drafts.set(new Map());
7154
+ if (committed().size > 0)
7155
+ committed.set(new Map());
7156
+ },
7157
+ /** The `filters` half of `Filter` — what the generated paginator reads. */
7158
+ toFilterPayload() {
7159
+ const payload = {};
7160
+ for (const [id, constraint] of committed()) {
7161
+ payload[id] = [
7162
+ { matchMode: constraint.operator, value: constraint.value },
7163
+ ];
7164
+ }
7165
+ return payload;
7166
+ },
7167
+ };
7168
+ return store;
7169
+ }
7170
+
5392
7171
  class SpiderlyDataViewComponent {
5393
7172
  constructor(translocoService, locale) {
5394
7173
  this.translocoService = translocoService;
@@ -5415,34 +7194,15 @@ class SpiderlyDataViewComponent {
5415
7194
  };
5416
7195
  }
5417
7196
  ngOnInit() {
5418
- this.matchModeDateOptions = [
5419
- {
5420
- label: this.translocoService.translate('OnDate'),
5421
- value: MatchModeCodes.Equals,
5422
- },
5423
- {
5424
- label: this.translocoService.translate('DatesBefore'),
5425
- value: MatchModeCodes.LessThan,
5426
- },
5427
- {
5428
- label: this.translocoService.translate('DatesAfter'),
5429
- value: MatchModeCodes.GreaterThan,
5430
- },
5431
- ];
5432
- this.matchModeNumberOptions = [
5433
- {
5434
- label: this.translocoService.translate('Equals'),
5435
- value: MatchModeCodes.Equals,
5436
- },
5437
- {
5438
- label: this.translocoService.translate('LessThan'),
5439
- value: MatchModeCodes.LessThan,
5440
- },
5441
- {
5442
- label: this.translocoService.translate('MoreThan'),
5443
- value: MatchModeCodes.GreaterThan,
5444
- },
5445
- ];
7197
+ // Derived from the ONE operator table (`filters/allowed-operators.ts`) rather than
7198
+ // hand-kept: these were the last hand-written copies of the per-kind lists, and a backend
7199
+ // operator change edited in the shared table would silently never have reached them.
7200
+ const matchModeOptionsFor = (kind) => operatorOptionsForKind(kind).map((option) => ({
7201
+ label: this.translocoService.translate(option.labelKey),
7202
+ value: option.value,
7203
+ }));
7204
+ this.matchModeDateOptions = matchModeOptionsFor('date');
7205
+ this.matchModeNumberOptions = matchModeOptionsFor('number');
5446
7206
  }
5447
7207
  // Paging semantics and the scroll-margin contract are documented once, on the data table:
5448
7208
  // spiderly-data-table/CLAUDE.md → "Scroll back to the table on page change".
@@ -5542,7 +7302,7 @@ class SpiderlyDataViewComponent {
5542
7302
  this.table.clear();
5543
7303
  }
5544
7304
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyDataViewComponent, deps: [{ token: i1.TranslocoService }, { token: LOCALE_ID }], target: i0.ɵɵFactoryTarget.Component }); }
5545
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.13", type: SpiderlyDataViewComponent, isStandalone: true, selector: "spiderly-data-view", inputs: { items: "items", rows: "rows", filters: "filters", showCardWrapper: "showCardWrapper", showPaginatorRightData: "showPaginatorRightData", showTotalRecordsNumber: "showTotalRecordsNumber", applyFiltersIcon: "applyFiltersIcon", clearFiltersIcon: "clearFiltersIcon", getPaginatedListObservableMethod: "getPaginatedListObservableMethod" }, outputs: { onLazyLoad: "onLazyLoad" }, queries: [{ propertyName: "cardBody", first: true, predicate: ["cardBody"], descendants: true, read: TemplateRef }], viewQueries: [{ propertyName: "table", first: true, predicate: ["dt"], descendants: 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 [rowHover]=\"false\"\n responsiveLayout=\"scroll\"\n [lazy]=\"true\"\n (onLazyLoad)=\"lazyLoad($event)\"\n (onPage)=\"onPageChange(tableContainer)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"isPending\"\n dataKey=\"id\"\n (onFilter)=\"filter($event)\"\n [filterDelay]=\"500\"\n [paginator]=\"true\"\n >\n <ng-template #caption>\n <div *ngIf=\"filters.length !== 0\">\n <div class=\"data-view-grid\">\n @for (filter of filters; track $index) {\n <div>\n <div\n style=\"\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n position: relative;\n \"\n >\n <div>\n <label>\n {{ filter.label }}\n </label>\n </div>\n\n <p-columnFilter\n *ngIf=\"filter.type != null\"\n [type]=\"filter.type\"\n [field]=\"filter.filterField ?? filter.field\"\n display=\"row\"\n [placeholder]=\"filter.placeholder\"\n [showOperator]=\"false\"\n [showMenu]=\"filter.showMatchModes\"\n [showMatchModes]=\"true\"\n [showClearButton]=\"false\"\n [matchMode]=\"getDefaultMatchMode(filter.type)\"\n [matchModeOptions]=\"getMatchModeOptions(filter.type)\"\n >\n <ng-container *ngIf=\"filter.type == 'multiselect'\">\n <ng-template\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-multiSelect\n [ngModel]=\"value\"\n [options]=\"filter.dropdownOrMultiselectValues\"\n [placeholder]=\"t('All')\"\n (onChange)=\"filter($event.value)\"\n optionLabel=\"label\"\n optionValue=\"code\"\n [style]=\"{ width: '100%' }\"\n >\n <ng-template let-item pTemplate=\"item\">\n <div class=\"p-multiselect-representative-option\">\n <span class=\"ml-2\">{{ item.label }}</span>\n </div>\n </ng-template>\n </p-multiSelect>\n </ng-template>\n </ng-container>\n </p-columnFilter>\n </div>\n </div>\n }\n </div>\n <div style=\"display: flex; gap: 12px\">\n <spiderly-button\n [label]=\"t('ApplyFilters')\"\n (onClick)=\"applyFilters()\"\n [icon]=\"applyFiltersIcon\"\n ></spiderly-button>\n <spiderly-button\n [label]=\"t('ClearFilters')\"\n (onClick)=\"clearFilters()\"\n [icon]=\"clearFiltersIcon\"\n ></spiderly-button>\n </div>\n </div>\n </ng-template>\n <ng-template #body let-rowData let-index=\"rowIndex\">\n <tr style=\"padding: 0; margin-bottom: 0\">\n <td style=\"display: block; border: 0; padding: 0\">\n <ng-container\n *ngTemplateOutlet=\"\n cardBody;\n context: { $implicit: rowData, item: rowData, index: index }\n \"\n >\n </ng-container>\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n {{ t(\"NoRecordsFound\") }}\n </ng-template>\n <ng-template *ngIf=\"showTotalRecordsNumber\" pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template *ngIf=\"showPaginatorRightData\" pTemplate=\"paginatorright\">\n <ng-content select=\"[PAGINATORRIGHT]\"></ng-content>\n </ng-template>\n </p-table>\n </div>\n</ng-container>\n", styles: [":host .spiderly-table-container{scroll-margin-top:calc(var(--spiderly-viewport-top-inset, 0px) + 1rem)}: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}::ng-deep .p-datatable-tbody{display:flex;flex-direction:column;gap:18px}::ng-deep .p-datatable-header{margin-bottom:36px;border:0!important;padding:0!important;background-color:transparent!important}::ng-deep .p-paginator{padding:18px 9px 0!important}::ng-deep .p-fluid{width:100%!important}::ng-deep .p-inputtext{width:100%!important}::ng-deep .p-inputnumber{width:100%!important}::ng-deep .p-datepicker{width:100%!important}.data-view-grid{display:grid;gap:18px;grid-template-columns:1fr 1fr 1fr;margin-bottom:18px}@media (max-width: 1080px){.data-view-grid{gap:12px;grid-template-columns:1fr 1fr}}@media (max-width: 500px){.data-view-grid{grid-template-columns:1fr}}\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.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: "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: "component", type: i10.ColumnFilter, selector: "p-columnFilter", inputs: ["field", "type", "display", "showMenu", "matchMode", "operator", "showOperator", "showClearButton", "showApplyButton", "showMatchModes", "showAddButton", "hideOnClear", "placeholder", "matchModeOptions", "maxConstraints", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "locale", "localeMatcher", "currency", "currencyDisplay", "useGrouping", "showButtons", "ariaLabel", "filterButtonProps"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i4$5.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: CheckboxModule }, { kind: "ngmodule", type: TooltipModule }, { kind: "ngmodule", type: DatePickerModule }, { kind: "ngmodule", type: InputTextModule }, { kind: "ngmodule", type: InputNumberModule }, { kind: "ngmodule", type: SelectModule }] }); }
7305
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.13", type: SpiderlyDataViewComponent, isStandalone: true, selector: "spiderly-data-view", inputs: { items: "items", rows: "rows", filters: "filters", showCardWrapper: "showCardWrapper", showPaginatorRightData: "showPaginatorRightData", showTotalRecordsNumber: "showTotalRecordsNumber", applyFiltersIcon: "applyFiltersIcon", clearFiltersIcon: "clearFiltersIcon", getPaginatedListObservableMethod: "getPaginatedListObservableMethod" }, outputs: { onLazyLoad: "onLazyLoad" }, queries: [{ propertyName: "cardBody", first: true, predicate: ["cardBody"], descendants: true, read: TemplateRef }], viewQueries: [{ propertyName: "table", first: true, predicate: ["dt"], descendants: 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 [rowHover]=\"false\"\n responsiveLayout=\"scroll\"\n [lazy]=\"true\"\n (onLazyLoad)=\"lazyLoad($event)\"\n (onPage)=\"onPageChange(tableContainer)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"isPending\"\n dataKey=\"id\"\n (onFilter)=\"filter($event)\"\n [filterDelay]=\"500\"\n [paginator]=\"true\"\n >\n <ng-template #caption>\n <div *ngIf=\"filters.length !== 0\">\n <div class=\"data-view-grid\">\n @for (filter of filters; track $index) {\n <div>\n <div\n style=\"\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n position: relative;\n \"\n >\n <div>\n <label>\n {{ filter.label }}\n </label>\n </div>\n\n <p-columnFilter\n *ngIf=\"filter.type != null\"\n [type]=\"filter.type\"\n [field]=\"filter.filterField ?? filter.field\"\n display=\"row\"\n [placeholder]=\"filter.placeholder\"\n [showOperator]=\"false\"\n [showMenu]=\"filter.showMatchModes\"\n [showMatchModes]=\"true\"\n [showClearButton]=\"false\"\n [matchMode]=\"getDefaultMatchMode(filter.type)\"\n [matchModeOptions]=\"getMatchModeOptions(filter.type)\"\n >\n <ng-container *ngIf=\"filter.type == 'multiselect'\">\n <ng-template\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-multiSelect\n [ngModel]=\"value\"\n [options]=\"filter.dropdownOrMultiselectValues\"\n [placeholder]=\"t('All')\"\n (onChange)=\"filter($event.value)\"\n optionLabel=\"label\"\n optionValue=\"code\"\n [style]=\"{ width: '100%' }\"\n >\n <ng-template let-item pTemplate=\"item\">\n <div class=\"p-multiselect-representative-option\">\n <span class=\"ml-2\">{{ item.label }}</span>\n </div>\n </ng-template>\n </p-multiSelect>\n </ng-template>\n </ng-container>\n </p-columnFilter>\n </div>\n </div>\n }\n </div>\n <div style=\"display: flex; gap: 12px\">\n <spiderly-button\n [label]=\"t('ApplyFilters')\"\n (onClick)=\"applyFilters()\"\n [icon]=\"applyFiltersIcon\"\n ></spiderly-button>\n <spiderly-button\n [label]=\"t('ClearFilters')\"\n (onClick)=\"clearFilters()\"\n [icon]=\"clearFiltersIcon\"\n ></spiderly-button>\n </div>\n </div>\n </ng-template>\n <ng-template #body let-rowData let-index=\"rowIndex\">\n <tr style=\"padding: 0; margin-bottom: 0\">\n <td style=\"display: block; border: 0; padding: 0\">\n <ng-container\n *ngTemplateOutlet=\"\n cardBody;\n context: { $implicit: rowData, item: rowData, index: index }\n \"\n >\n </ng-container>\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n {{ t(\"NoRecordsFound\") }}\n </ng-template>\n <ng-template *ngIf=\"showTotalRecordsNumber\" pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template *ngIf=\"showPaginatorRightData\" pTemplate=\"paginatorright\">\n <ng-content select=\"[PAGINATORRIGHT]\"></ng-content>\n </ng-template>\n </p-table>\n </div>\n</ng-container>\n", styles: [":host .spiderly-table-container{scroll-margin-top:calc(var(--spiderly-viewport-top-inset, 0px) + 1rem)}: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}::ng-deep .p-datatable-tbody{display:flex;flex-direction:column;gap:18px}::ng-deep .p-datatable-header{margin-bottom:36px;border:0!important;padding:0!important;background-color:transparent!important}::ng-deep .p-paginator{padding:18px 9px 0!important}::ng-deep .p-fluid{width:100%!important}::ng-deep .p-inputtext{width:100%!important}::ng-deep .p-inputnumber{width:100%!important}::ng-deep .p-datepicker{width:100%!important}.data-view-grid{display:grid;gap:18px;grid-template-columns:1fr 1fr 1fr;margin-bottom:18px}@media (max-width: 1080px){.data-view-grid{gap:12px;grid-template-columns:1fr 1fr}}@media (max-width: 500px){.data-view-grid{grid-template-columns:1fr}}\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.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: "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: "component", type: i10.ColumnFilter, selector: "p-columnFilter", inputs: ["field", "type", "display", "showMenu", "matchMode", "operator", "showOperator", "showClearButton", "showApplyButton", "showMatchModes", "showAddButton", "hideOnClear", "placeholder", "matchModeOptions", "maxConstraints", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "locale", "localeMatcher", "currency", "currencyDisplay", "useGrouping", "showButtons", "ariaLabel", "filterButtonProps"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: ButtonModule }, { 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: CheckboxModule }, { kind: "ngmodule", type: TooltipModule }, { kind: "ngmodule", type: DatePickerModule }, { kind: "ngmodule", type: InputTextModule }, { kind: "ngmodule", type: InputNumberModule }, { kind: "ngmodule", type: SelectModule }] }); }
5546
7306
  }
5547
7307
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyDataViewComponent, decorators: [{
5548
7308
  type: Component,
@@ -5679,12 +7439,9 @@ class Filter extends BaseEntity {
5679
7439
  * The `FilterRule` class is a generic structure that defines a single filtering condition,
5680
7440
  * including the match mode (comparison operator), the value to compare, and an optional logical operator.
5681
7441
  *
5682
- * The allowed match modes are determined by the type parameter `T`:
5683
- * - For `string`: supports `Contains`, `StartsWith`, and `Equals`.
5684
- * - For `boolean`: supports `Equals`.
5685
- * - For `Date`: supports `Equals`, `GreaterThan`, and `LessThan`.
5686
- * - For `number`: supports `Equals`, `GreaterThan`, `LessThan`, and `In`.
5687
- * - For other types: allows any value from `MatchModeCodes`.
7442
+ * The allowed match modes are determined by the type parameter `T`, each union derived from the
7443
+ * one operator table in `filters/allowed-operators.ts` (for other types, any `MatchModeCodes`
7444
+ * value is allowed).
5688
7445
  *
5689
7446
  * @template T The type of the value to filter by.
5690
7447
  */
@@ -6086,5 +7843,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
6086
7843
  * Generated bundle index. Do not edit.
6087
7844
  */
6088
7845
 
6089
- export { Action, AllClickEvent, ApiErrorCodes, ApiSecurityService, AppSidebarComponent, AuthCardComponent, AuthGuard, AuthResult, AuthResultWithCookies, AuthServiceBase, BaseAutocompleteControl, BaseControl, BaseDropdownControl, BaseEntity, BaseFormComponent, BaseFormService, CardSkeletonComponent, Codebook, Column, ConfigServiceBase, DEFAULT_EXTERNAL_PROVIDER_ICONS, 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, SpiderlyFormArray, SpiderlyFormControl, SpiderlyFormGroup, SpiderlyLayoutComponent, SpiderlyLoginComponent, SpiderlyMarkdownComponent, SpiderlyMessageService, SpiderlyMultiAutocompleteComponent, SpiderlyMultiSelectComponent, SpiderlyNumberComponent, SpiderlyPanelComponent, SpiderlyPanelsModule, SpiderlyPasswordComponent, SpiderlyReturnButtonComponent, SpiderlySplitButtonComponent, SpiderlyTab, SpiderlyTemplateTypeDirective, SpiderlyTextareaComponent, SpiderlyTextboxComponent, SpiderlyTranslocoLoader, TopBarComponent, UserBase, UserRole, ValidatorAbstractService, VerificationTokenRequest, VerificationTypeCodes, VerificationWrapperComponent, adjustColor, authInitializer, capitalizeFirstChar, deleteAction, exportListToExcel, firstCharToUpper, getFileNameFromContentDisposition, getHtmlImgDisplayString64, getImageDimensions, getMimeTypeForFileName, getMonth, getParentUrl, getPrimengAutocompleteCodebookOptions, getPrimengAutocompleteNamebookOptions, getPrimengDropdownCodebookOptions, getPrimengDropdownNamebookOptions, getPrimengNamebookOptions, httpLoadingInterceptor, isExcelFileType, isFileImageType, isNullOrEmpty, jsonHttpInterceptor, jwtInterceptor, kebabToTitleCase, nameOf, nameof, parseDateOnlyLocal, pushAction, saveResponseAsFile, scrollElementIntoViewIfAboveViewport, selectedTab, singleOrDefault, splitPascalCase, toCommaSeparatedString, unauthorizedInterceptor, validatePrecisionScale };
7846
+ 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 };
6090
7847
  //# sourceMappingURL=spiderly.mjs.map