spiderly 19.11.8 → 19.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/docs/angular-customization/index.md +80 -5
- package/agent/docs/angular-customization/references/helper-functions.generated.md +2 -0
- package/agent/docs/e2e-testing/index.md +10 -15
- package/agent/skills/add-entity/SKILL.md +12 -1
- package/fesm2022/spiderly.mjs +2090 -271
- package/fesm2022/spiderly.mjs.map +1 -1
- package/lib/components/spiderly-data-table/spiderly-data-table.component.d.ts +426 -53
- package/lib/components/spiderly-data-view/spiderly-data-view.component.d.ts +8 -1
- package/lib/directives/spiderly-filter-template.directive.d.ts +34 -0
- package/lib/directives/spiderly-overflow-title.directive.d.ts +33 -0
- package/lib/entities/filter-rule.d.ts +5 -7
- package/lib/filters/allowed-operators.d.ts +23 -0
- package/lib/filters/filter-store.d.ts +255 -0
- package/lib/filters/spiderly-filter-bar.component.d.ts +132 -0
- package/lib/services/helper-functions.d.ts +14 -0
- package/package.json +1 -1
- package/public-api.d.ts +5 -0
- package/styles/components/spiderly-data-table/spiderly-data-table.component.scss +170 -4
- package/styles/components/spiderly-data-view/spiderly-data-view.component.scss +16 -0
- package/styles/filters/spiderly-filter-bar.component.scss +163 -0
- package/styles/styles/layout/_content.scss +1 -1
- package/styles/styles/layout/_main.scss +15 -1
- package/styles/styles/layout/_menu.scss +1 -1
- package/styles/styles/layout/_mixins.scss +46 -0
- package/styles/styles/layout/_topbar.scss +4 -4
package/fesm2022/spiderly.mjs
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
29
|
+
import * as i7 from 'primeng/multiselect';
|
|
30
30
|
import { MultiSelectModule } from 'primeng/multiselect';
|
|
31
|
-
import * as i4$
|
|
31
|
+
import * as i4$5 from 'primeng/textarea';
|
|
32
32
|
import { TextareaModule } from 'primeng/textarea';
|
|
33
|
-
import * as i4$
|
|
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$
|
|
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
|
|
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$
|
|
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$
|
|
66
|
+
import * as i4$9 from 'primeng/menubar';
|
|
67
67
|
import { MenubarModule } from 'primeng/menubar';
|
|
68
|
-
import * as
|
|
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
|
}
|
|
@@ -723,6 +732,31 @@ function getImageDimensions(file) {
|
|
|
723
732
|
img.src = URL.createObjectURL(file);
|
|
724
733
|
});
|
|
725
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* Scrolls `element` back under the top of the viewport, but only when it has already scrolled
|
|
737
|
+
* above it. Set the element's `scroll-margin-top` to whatever fixed chrome must stay clear.
|
|
738
|
+
* Assumes the element's nearest scroll container is the viewport itself.
|
|
739
|
+
*/
|
|
740
|
+
// This is the one full telling of the three decisions below; paging is the caller (see
|
|
741
|
+
// spiderly-data-table/CLAUDE.md → "Scroll back to the table on page change").
|
|
742
|
+
// - The offset is READ from CSS rather than passed in, so the amount of fixed chrome to clear
|
|
743
|
+
// stays declared in one place per component and no TypeScript knows the shell's dimensions.
|
|
744
|
+
// That read is also why the viewport assumption above matters: getBoundingClientRect is
|
|
745
|
+
// viewport-relative, so inside a consumer's own overflow container it measures the wrong box.
|
|
746
|
+
// - The early return is what stops a list sitting BELOW other page content (a table inside a
|
|
747
|
+
// details form) from shoving that content off-screen when it was already visible.
|
|
748
|
+
// - `behavior: 'instant'` overrides the element's CSS `scroll-behavior`; `'auto'` defers to it,
|
|
749
|
+
// so an app setting `html { scroll-behavior: smooth }` would animate every call.
|
|
750
|
+
function scrollElementIntoViewIfAboveViewport(element) {
|
|
751
|
+
const offset = parseFloat(getComputedStyle(element).scrollMarginTop) || 0;
|
|
752
|
+
if (element.getBoundingClientRect().top >= offset)
|
|
753
|
+
return;
|
|
754
|
+
element.scrollIntoView({
|
|
755
|
+
block: 'start',
|
|
756
|
+
inline: 'nearest',
|
|
757
|
+
behavior: 'instant',
|
|
758
|
+
});
|
|
759
|
+
}
|
|
726
760
|
|
|
727
761
|
class SpiderlyCalendarComponent extends BaseControl {
|
|
728
762
|
constructor(translocoService) {
|
|
@@ -1012,7 +1046,7 @@ class SpiderlyMultiSelectComponent extends BaseDropdownControl {
|
|
|
1012
1046
|
super.ngOnInit();
|
|
1013
1047
|
}
|
|
1014
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 }); }
|
|
1015
|
-
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:
|
|
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" }] }); }
|
|
1016
1050
|
}
|
|
1017
1051
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyMultiSelectComponent, decorators: [{
|
|
1018
1052
|
type: Component,
|
|
@@ -1034,7 +1068,7 @@ class SpiderlyTextareaComponent extends BaseControl {
|
|
|
1034
1068
|
super.ngOnInit();
|
|
1035
1069
|
}
|
|
1036
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 }); }
|
|
1037
|
-
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$
|
|
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" }] }); }
|
|
1038
1072
|
}
|
|
1039
1073
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyTextareaComponent, decorators: [{
|
|
1040
1074
|
type: Component,
|
|
@@ -1059,7 +1093,7 @@ class SpiderlyNumberComponent extends BaseControl {
|
|
|
1059
1093
|
super.ngOnInit();
|
|
1060
1094
|
}
|
|
1061
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 }); }
|
|
1062
|
-
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$
|
|
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" }] }); }
|
|
1063
1097
|
}
|
|
1064
1098
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyNumberComponent, decorators: [{
|
|
1065
1099
|
type: Component,
|
|
@@ -1168,7 +1202,7 @@ class SpiderlyEditorComponent extends BaseControl {
|
|
|
1168
1202
|
};
|
|
1169
1203
|
}
|
|
1170
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 }); }
|
|
1171
|
-
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$
|
|
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" }] }); }
|
|
1172
1206
|
}
|
|
1173
1207
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyEditorComponent, decorators: [{
|
|
1174
1208
|
type: Component,
|
|
@@ -1275,7 +1309,7 @@ class SpiderlyMarkdownComponent extends BaseControl {
|
|
|
1275
1309
|
this.control.markAsDirty();
|
|
1276
1310
|
}
|
|
1277
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 }); }
|
|
1278
|
-
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$
|
|
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"] }] }); }
|
|
1279
1313
|
}
|
|
1280
1314
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyMarkdownComponent, decorators: [{
|
|
1281
1315
|
type: Component,
|
|
@@ -1368,7 +1402,7 @@ class SpiderlyButtonComponent extends SpiderlyButtonBaseComponent {
|
|
|
1368
1402
|
this.type = 'button';
|
|
1369
1403
|
}
|
|
1370
1404
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyButtonComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1371
|
-
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:
|
|
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 }] }); }
|
|
1372
1406
|
}
|
|
1373
1407
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyButtonComponent, decorators: [{
|
|
1374
1408
|
type: Component,
|
|
@@ -2830,7 +2864,7 @@ class SpiderlyPanelComponent {
|
|
|
2830
2864
|
this.onRemoveIconClick.next(null);
|
|
2831
2865
|
}
|
|
2832
2866
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2833
|
-
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$
|
|
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"] }] }); }
|
|
2834
2868
|
}
|
|
2835
2869
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyPanelComponent, decorators: [{
|
|
2836
2870
|
type: Component,
|
|
@@ -2928,7 +2962,7 @@ class VerificationWrapperComponent extends BaseFormComponent {
|
|
|
2928
2962
|
this.onResendVerificationToken.next(null);
|
|
2929
2963
|
}
|
|
2930
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 }); }
|
|
2931
|
-
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:
|
|
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"] }] }); }
|
|
2932
2966
|
}
|
|
2933
2967
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: VerificationWrapperComponent, decorators: [{
|
|
2934
2968
|
type: Component,
|
|
@@ -3574,7 +3608,7 @@ class ProfileAvatarComponent {
|
|
|
3574
3608
|
}
|
|
3575
3609
|
}
|
|
3576
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 }); }
|
|
3577
|
-
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"] }] }); }
|
|
3578
3612
|
}
|
|
3579
3613
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ProfileAvatarComponent, decorators: [{
|
|
3580
3614
|
type: Component,
|
|
@@ -3680,7 +3714,7 @@ class TopBarComponent {
|
|
|
3680
3714
|
}
|
|
3681
3715
|
}
|
|
3682
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 }); }
|
|
3683
|
-
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$
|
|
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"] }] }); }
|
|
3684
3718
|
}
|
|
3685
3719
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: TopBarComponent, decorators: [{
|
|
3686
3720
|
type: Component,
|
|
@@ -3853,7 +3887,7 @@ class NotFoundComponent {
|
|
|
3853
3887
|
this.logoPath = this.config.logoPath;
|
|
3854
3888
|
}
|
|
3855
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 }); }
|
|
3856
|
-
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:
|
|
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"] }] }); }
|
|
3857
3891
|
}
|
|
3858
3892
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: NotFoundComponent, decorators: [{
|
|
3859
3893
|
type: Component,
|
|
@@ -3936,15 +3970,747 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
3936
3970
|
}]
|
|
3937
3971
|
}] });
|
|
3938
3972
|
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
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
|
+
×
|
|
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
|
+
×
|
|
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
|
+
}] });
|
|
3948
4714
|
|
|
3949
4715
|
class SpiderlyDeleteConfirmationComponent {
|
|
3950
4716
|
constructor(ref, config) {
|
|
@@ -3976,25 +4742,139 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
3976
4742
|
args: [{ selector: 'spiderly-delete-confirmation', imports: [SpiderlyButtonComponent, TranslocoDirective, ConfirmDialogModule], providers: [ConfirmationService], template: "<ng-container *transloco=\"let t\">\n <p-confirmDialog #cd></p-confirmDialog>\n <div\n class=\"border-b surface-border text-lg\"\n style=\"\n display: flex;\n align-items: center;\n padding-bottom: 10px;\n width: 100%;\n gap: 10px;\n \"\n >\n <i class=\"pi pi-exclamation-circle text-4xl\"></i>\n <p>{{ config.data.message }}</p>\n </div>\n\n <div\n style=\"display: flex; gap: 12px; justify-content: right; margin-top: 28px\"\n >\n <spiderly-button\n (onClick)=\"reject()\"\n icon=\"pi pi-times\"\n [label]=\"t('Cancel')\"\n severity=\"secondary\"\n ></spiderly-button>\n <spiderly-button\n (onClick)=\"accept()\"\n icon=\"pi pi-check\"\n [label]=\"t('Confirm')\"\n styleClass=\"p-button-danger\"\n ></spiderly-button>\n </div>\n</ng-container>\n" }]
|
|
3977
4743
|
}], ctorParameters: () => [{ type: i1$5.DynamicDialogRef }, { type: i1$5.DynamicDialogConfig }] });
|
|
3978
4744
|
|
|
4745
|
+
/**
|
|
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.
|
|
4749
|
+
*
|
|
4750
|
+
* A table, not a switch, so the mapping is exhaustive: a new `filterType` fails the build here
|
|
4751
|
+
* instead of silently inheriting the actions-column reservation below.
|
|
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
|
+
const DEFAULT_COLUMN_WIDTH_REM = {
|
|
4758
|
+
text: 12,
|
|
4759
|
+
date: 10,
|
|
4760
|
+
multiselect: 12,
|
|
4761
|
+
boolean: 8,
|
|
4762
|
+
numeric: 12,
|
|
4763
|
+
// Fits the 45px thumbnail `#defaultCell` renders for a blob.
|
|
4764
|
+
blob: 5,
|
|
4765
|
+
};
|
|
3979
4766
|
class SpiderlyDataTableComponent {
|
|
3980
|
-
|
|
4767
|
+
/**
|
|
4768
|
+
* Whether this column is pinned against the left edge. Only the FIRST column, and only when it
|
|
4769
|
+
* is `lockVisible`: a sticky column in the middle of the grid pins the wrong thing and leaves a
|
|
4770
|
+
* hole where it used to be. Once the grid scrolls sideways a row otherwise loses its identity
|
|
4771
|
+
* — "Intesa, plaćeno, 12.400" with no idea whose order it is — and NN/g's Data Tables is
|
|
4772
|
+
* explicit that the leftmost header column must lock in place. `lockVisible` already names
|
|
4773
|
+
* exactly that column in every table, so this needs no new API (CLAUDE.md -> decision 8).
|
|
4774
|
+
*/
|
|
4775
|
+
isColumnFrozen(col) {
|
|
4776
|
+
return col.lockVisible === true && this.visibleCols[0] === col;
|
|
4777
|
+
}
|
|
4778
|
+
/**
|
|
4779
|
+
* The rightmost pinned cell, which carries the seam's shadow. It is the identity column when
|
|
4780
|
+
* there is one and the checkbox column otherwise — a fact only this side knows, which is why
|
|
4781
|
+
* the SCSS asking for it as `:last-of-type` drew nothing at all.
|
|
4782
|
+
*/
|
|
4783
|
+
isLastFrozenColumn(col) {
|
|
4784
|
+
return this.isColumnFrozen(col);
|
|
4785
|
+
}
|
|
4786
|
+
get isSelectionColumnLastFrozen() {
|
|
4787
|
+
return !this.visibleCols.some((col) => this.isColumnFrozen(col));
|
|
4788
|
+
}
|
|
4789
|
+
/** A frozen column starts after the checkbox column, when there is one. */
|
|
4790
|
+
frozenColumnOffset() {
|
|
4791
|
+
return this.selectionMode === 'multiple' ? `${this.frozenOffsetPx}px` : '0';
|
|
4792
|
+
}
|
|
4793
|
+
/**
|
|
4794
|
+
* Tracks the selection column's rendered width. A ResizeObserver rather than a one-off read:
|
|
4795
|
+
* every column drag and every window resize re-splits the shares, so the offset moves without
|
|
4796
|
+
* anything about the selection column itself changing.
|
|
4797
|
+
*/
|
|
4798
|
+
observeSelectionWidth() {
|
|
4799
|
+
const cell = this.selectionHeader()?.nativeElement;
|
|
4800
|
+
if (!cell || this.selectionWidthObserver)
|
|
4801
|
+
return;
|
|
4802
|
+
const measure = () => {
|
|
4803
|
+
const width = cell.offsetWidth;
|
|
4804
|
+
if (width === this.frozenOffsetPx)
|
|
4805
|
+
return;
|
|
4806
|
+
this.zone.run(() => (this.frozenOffsetPx = width));
|
|
4807
|
+
};
|
|
4808
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
4809
|
+
this.zone.runOutsideAngular(() => {
|
|
4810
|
+
this.selectionWidthObserver = new ResizeObserver(measure);
|
|
4811
|
+
this.selectionWidthObserver.observe(cell);
|
|
4812
|
+
});
|
|
4813
|
+
}
|
|
4814
|
+
// Measured once immediately as well, on a microtask. The observer's first callback arrives an
|
|
4815
|
+
// animation frame later, and until it does the identity column sits at left:0 — on top of the
|
|
4816
|
+
// checkbox column it is supposed to start after. The microtask keeps the write out of the CD
|
|
4817
|
+
// pass that just finished, which doing it inline here would break with an NG0100.
|
|
4818
|
+
queueMicrotask(measure);
|
|
4819
|
+
}
|
|
4820
|
+
constructor(router, dialogService, route, messageService, translocoService, configService, errorHandler, locale) {
|
|
3981
4821
|
this.router = router;
|
|
3982
4822
|
this.dialogService = dialogService;
|
|
3983
4823
|
this.route = route;
|
|
3984
4824
|
this.messageService = messageService;
|
|
3985
4825
|
this.translocoService = translocoService;
|
|
3986
4826
|
this.configService = configService;
|
|
4827
|
+
this.errorHandler = errorHandler;
|
|
3987
4828
|
this.locale = locale;
|
|
3988
4829
|
this.destroy$ = new Subject();
|
|
4830
|
+
this.injector = inject(Injector);
|
|
4831
|
+
this.zone = inject(NgZone);
|
|
4832
|
+
/**
|
|
4833
|
+
* The selection column's declared share. Under the fixed layout this is a RATIO, not a width.
|
|
4834
|
+
*
|
|
4835
|
+
* 4rem, down from 6: the original reserved room for a five-digit select-all counter sitting at
|
|
4836
|
+
* body text size beside the box, which is the widest thing it can ever hold and not what it
|
|
4837
|
+
* holds on any ordinary screen. The counter is a smaller step now (see the SCSS), so the same
|
|
4838
|
+
* five digits fit in two thirds of the space.
|
|
4839
|
+
*/
|
|
4840
|
+
this.selectionColumnWidth = '4rem';
|
|
4841
|
+
/**
|
|
4842
|
+
* The selection column's MEASURED width, which is what the frozen identity column offsets
|
|
4843
|
+
* itself by. Declaring `6rem` on both looked like the one-telling answer and is wrong: widths
|
|
4844
|
+
* are shares, so that column renders at whatever proportion of the table it gets — 148px for a
|
|
4845
|
+
* declared 6rem in one spec — and the identity column would sit 52px inside it, overlapping the
|
|
4846
|
+
* checkboxes the moment anyone scrolled. The spec caught it; nothing about the page would have.
|
|
4847
|
+
*/
|
|
4848
|
+
this.frozenOffsetPx = 0;
|
|
4849
|
+
/** Aborts an in-flight column resize's document listeners, from `onUp` or from teardown. */
|
|
4850
|
+
this.resizeAbort = null;
|
|
3989
4851
|
this.tableIcon = 'pi pi-list';
|
|
3990
4852
|
/** Paginator page-size choices; merge rules and persistence: CLAUDE.md → "Rows-per-page". */
|
|
3991
4853
|
this.rowsPerPageOptions = [10, 25, 50, 100];
|
|
4854
|
+
/**
|
|
4855
|
+
* The view currently selected. Seeded from the first view in `ngOnInit`, never left null on a
|
|
4856
|
+
* table that has views: `viewScope` keys the layout off it, so an unseeded table writes its
|
|
4857
|
+
* width and order to the SAME key an unmigrated table uses — orphaned the moment the operator
|
|
4858
|
+
* clicks the first tab — and no tab reads as selected until they do.
|
|
4859
|
+
*/
|
|
4860
|
+
this.activeViewId = null;
|
|
3992
4861
|
/** Whether the paginator is shown. Pass only when `hasLazyLoad === false`. Defaults to `true`. */
|
|
3993
4862
|
this.showPaginator = true;
|
|
3994
4863
|
/** Whether the table is wrapped in a card container. Defaults to `false`. */
|
|
3995
4864
|
this.showCardWrapper = false;
|
|
3996
4865
|
this.readonly = false;
|
|
3997
4866
|
this.idField = 'id';
|
|
4867
|
+
/**
|
|
4868
|
+
* Sizes the columns from their declared widths instead of from the rendered rows.
|
|
4869
|
+
*
|
|
4870
|
+
* The browser default is `table-layout: auto`, which computes every column from the content
|
|
4871
|
+
* of the rows currently on screen — so a column is one width on this page and another on the
|
|
4872
|
+
* next, and a filter (which narrows the variety of values in the column it filters) moves the
|
|
4873
|
+
* whole grid. PrimeNG sets no `table-layout` of its own, so without this the table inherits
|
|
4874
|
+
* that default. Bound to a field, never an object literal in the template: a literal is a new
|
|
4875
|
+
* reference on every change-detection pass.
|
|
4876
|
+
*/
|
|
4877
|
+
this.tableStyle = { 'table-layout': 'fixed' };
|
|
3998
4878
|
this.onTotalRecordsChange = new EventEmitter();
|
|
3999
4879
|
this.loading = true;
|
|
4000
4880
|
this.newlySelectedItems = [];
|
|
@@ -4010,8 +4890,6 @@ class SpiderlyDataTableComponent {
|
|
|
4010
4890
|
this.rangeAnchorId = null;
|
|
4011
4891
|
this.pendingShift = null;
|
|
4012
4892
|
this.onIsAllSelectedChange = new EventEmitter();
|
|
4013
|
-
this.matchModeDateOptions = [];
|
|
4014
|
-
this.matchModeNumberOptions = [];
|
|
4015
4893
|
/** Whether the "Add" button is shown. Defaults to `true`. */
|
|
4016
4894
|
this.showAddButton = true;
|
|
4017
4895
|
/** Whether the "Export to Excel" button is shown. Defaults to `true`. */
|
|
@@ -4049,15 +4927,54 @@ class SpiderlyDataTableComponent {
|
|
|
4049
4927
|
*/
|
|
4050
4928
|
this.columnVisibilityOverrides = {};
|
|
4051
4929
|
/**
|
|
4052
|
-
*
|
|
4053
|
-
*
|
|
4054
|
-
*
|
|
4930
|
+
* Per-column wrap, by `field`. A SEPARATE key from the visibility overrides on purpose: folding
|
|
4931
|
+
* both into one object would invalidate every layout already in an operator's localStorage, and
|
|
4932
|
+
* this is the key the width and order overrides will join (decision 5).
|
|
4933
|
+
*/
|
|
4934
|
+
this.columnWrap = {};
|
|
4935
|
+
/** Data-column fields in the operator's order. Empty means "as declared". */
|
|
4936
|
+
this.columnOrder = [];
|
|
4937
|
+
/**
|
|
4938
|
+
* Per-column width overrides, as SHARES in the same unit `getColWidth` emits — never pixels.
|
|
4939
|
+
* Under the fixed layout the browser splits surplus in proportion to these, so a pixel would
|
|
4940
|
+
* pin the column and stop it answering the window: the operator on a 1280 laptop and the one
|
|
4941
|
+
* on a 2560 monitor would get the same frozen column (decision 4).
|
|
4942
|
+
*/
|
|
4943
|
+
this.columnWidths = {};
|
|
4944
|
+
/** The column the header menu is currently open for, and its header cell. */
|
|
4945
|
+
this.menuColumn = null;
|
|
4946
|
+
this.menuHeaderCell = null;
|
|
4947
|
+
this.draggedColumn = null;
|
|
4948
|
+
/** False while a gesture that began on the menu chevron or the resize grip is in flight. */
|
|
4949
|
+
this.headerDragArmed = true;
|
|
4950
|
+
/**
|
|
4951
|
+
* A ViewChild rather than a template reference passed in from the header. The popover lives in
|
|
4952
|
+
* the CAPTION template and the button that opens it in the HEADER one, and a reference variable
|
|
4953
|
+
* does not cross an ng-template boundary — it arrived as undefined, and the click did nothing
|
|
4954
|
+
* at all.
|
|
4955
|
+
*/
|
|
4956
|
+
this.columnMenu = viewChild.required('columnMenu');
|
|
4957
|
+
this.filterBar = viewChild(SpiderlyFilterBarComponent);
|
|
4958
|
+
this.selectionHeader = viewChild('selectionHeader');
|
|
4959
|
+
/**
|
|
4960
|
+
* What the grid is ordered by, resolved to COLUMN NAMES for the bar. Read off PrimeNG's live
|
|
4961
|
+
* meta rather than mirrored into a field, so it cannot fall out of step with the sort the table
|
|
4962
|
+
* is actually applying. A field with no column left (hidden, or removed in a later release)
|
|
4963
|
+
* falls back to its own name rather than vanishing from the list.
|
|
4964
|
+
*/
|
|
4965
|
+
/**
|
|
4966
|
+
* What the grid is ordered by, resolved to COLUMN NAMES for the bar.
|
|
4967
|
+
*
|
|
4968
|
+
* A field refreshed per fetch, not a getter: it is bound to the bar's `sort`, which is a SIGNAL
|
|
4969
|
+
* input, so a freshly mapped array failed `Object.is` on every change-detection pass and marked
|
|
4970
|
+
* the bar's consumers dirty forever — for a value that changes once per request. Same reason
|
|
4971
|
+
* `tableStyle` is a field.
|
|
4055
4972
|
*/
|
|
4056
|
-
this.
|
|
4057
|
-
this.clientFilterCount = 0;
|
|
4973
|
+
this.sortKeys = [];
|
|
4058
4974
|
}
|
|
4059
4975
|
ngAfterViewInit() {
|
|
4060
4976
|
this.setupRemovableSort();
|
|
4977
|
+
this.observeSelectionWidth();
|
|
4061
4978
|
}
|
|
4062
4979
|
/**
|
|
4063
4980
|
* The template that renders this column's cells, or null to use the built-in rendering.
|
|
@@ -4139,37 +5056,10 @@ class SpiderlyDataTableComponent {
|
|
|
4139
5056
|
ngOnInit() {
|
|
4140
5057
|
if (this.rows == null)
|
|
4141
5058
|
this.rows = this.configService.defaultPageSize;
|
|
5059
|
+
this.activeViewId ??= this.views?.[0]?.id ?? null;
|
|
4142
5060
|
if (this.deleteListFromTableObservableMethod && !this.selectionMode) {
|
|
4143
5061
|
this.selectionMode = 'multiple';
|
|
4144
5062
|
}
|
|
4145
|
-
this.matchModeDateOptions = [
|
|
4146
|
-
{
|
|
4147
|
-
label: this.translocoService.translate('OnDate'),
|
|
4148
|
-
value: MatchModeCodes.Equals,
|
|
4149
|
-
},
|
|
4150
|
-
{
|
|
4151
|
-
label: this.translocoService.translate('DatesBefore'),
|
|
4152
|
-
value: MatchModeCodes.LessThan,
|
|
4153
|
-
},
|
|
4154
|
-
{
|
|
4155
|
-
label: this.translocoService.translate('DatesAfter'),
|
|
4156
|
-
value: MatchModeCodes.GreaterThan,
|
|
4157
|
-
},
|
|
4158
|
-
];
|
|
4159
|
-
this.matchModeNumberOptions = [
|
|
4160
|
-
{
|
|
4161
|
-
label: this.translocoService.translate('Equals'),
|
|
4162
|
-
value: MatchModeCodes.Equals,
|
|
4163
|
-
},
|
|
4164
|
-
{
|
|
4165
|
-
label: this.translocoService.translate('LessThan'),
|
|
4166
|
-
value: MatchModeCodes.LessThan,
|
|
4167
|
-
},
|
|
4168
|
-
{
|
|
4169
|
-
label: this.translocoService.translate('MoreThan'),
|
|
4170
|
-
value: MatchModeCodes.GreaterThan,
|
|
4171
|
-
},
|
|
4172
|
-
];
|
|
4173
5063
|
if (this.hasLazyLoad) {
|
|
4174
5064
|
const baseKey = this.stateKey ?? `spiderly-table:${this.router.url}`;
|
|
4175
5065
|
this.resolvedStateKey =
|
|
@@ -4182,7 +5072,14 @@ class SpiderlyDataTableComponent {
|
|
|
4182
5072
|
}
|
|
4183
5073
|
this.mergeActivePageSizesIntoOptions();
|
|
4184
5074
|
this.restoreColumnVisibility();
|
|
4185
|
-
this.
|
|
5075
|
+
this.restoreColumnLayout();
|
|
5076
|
+
// AFTER `resolvedStateKey` is derived, which is what both of these key off — placed above it
|
|
5077
|
+
// first, they read a null key and silently restored nothing. And before the effect is armed,
|
|
5078
|
+
// so the first request already carries the filters and the effect's skipped first run sees
|
|
5079
|
+
// the state it will be watching.
|
|
5080
|
+
this.restoreAppliedFilters();
|
|
5081
|
+
if (this.filters)
|
|
5082
|
+
this.requeryOnAppliedFilters();
|
|
4186
5083
|
this.chooserCols = this.cols.filter(SpiderlyDataTableComponent.isDataColumn);
|
|
4187
5084
|
this.refreshVisibleCols();
|
|
4188
5085
|
// Bound to p-table's [multiSortMeta] so the FIRST request already carries the right
|
|
@@ -4202,20 +5099,110 @@ class SpiderlyDataTableComponent {
|
|
|
4202
5099
|
* are a transient working set — different natural lifetimes.
|
|
4203
5100
|
*/
|
|
4204
5101
|
get columnsStateKey() {
|
|
4205
|
-
return this.resolvedStateKey
|
|
5102
|
+
return this.resolvedStateKey
|
|
5103
|
+
? `${this.resolvedStateKey}${this.viewScope}:columns`
|
|
5104
|
+
: null;
|
|
4206
5105
|
}
|
|
4207
|
-
/** Actions columns always render; data columns follow
|
|
5106
|
+
/** Actions columns always render; data columns follow the override, then declared default. */
|
|
4208
5107
|
isColumnVisible(col) {
|
|
4209
5108
|
if (!SpiderlyDataTableComponent.isDataColumn(col))
|
|
4210
5109
|
return true;
|
|
4211
5110
|
if (col.lockVisible)
|
|
4212
5111
|
return true; // pinned — wins over any (possibly stale) override
|
|
4213
|
-
if (this.revealedByConstraint.has(col.field))
|
|
4214
|
-
return true;
|
|
4215
5112
|
return this.columnVisibilityOverrides[col.field] ?? col.visible !== false;
|
|
4216
5113
|
}
|
|
5114
|
+
/**
|
|
5115
|
+
* `cols` in the operator's order. A field the stored order has never seen — a column added in a
|
|
5116
|
+
* later release — sorts to the END rather than displacing a layout someone built by hand
|
|
5117
|
+
* (CLAUDE.md -> "Operator-owned view", decision 7); the sort is stable, so those keep their
|
|
5118
|
+
* declared order among themselves. Actions columns carry no field and are always last, which is
|
|
5119
|
+
* where every consumer already declares them.
|
|
5120
|
+
*/
|
|
5121
|
+
orderedCols() {
|
|
5122
|
+
if (this.columnOrder.length === 0)
|
|
5123
|
+
return this.cols;
|
|
5124
|
+
const rank = new Map(this.columnOrder.map((field, index) => [field, index]));
|
|
5125
|
+
return [...this.cols].sort((a, b) => (rank.get(a.field) ?? Number.MAX_SAFE_INTEGER) -
|
|
5126
|
+
(rank.get(b.field) ?? Number.MAX_SAFE_INTEGER));
|
|
5127
|
+
}
|
|
5128
|
+
/**
|
|
5129
|
+
* Whether this column can move one place in `direction` (-1 left, 1 right). A locked column is
|
|
5130
|
+
* the row's anchor — and, once the left edge freezes, the thing a horizontal scroll keeps in
|
|
5131
|
+
* view — so it does not move and nothing moves in front of it.
|
|
5132
|
+
*/
|
|
5133
|
+
canMoveColumn(col, direction) {
|
|
5134
|
+
if (col.lockVisible || !col.field)
|
|
5135
|
+
return false;
|
|
5136
|
+
const order = this.dataColumnsInOrder();
|
|
5137
|
+
const from = order.indexOf(col);
|
|
5138
|
+
const to = from + direction;
|
|
5139
|
+
if (from < 0 || to < 0 || to >= order.length)
|
|
5140
|
+
return false;
|
|
5141
|
+
return !order[to].lockVisible;
|
|
5142
|
+
}
|
|
5143
|
+
moveMenuColumn(direction) {
|
|
5144
|
+
const col = this.menuColumn;
|
|
5145
|
+
this.columnMenu().hide();
|
|
5146
|
+
if (!col || !this.canMoveColumn(col, direction))
|
|
5147
|
+
return;
|
|
5148
|
+
const order = this.dataColumnsInOrder();
|
|
5149
|
+
this.placeColumn(order, col, order.indexOf(col) + direction);
|
|
5150
|
+
}
|
|
5151
|
+
/** The move itself, shared by the menu and the drag — two entry points, one rule. */
|
|
5152
|
+
placeColumn(order, col, to) {
|
|
5153
|
+
order.splice(to, 0, ...order.splice(order.indexOf(col), 1));
|
|
5154
|
+
this.columnOrder = order.map((entry) => entry.field);
|
|
5155
|
+
this.persistColumnLayout();
|
|
5156
|
+
this.refreshVisibleCols();
|
|
5157
|
+
}
|
|
5158
|
+
dataColumnsInOrder() {
|
|
5159
|
+
return this.orderedCols().filter(SpiderlyDataTableComponent.isDataColumn);
|
|
5160
|
+
}
|
|
5161
|
+
/**
|
|
5162
|
+
* Header drag, ours rather than PrimeNG's `pReorderableColumn`, for two reasons: its
|
|
5163
|
+
* `onMouseDown` arms the th for anything that is not an INPUT, a TEXTAREA or its OWN resizer —
|
|
5164
|
+
* so our menu chevron and our resize grip would each start a column drag, and reaching for a
|
|
5165
|
+
* width would silently reorder the grid — and its `onColumnDrop` reorders blind, with no notion
|
|
5166
|
+
* of a locked column (CLAUDE.md -> decision 6).
|
|
5167
|
+
*
|
|
5168
|
+
* A shortcut for neighbours, never the mechanism: HTML5 dnd has no edge auto-scroll, so a column
|
|
5169
|
+
* scrolled off the right is unreachable this way, and there is no keyboard path at all. The menu
|
|
5170
|
+
* carries both.
|
|
5171
|
+
*
|
|
5172
|
+
* This half is the arming rule; the drop rule is `canDropOn` below.
|
|
5173
|
+
*/
|
|
5174
|
+
onHeaderMouseDown(event) {
|
|
5175
|
+
const target = event.target;
|
|
5176
|
+
this.headerDragArmed = !target.closest('.column-menu-button, .column-resizer');
|
|
5177
|
+
}
|
|
5178
|
+
onHeaderDragStart(col, event) {
|
|
5179
|
+
if (col.lockVisible || !this.headerDragArmed) {
|
|
5180
|
+
event.preventDefault();
|
|
5181
|
+
return;
|
|
5182
|
+
}
|
|
5183
|
+
this.draggedColumn = col;
|
|
5184
|
+
event.dataTransfer?.setData('text/plain', col.field ?? '');
|
|
5185
|
+
}
|
|
5186
|
+
onHeaderDragOver(col, event) {
|
|
5187
|
+
if (this.canDropOn(col))
|
|
5188
|
+
event.preventDefault();
|
|
5189
|
+
}
|
|
5190
|
+
onHeaderDrop(col, event) {
|
|
5191
|
+
event.preventDefault();
|
|
5192
|
+
const dragged = this.draggedColumn;
|
|
5193
|
+
this.draggedColumn = null;
|
|
5194
|
+
if (!dragged || dragged === col || !this.canDropOn(col))
|
|
5195
|
+
return;
|
|
5196
|
+
const order = this.dataColumnsInOrder();
|
|
5197
|
+
this.placeColumn(order, dragged, order.indexOf(col));
|
|
5198
|
+
}
|
|
5199
|
+
/** Nothing lands on, or in front of, the locked column. Enforced here as well as in the menu:
|
|
5200
|
+
* a drop is a separate entry point, and the rule is about the grid rather than about a button. */
|
|
5201
|
+
canDropOn(col) {
|
|
5202
|
+
return !col.lockVisible && SpiderlyDataTableComponent.isDataColumn(col);
|
|
5203
|
+
}
|
|
4217
5204
|
refreshVisibleCols() {
|
|
4218
|
-
this.visibleCols = this.
|
|
5205
|
+
this.visibleCols = this.orderedCols().filter((col) => this.isColumnVisible(col));
|
|
4219
5206
|
this.visibleDataColsCount = this.visibleCols.filter(SpiderlyDataTableComponent.isDataColumn).length;
|
|
4220
5207
|
}
|
|
4221
5208
|
/**
|
|
@@ -4230,7 +5217,6 @@ class SpiderlyDataTableComponent {
|
|
|
4230
5217
|
return this.visibleDataColsCount > 1;
|
|
4231
5218
|
}
|
|
4232
5219
|
toggleColumn(col, visible) {
|
|
4233
|
-
this.revealedByConstraint.delete(col.field); // an explicit choice supersedes a safety reveal
|
|
4234
5220
|
if (visible === (col.visible !== false)) {
|
|
4235
5221
|
delete this.columnVisibilityOverrides[col.field]; // back at the declared default
|
|
4236
5222
|
}
|
|
@@ -4239,48 +5225,99 @@ class SpiderlyDataTableComponent {
|
|
|
4239
5225
|
}
|
|
4240
5226
|
this.persistColumnVisibility();
|
|
4241
5227
|
this.refreshVisibleCols();
|
|
4242
|
-
if (!visible)
|
|
4243
|
-
this.clearHiddenColumnConstraints([col]);
|
|
4244
5228
|
}
|
|
4245
|
-
/**
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
5229
|
+
/**
|
|
5230
|
+
* Restores every column to its declared default and forgets the stored overrides — visibility,
|
|
5231
|
+
* wrap, order and width alike. It undid visibility only until the header menu shipped three
|
|
5232
|
+
* more gestures beside it, and a layout with no way back is worse than one with no knobs.
|
|
5233
|
+
*
|
|
5234
|
+
* Hiding — here or from the menu — never touches filters or sort: the chip bar names every
|
|
5235
|
+
* applied filter and the sort chip names the sort, hidden column or not (decision 2b). The
|
|
5236
|
+
* legacy "hidden contributes nothing" apparatus died with the header-filter path.
|
|
5237
|
+
*/
|
|
5238
|
+
resetColumnLayout() {
|
|
5239
|
+
this.columnWrap = {};
|
|
5240
|
+
this.columnOrder = [];
|
|
5241
|
+
this.columnWidths = {};
|
|
5242
|
+
this.persistColumnLayout();
|
|
4249
5243
|
this.columnVisibilityOverrides = {};
|
|
4250
5244
|
this.persistColumnVisibility();
|
|
4251
5245
|
this.refreshVisibleCols();
|
|
4252
|
-
// Columns the reset just hid follow the same rule as a manual hide.
|
|
4253
|
-
this.clearHiddenColumnConstraints(wasVisible.filter((col) => !this.isColumnVisible(col)));
|
|
4254
5246
|
}
|
|
4255
5247
|
/**
|
|
4256
|
-
*
|
|
4257
|
-
*
|
|
4258
|
-
*
|
|
5248
|
+
* Layout keys carry the ACTIVE VIEW, which is what makes a view more than a saved filter: a
|
|
5249
|
+
* picking view and a payments view want different COLUMNS, not just different rows. Global
|
|
5250
|
+
* layout would re-create the original complaint one level up — the operator still re-picking
|
|
5251
|
+
* columns every time the job changes.
|
|
5252
|
+
*
|
|
5253
|
+
* A table with no views has no segment, so the twenty-six tables that have not migrated keep
|
|
5254
|
+
* reading exactly the keys they already wrote.
|
|
4259
5255
|
*/
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
return;
|
|
4263
|
-
let cleared = false;
|
|
4264
|
-
for (const col of cols) {
|
|
4265
|
-
const filterKey = col.filterField ?? col.field;
|
|
4266
|
-
if (SpiderlyDataTableComponent.isActiveFilterMeta(this.table.filters?.[filterKey])) {
|
|
4267
|
-
delete this.table.filters[filterKey];
|
|
4268
|
-
cleared = true;
|
|
4269
|
-
}
|
|
4270
|
-
if (this.table._multiSortMeta?.some((m) => m.field === col.field)) {
|
|
4271
|
-
this.table._multiSortMeta = this.table._multiSortMeta.filter((m) => m.field !== col.field);
|
|
4272
|
-
this.table.tableService.onSort(this.table._multiSortMeta);
|
|
4273
|
-
cleared = true;
|
|
4274
|
-
}
|
|
4275
|
-
}
|
|
4276
|
-
if (cleared)
|
|
4277
|
-
this.table._filter(); // re-emits the lazy load and saves the cleaned state
|
|
5256
|
+
get viewScope() {
|
|
5257
|
+
return this.activeViewId ? `:${this.activeViewId}` : '';
|
|
4278
5258
|
}
|
|
4279
|
-
/**
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
5259
|
+
/**
|
|
5260
|
+
* Where the applied filters live. Follows `stateStorage` rather than the layout's always-local
|
|
5261
|
+
* rule, because that is where they lived before the bar took them off the header: PrimeNG's
|
|
5262
|
+
* stateful table persisted them for free, and nothing replaced it — a refresh dropped every
|
|
5263
|
+
* filter (Filip, on /tags). A filter is a question you are in the middle of asking, not a
|
|
5264
|
+
* durable preference like a column's width.
|
|
5265
|
+
*/
|
|
5266
|
+
get filtersStateKey() {
|
|
5267
|
+
return this.resolvedStateKey
|
|
5268
|
+
? `${this.resolvedStateKey}${this.viewScope}:filters`
|
|
5269
|
+
: null;
|
|
5270
|
+
}
|
|
5271
|
+
get filterStorage() {
|
|
5272
|
+
return this.stateStorage === 'local' ? localStorage : sessionStorage;
|
|
5273
|
+
}
|
|
5274
|
+
persistAppliedFilters() {
|
|
5275
|
+
if (!this.filtersStateKey || !this.filters)
|
|
5276
|
+
return;
|
|
5277
|
+
const snapshot = this.filters.snapshot();
|
|
5278
|
+
if (Object.keys(snapshot).length === 0) {
|
|
5279
|
+
this.filterStorage.removeItem(this.filtersStateKey);
|
|
5280
|
+
}
|
|
5281
|
+
else {
|
|
5282
|
+
writeStoredJson(this.filterStorage, this.filtersStateKey, snapshot);
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
restoreAppliedFilters() {
|
|
5286
|
+
if (!this.filtersStateKey || !this.filters)
|
|
5287
|
+
return;
|
|
5288
|
+
const snapshot = readStoredJson(this.filterStorage, this.filtersStateKey);
|
|
5289
|
+
if (snapshot)
|
|
5290
|
+
this.filters.restore(snapshot);
|
|
5291
|
+
}
|
|
5292
|
+
get layoutStateKey() {
|
|
5293
|
+
return this.resolvedStateKey
|
|
5294
|
+
? `${this.resolvedStateKey}${this.viewScope}:layout`
|
|
5295
|
+
: null;
|
|
5296
|
+
}
|
|
5297
|
+
/** One key for the whole layout — wrap now, widths next (decision 5). */
|
|
5298
|
+
persistColumnLayout() {
|
|
5299
|
+
if (!this.layoutStateKey)
|
|
5300
|
+
return;
|
|
5301
|
+
const layout = {
|
|
5302
|
+
wrap: this.columnWrap,
|
|
5303
|
+
order: this.columnOrder,
|
|
5304
|
+
widths: this.columnWidths,
|
|
5305
|
+
};
|
|
5306
|
+
const isDefault = Object.keys(this.columnWrap).length === 0 &&
|
|
5307
|
+
this.columnOrder.length === 0 &&
|
|
5308
|
+
Object.keys(this.columnWidths).length === 0;
|
|
5309
|
+
if (isDefault)
|
|
5310
|
+
localStorage.removeItem(this.layoutStateKey);
|
|
5311
|
+
else
|
|
5312
|
+
writeStoredJson(localStorage, this.layoutStateKey, layout);
|
|
5313
|
+
}
|
|
5314
|
+
restoreColumnLayout() {
|
|
5315
|
+
if (!this.layoutStateKey)
|
|
5316
|
+
return;
|
|
5317
|
+
const layout = readStoredJson(localStorage, this.layoutStateKey);
|
|
5318
|
+
this.columnWrap = layout?.wrap ?? {};
|
|
5319
|
+
this.columnOrder = layout?.order ?? [];
|
|
5320
|
+
this.columnWidths = layout?.widths ?? {};
|
|
4284
5321
|
}
|
|
4285
5322
|
persistColumnVisibility() {
|
|
4286
5323
|
if (!this.columnsStateKey)
|
|
@@ -4307,47 +5344,16 @@ class SpiderlyDataTableComponent {
|
|
|
4307
5344
|
return null;
|
|
4308
5345
|
return readStoredJson(this.stateStorage === 'local' ? localStorage : sessionStorage, this.resolvedStateKey);
|
|
4309
5346
|
}
|
|
4310
|
-
/**
|
|
4311
|
-
* Guards the "hidden contributes nothing" invariant against state this component didn't
|
|
4312
|
-
* write (older blobs, hand-edited storage): a hidden column the persisted table state
|
|
4313
|
-
* still filters or sorts by is revealed rather than constraining the data invisibly.
|
|
4314
|
-
* In-memory only — once the constraint is gone, the user's stored choice reapplies.
|
|
4315
|
-
*/
|
|
4316
|
-
reconcileVisibilityWithPersistedConstraints() {
|
|
4317
|
-
const state = this.persistedTableState();
|
|
4318
|
-
if (!state)
|
|
4319
|
-
return;
|
|
4320
|
-
const constrained = new Set();
|
|
4321
|
-
for (const [field, meta] of Object.entries(state.filters ?? {})) {
|
|
4322
|
-
if (SpiderlyDataTableComponent.isActiveFilterMeta(meta))
|
|
4323
|
-
constrained.add(field);
|
|
4324
|
-
}
|
|
4325
|
-
for (const sortMeta of state.multiSortMeta ?? []) {
|
|
4326
|
-
if (sortMeta?.field)
|
|
4327
|
-
constrained.add(sortMeta.field);
|
|
4328
|
-
}
|
|
4329
|
-
for (const col of this.cols) {
|
|
4330
|
-
if (!SpiderlyDataTableComponent.isDataColumn(col))
|
|
4331
|
-
continue;
|
|
4332
|
-
if (this.isColumnVisible(col))
|
|
4333
|
-
continue;
|
|
4334
|
-
if (constrained.has(col.field) || constrained.has(col.filterField)) {
|
|
4335
|
-
this.revealedByConstraint.add(col.field);
|
|
4336
|
-
}
|
|
4337
|
-
}
|
|
4338
|
-
}
|
|
4339
5347
|
//#endregion
|
|
4340
5348
|
/**
|
|
4341
|
-
* The declared default as PrimeNG sort meta, or null when none is declared
|
|
4342
|
-
* column is hidden
|
|
4343
|
-
*
|
|
5349
|
+
* The declared default as PrimeNG sort meta, or null when none is declared. Applies even
|
|
5350
|
+
* while its column is hidden: the sort chip names the ordering whether or not the column
|
|
5351
|
+
* is on screen (decision 2b).
|
|
4344
5352
|
*/
|
|
4345
5353
|
defaultMultiSortMeta() {
|
|
4346
5354
|
if (!this.defaultSortField)
|
|
4347
5355
|
return null;
|
|
4348
5356
|
const defaultSortCol = this.cols?.find((col) => col.field === this.defaultSortField);
|
|
4349
|
-
if (defaultSortCol && !this.isColumnVisible(defaultSortCol))
|
|
4350
|
-
return null;
|
|
4351
5357
|
// A declared default on a non-sortable column is a consumer mistake the backend answers
|
|
4352
5358
|
// with a 400 on every load; fall back to its implicit Id DESC instead (see keepSortableMeta).
|
|
4353
5359
|
if (defaultSortCol && !this.isColumnSortable(defaultSortCol))
|
|
@@ -4392,58 +5398,118 @@ class SpiderlyDataTableComponent {
|
|
|
4392
5398
|
this.table.tableService.onSort(defaultSort);
|
|
4393
5399
|
}
|
|
4394
5400
|
}
|
|
5401
|
+
onPageChange(container) {
|
|
5402
|
+
scrollElementIntoViewIfAboveViewport(container);
|
|
5403
|
+
}
|
|
5404
|
+
/**
|
|
5405
|
+
* The one pending predicate: PrimeNG's overlay and the container's `aria-busy` must never
|
|
5406
|
+
* disagree about whether a fetch is in flight.
|
|
5407
|
+
*
|
|
5408
|
+
* It used to also test `items === undefined`, for the first load. That clause is dead now
|
|
5409
|
+
* that `loading` initialises true and every lazyLoad raises it — except in the one state
|
|
5410
|
+
* where it was actively wrong: a FAILED first load lowers the flag with `items` still
|
|
5411
|
+
* undefined, which pinned the overlay up forever. Same stranded overlay this component was
|
|
5412
|
+
* fixed for once already, through the other door.
|
|
5413
|
+
*/
|
|
5414
|
+
get isPending() {
|
|
5415
|
+
return this.loading;
|
|
5416
|
+
}
|
|
4395
5417
|
lazyLoad(event) {
|
|
5418
|
+
// Every refetch is pending, not just the first load and reload(). `items` is deliberately
|
|
5419
|
+
// left alone: the previous page stays on screen under the overlay (stale-while-revalidate)
|
|
5420
|
+
// rather than blanking. Raising the flag is also what stops PrimeNG's empty message, gated
|
|
5421
|
+
// on `isEmpty() && !loading`, from claiming "no records" for the length of the request.
|
|
5422
|
+
this.loading = true;
|
|
4396
5423
|
this.applyDefaultSortIfUnsorted(event);
|
|
4397
5424
|
this.lastLazyLoadEvent = event;
|
|
5425
|
+
this.refreshSortKeys();
|
|
4398
5426
|
this.rangeAnchorId = null;
|
|
4399
5427
|
let tableFilter = event;
|
|
4400
5428
|
tableFilter.additionalFilterIdLong = this.additionalFilterIdLong;
|
|
5429
|
+
// The store is the ONLY source of what narrows the grid, on every table. Unconditional on
|
|
5430
|
+
// purpose: with the header filters gone, the one remaining writer of PrimeNG's own
|
|
5431
|
+
// `event.filters` is a stale persisted blob from the header-filter era replayed by
|
|
5432
|
+
// `restoreState` — and letting that through would narrow a storeless table invisibly, with
|
|
5433
|
+
// no chip and no button left to clear it.
|
|
5434
|
+
tableFilter.filters = this.filters?.toFilterPayload() ?? {};
|
|
4401
5435
|
this.onLazyLoad.next(tableFilter);
|
|
5436
|
+
// Issued NOW rather than after the list lands: it needs only `tableFilter`, so serialising
|
|
5437
|
+
// it behind the list response cost a second round trip on every page of every
|
|
5438
|
+
// selection-enabled table — and the overlay is held up for both. The `.catch` at creation
|
|
5439
|
+
// is mandatory, not defensive: when the list request errors, `next` never runs, and a
|
|
5440
|
+
// rejected promise nobody awaits is exactly the unowned rejection this handler exists to
|
|
5441
|
+
// prevent. The rejection is re-thrown out of `awaitSelectedIds` instead, where it has one.
|
|
5442
|
+
const selectedIds = this.selectedLazyLoadObservableMethod != null
|
|
5443
|
+
? firstValueFrom(this.selectedLazyLoadObservableMethod(tableFilter)).catch((error) => error)
|
|
5444
|
+
: null;
|
|
4402
5445
|
this.getPaginatedListObservableMethod(tableFilter).subscribe({
|
|
4403
5446
|
next: async (res) => {
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
this.
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
if (this.isFirstTimeLazyLoad == true) {
|
|
4413
|
-
this.rowsSelectedNumber =
|
|
4414
|
-
selectedRowsMethodResult.totalRecordsSelected;
|
|
4415
|
-
this.setFakeIsAllSelected();
|
|
4416
|
-
this.isFirstTimeLazyLoad = false;
|
|
4417
|
-
}
|
|
4418
|
-
if (this.isAllSelected == true) {
|
|
4419
|
-
let idsToInsert = [...this.items.map((x) => x[this.idField])];
|
|
4420
|
-
idsToInsert = idsToInsert.filter((x) => this.unselectedItems.includes(x) == false);
|
|
4421
|
-
this.fakeSelectedItems = [...idsToInsert]; // Only for showing checkboxes, we will not send this to the backend
|
|
4422
|
-
}
|
|
4423
|
-
else if (this.isAllSelected == false) {
|
|
4424
|
-
this.fakeSelectedItems = [...this.newlySelectedItems]; // Only for showing checkboxes, we will not send this to the backend
|
|
4425
|
-
}
|
|
4426
|
-
else if (this.isAllSelected == null) {
|
|
4427
|
-
let idsToInsert = [
|
|
4428
|
-
...selectedRowsMethodResult.selectedIds,
|
|
4429
|
-
...this.newlySelectedItems,
|
|
4430
|
-
];
|
|
4431
|
-
idsToInsert = idsToInsert.filter((x) => this.unselectedItems.includes(x) == false);
|
|
4432
|
-
this.fakeSelectedItems = [...idsToInsert];
|
|
4433
|
-
}
|
|
5447
|
+
// The await below is INSIDE an async subscriber, so a rejection there settles nothing:
|
|
5448
|
+
// `error:` belongs to the paginated-list observable and never runs. That used to strand
|
|
5449
|
+
// the overlay forever, hence the finally.
|
|
5450
|
+
try {
|
|
5451
|
+
this.items = res.data;
|
|
5452
|
+
this.totalRecords = res.totalRecords;
|
|
5453
|
+
this.onTotalRecordsChange.next(res.totalRecords);
|
|
5454
|
+
await this.reconcileSelectionForLoadedPage(selectedIds);
|
|
4434
5455
|
}
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
5456
|
+
catch (error) {
|
|
5457
|
+
// The library's own owner: it toasts, it skips HttpErrorResponse (the interceptor
|
|
5458
|
+
// already reported those), and a consumer's ErrorHandler wrapper forwards to their
|
|
5459
|
+
// tracker. console.error here would have dropped both.
|
|
5460
|
+
this.errorHandler.handleError(error);
|
|
5461
|
+
}
|
|
5462
|
+
finally {
|
|
5463
|
+
this.loading = false;
|
|
4439
5464
|
}
|
|
4440
|
-
this.loading = false;
|
|
4441
5465
|
},
|
|
4442
5466
|
error: () => {
|
|
4443
5467
|
this.loading = false;
|
|
4444
5468
|
},
|
|
4445
5469
|
});
|
|
4446
5470
|
}
|
|
5471
|
+
/**
|
|
5472
|
+
* Repaints the checkbox column for the page that just landed. Split out of the subscriber so
|
|
5473
|
+
* the try/finally that guarantees the pending flag wraps an await, not fifty lines of
|
|
5474
|
+
* selection algebra.
|
|
5475
|
+
*/
|
|
5476
|
+
async reconcileSelectionForLoadedPage(selectedIds) {
|
|
5477
|
+
if (selectedIds == null) {
|
|
5478
|
+
if (this.deleteListFromTableObservableMethod) {
|
|
5479
|
+
this.fakeSelectedItems = this.items
|
|
5480
|
+
.map((x) => x[this.idField])
|
|
5481
|
+
.filter((id) => this.newlySelectedItems.includes(id));
|
|
5482
|
+
}
|
|
5483
|
+
return;
|
|
5484
|
+
}
|
|
5485
|
+
const selectedRowsMethodResult = await selectedIds;
|
|
5486
|
+
if (selectedRowsMethodResult instanceof Error)
|
|
5487
|
+
throw selectedRowsMethodResult;
|
|
5488
|
+
this.currentPageSelectedItemsFromDb = [
|
|
5489
|
+
...selectedRowsMethodResult.selectedIds,
|
|
5490
|
+
];
|
|
5491
|
+
if (this.isFirstTimeLazyLoad == true) {
|
|
5492
|
+
this.rowsSelectedNumber = selectedRowsMethodResult.totalRecordsSelected;
|
|
5493
|
+
this.setFakeIsAllSelected();
|
|
5494
|
+
this.isFirstTimeLazyLoad = false;
|
|
5495
|
+
}
|
|
5496
|
+
if (this.isAllSelected == true) {
|
|
5497
|
+
let idsToInsert = [...this.items.map((x) => x[this.idField])];
|
|
5498
|
+
idsToInsert = idsToInsert.filter((x) => this.unselectedItems.includes(x) == false);
|
|
5499
|
+
this.fakeSelectedItems = [...idsToInsert]; // Only for showing checkboxes, we will not send this to the backend
|
|
5500
|
+
}
|
|
5501
|
+
else if (this.isAllSelected == false) {
|
|
5502
|
+
this.fakeSelectedItems = [...this.newlySelectedItems]; // Only for showing checkboxes, we will not send this to the backend
|
|
5503
|
+
}
|
|
5504
|
+
else if (this.isAllSelected == null) {
|
|
5505
|
+
let idsToInsert = [
|
|
5506
|
+
...selectedRowsMethodResult.selectedIds,
|
|
5507
|
+
...this.newlySelectedItems,
|
|
5508
|
+
];
|
|
5509
|
+
idsToInsert = idsToInsert.filter((x) => this.unselectedItems.includes(x) == false);
|
|
5510
|
+
this.fakeSelectedItems = [...idsToInsert];
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
4447
5513
|
clientLoad() {
|
|
4448
5514
|
this.loading = false;
|
|
4449
5515
|
this.loadFormArrayItems();
|
|
@@ -4458,19 +5524,6 @@ class SpiderlyDataTableComponent {
|
|
|
4458
5524
|
this.rowsSelectedNumber = this.selectedItemIds.length;
|
|
4459
5525
|
this.setFakeIsAllSelected();
|
|
4460
5526
|
}
|
|
4461
|
-
filter(event) {
|
|
4462
|
-
if (this.hasLazyLoad && this.selectionMode === 'multiple')
|
|
4463
|
-
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
|
|
4464
|
-
if (this.hasLazyLoad === false && this.selectionMode === 'multiple') {
|
|
4465
|
-
if (this.clientFilterCount === 0) {
|
|
4466
|
-
this.loadFormArrayItems();
|
|
4467
|
-
this.clientFilterCount++;
|
|
4468
|
-
}
|
|
4469
|
-
else {
|
|
4470
|
-
this.clientFilterCount--;
|
|
4471
|
-
}
|
|
4472
|
-
}
|
|
4473
|
-
}
|
|
4474
5527
|
loadFormArrayItems() {
|
|
4475
5528
|
this.items = this.getFormArrayItems(this.additionalIndexes);
|
|
4476
5529
|
this.rangeAnchorId = null;
|
|
@@ -4490,64 +5543,331 @@ class SpiderlyDataTableComponent {
|
|
|
4490
5543
|
!!col.field &&
|
|
4491
5544
|
!col.field.endsWith('CommaSeparated'));
|
|
4492
5545
|
}
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
return 'min-width: 12rem;';
|
|
4500
|
-
case 'date':
|
|
4501
|
-
return 'min-width: 10rem;';
|
|
4502
|
-
case 'multiselect':
|
|
4503
|
-
return 'min-width: 12rem;';
|
|
4504
|
-
case 'boolean':
|
|
4505
|
-
return 'min-width: 8rem;';
|
|
4506
|
-
case 'numeric':
|
|
4507
|
-
return 'min-width: 12rem;';
|
|
4508
|
-
default:
|
|
4509
|
-
return 'width: 0rem;'; // fitting content of the row like this
|
|
4510
|
-
}
|
|
5546
|
+
openColumnMenu(col, event) {
|
|
5547
|
+
this.menuColumn = col;
|
|
5548
|
+
// Kept so `fitMenuColumn` can find the column's cells: it needs the header's position among
|
|
5549
|
+
// its siblings, which is the only thing that ties a Column to a table column in the DOM.
|
|
5550
|
+
this.menuHeaderCell = event.target.closest('th');
|
|
5551
|
+
this.columnMenu().toggle(event);
|
|
4511
5552
|
}
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
5553
|
+
/**
|
|
5554
|
+
* Switches to a view. Clears first, always: a view is a STATE rather than an addition, and two
|
|
5555
|
+
* of them composed produce a question nobody asked. The clear runs through the store directly
|
|
5556
|
+
* rather than through `clear(table)`, which would also wipe the sort and the persisted state a
|
|
5557
|
+
* view is entitled to keep.
|
|
5558
|
+
*/
|
|
5559
|
+
selectView(view) {
|
|
5560
|
+
this.activeViewId = view.id;
|
|
5561
|
+
// The keys just changed under us, so the layout has to be re-read for the view being entered
|
|
5562
|
+
// — otherwise the one being left keeps rendering until something else forces a reload.
|
|
5563
|
+
this.restoreColumnVisibility();
|
|
5564
|
+
this.restoreColumnLayout();
|
|
5565
|
+
this.refreshVisibleCols();
|
|
5566
|
+
if (!this.filters)
|
|
5567
|
+
return;
|
|
5568
|
+
// The filter key moved with the view too, so this view's own stored answer wins over whatever
|
|
5569
|
+
// the last one left applied; `apply` runs only when the view has nothing stored yet. A
|
|
5570
|
+
// TRANSIENT view inverts that: its apply is a function of now, so it always re-derives —
|
|
5571
|
+
// restoring would put yesterday's date under a tab claiming "today".
|
|
5572
|
+
this.filters.clear();
|
|
5573
|
+
const stored = !view.transient && this.filtersStateKey
|
|
5574
|
+
? readStoredJson(this.filterStorage, this.filtersStateKey)
|
|
5575
|
+
: null;
|
|
5576
|
+
if (stored)
|
|
5577
|
+
this.filters.restore(stored);
|
|
5578
|
+
else
|
|
5579
|
+
view.apply?.(this.filters);
|
|
4527
5580
|
}
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
return MatchModeCodes.Equals;
|
|
4538
|
-
case 'numeric':
|
|
4539
|
-
return MatchModeCodes.Equals;
|
|
4540
|
-
default:
|
|
4541
|
-
return null;
|
|
4542
|
-
}
|
|
5581
|
+
/**
|
|
5582
|
+
* The filter this column stands for. Defaults to the column's own `field`, because a store id
|
|
5583
|
+
* IS a backend property name — `toFilterPayload` emits it straight into `Filter.filters`. So
|
|
5584
|
+
* `filterId` is an escape hatch for the rare mismatch, not something every column restates:
|
|
5585
|
+
* all eight of the first migrated table's declarations were the field name written a second
|
|
5586
|
+
* time.
|
|
5587
|
+
*/
|
|
5588
|
+
filterIdFor(col) {
|
|
5589
|
+
return col.filterId ?? col.field;
|
|
4543
5590
|
}
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
5591
|
+
/**
|
|
5592
|
+
* Asks the STORE, not the column. Checking only that an id was declared let a typo through to
|
|
5593
|
+
* `store.get(id)`, which reads `definitions[id].label` — so a mistyped id threw on click rather
|
|
5594
|
+
* than greying the item out. With 195 declarations to migrate, that is the failure mode worth
|
|
5595
|
+
* closing here rather than in each consumer.
|
|
5596
|
+
*/
|
|
5597
|
+
canFilterColumn(col) {
|
|
5598
|
+
const id = this.filterIdFor(col);
|
|
5599
|
+
return id != null && this.filters?.definitions[id] != null;
|
|
5600
|
+
}
|
|
5601
|
+
/**
|
|
5602
|
+
* Opens the bar's editor for this column's filter. The bar took the filter off the header and
|
|
5603
|
+
* the header is still where an operator reaches for it, which is twenty-seven tables' worth of
|
|
5604
|
+
* habit; this is the shortcut back rather than a second filter surface — it drives the same
|
|
5605
|
+
* editor, on the same bar, that `+ Filter` opens.
|
|
5606
|
+
*/
|
|
5607
|
+
filterMenuColumn() {
|
|
5608
|
+
const col = this.menuColumn;
|
|
5609
|
+
this.columnMenu().hide();
|
|
5610
|
+
if (!col || !this.canFilterColumn(col))
|
|
5611
|
+
return;
|
|
5612
|
+
this.filterBar()?.startEditing(this.filterIdFor(col));
|
|
5613
|
+
}
|
|
5614
|
+
/**
|
|
5615
|
+
* Sorts by this column in the NAMED direction. Clicking a header cycles asc, desc and off,
|
|
5616
|
+
* which is fine for one column and a guess for a direction someone wants now — you click, look,
|
|
5617
|
+
* and click again if it went the other way. This is also the only sort path that works from a
|
|
5618
|
+
* keyboard.
|
|
5619
|
+
*
|
|
5620
|
+
* A column the generated paginator has no sort case for answers every load with a 400, so the
|
|
5621
|
+
* items are disabled by the same predicate that keeps its header from being clickable.
|
|
5622
|
+
*/
|
|
5623
|
+
sortMenuColumn(order) {
|
|
5624
|
+
const col = this.menuColumn;
|
|
5625
|
+
this.columnMenu().hide();
|
|
5626
|
+
if (!col?.field || !this.isColumnSortable(col))
|
|
5627
|
+
return;
|
|
5628
|
+
this.table._multiSortMeta = [{ field: col.field, order }];
|
|
5629
|
+
this.table.sortMultiple();
|
|
5630
|
+
}
|
|
5631
|
+
/**
|
|
5632
|
+
* Sizes the column to the widest thing in it. The cheaper escape from a cramped column: no
|
|
5633
|
+
* drag, no aim. Unlike a drag it does NOT trade with the neighbour — a minimum that grew is
|
|
5634
|
+
* exactly the case decision 4 lets push the table past its container and start scrolling.
|
|
5635
|
+
*
|
|
5636
|
+
* Measures the CELL CONTENT, not the cell: the clamp lives on the inner span, so a `td` reports
|
|
5637
|
+
* a scrollWidth that fits while the text inside it does not.
|
|
5638
|
+
*/
|
|
5639
|
+
fitMenuColumn() {
|
|
5640
|
+
const col = this.menuColumn;
|
|
5641
|
+
const th = this.menuHeaderCell;
|
|
5642
|
+
this.columnMenu().hide();
|
|
5643
|
+
if (!col?.field || !th?.parentElement)
|
|
5644
|
+
return;
|
|
5645
|
+
// Walked up from the header rather than reached through PrimeNG's own ElementRef: the th is
|
|
5646
|
+
// the only thing that ties a Column to a table column in the DOM, and it already knows which
|
|
5647
|
+
// table it is in.
|
|
5648
|
+
const index = Array.from(th.parentElement.children).indexOf(th) + 1;
|
|
5649
|
+
const cells = th
|
|
5650
|
+
.closest('table')
|
|
5651
|
+
?.querySelectorAll(`tbody tr > td:nth-child(${index})`);
|
|
5652
|
+
if (!cells?.length)
|
|
5653
|
+
return;
|
|
5654
|
+
// Padding comes from one CSS rule for every cell in the column, so it is read once off the
|
|
5655
|
+
// first — a getComputedStyle per cell was a hundred style flushes for one number.
|
|
5656
|
+
const styles = getComputedStyle(cells[0]);
|
|
5657
|
+
const padding = (parseFloat(styles.paddingLeft) || 0) +
|
|
5658
|
+
(parseFloat(styles.paddingRight) || 0);
|
|
5659
|
+
// Cells AND their descendants in one query rather than one per cell: the clamp lives on an
|
|
5660
|
+
// inner span, so a td reports a scrollWidth that fits while the text inside it does not, and
|
|
5661
|
+
// a consumer's cell template can nest the overflowing element arbitrarily deep.
|
|
5662
|
+
const content = th
|
|
5663
|
+
.closest('table')
|
|
5664
|
+
.querySelectorAll(`tbody tr > td:nth-child(${index}), tbody tr > td:nth-child(${index}) *`);
|
|
5665
|
+
let widest = th.scrollWidth;
|
|
5666
|
+
for (const node of Array.from(content)) {
|
|
5667
|
+
widest = Math.max(widest, node.scrollWidth + padding);
|
|
5668
|
+
}
|
|
5669
|
+
this.columnWidths[col.field] = this.shareThatFits(col, th, widest);
|
|
5670
|
+
this.persistColumnLayout();
|
|
5671
|
+
}
|
|
5672
|
+
/**
|
|
5673
|
+
* The share that actually yields `needed` pixels — not `needed` scaled by the column's current
|
|
5674
|
+
* px-per-share, which is the obvious answer and the wrong one. Shares are a PROPORTION of
|
|
5675
|
+
* whatever width the table has, so growing this column's share also shrinks the realized width
|
|
5676
|
+
* of every other column, and the naive figure lands short. The spec caught it as a cell that
|
|
5677
|
+
* was still clipped after being told to fit.
|
|
5678
|
+
*
|
|
5679
|
+
* While the columns fit, the table keeps the container's width and the proportion has to solve
|
|
5680
|
+
* needed / tableWidth = newShare / (otherShares + newShare)
|
|
5681
|
+
* with `otherShares` recovered from what this column currently renders at. Once `needed` is the
|
|
5682
|
+
* whole container the proportional regime has no answer: the table is about to overrun its
|
|
5683
|
+
* container and scroll (decision 4), and there a share IS its own length, so the pixels convert
|
|
5684
|
+
* straight through the root font size.
|
|
5685
|
+
*/
|
|
5686
|
+
shareThatFits(col, th, needed) {
|
|
5687
|
+
const share = this.columnShare(col);
|
|
5688
|
+
const thPx = Math.max(th.offsetWidth, 1);
|
|
5689
|
+
const tableWidth = th.closest('table')?.offsetWidth ?? 0;
|
|
5690
|
+
if (needed < tableWidth) {
|
|
5691
|
+
const otherShares = (share * (tableWidth - thPx)) / thPx;
|
|
5692
|
+
return (needed * otherShares) / (tableWidth - needed);
|
|
4547
5693
|
}
|
|
4548
|
-
|
|
4549
|
-
|
|
5694
|
+
return needed / rootFontSizePx();
|
|
5695
|
+
}
|
|
5696
|
+
hideMenuColumn() {
|
|
5697
|
+
if (this.menuColumn)
|
|
5698
|
+
this.toggleColumn(this.menuColumn, false);
|
|
5699
|
+
this.columnMenu().hide();
|
|
5700
|
+
}
|
|
5701
|
+
/**
|
|
5702
|
+
* Whether this column's cells give up the one-line clamp. The DEFAULT stays clamped
|
|
5703
|
+
* (CLAUDE.md -> "Operator-owned view", decision 9); what changes is that the operator picks
|
|
5704
|
+
* which column pays the row height, not whoever wrote `cols`.
|
|
5705
|
+
*
|
|
5706
|
+
* Lands on the td as `.cell-wrap`, whose inherited `--spiderly-cell-*` properties are how the
|
|
5707
|
+
* toggle crosses into a projected `spiderlyCellTemplate` too — the contract's one telling is
|
|
5708
|
+
* on that SCSS rule; a consumer clamp that hard-codes its values opts out.
|
|
5709
|
+
*/
|
|
5710
|
+
isColumnWrapped(col) {
|
|
5711
|
+
return col.field != null && this.columnWrap[col.field] === true;
|
|
5712
|
+
}
|
|
5713
|
+
get isMenuColumnWrapped() {
|
|
5714
|
+
return this.menuColumn != null && this.isColumnWrapped(this.menuColumn);
|
|
5715
|
+
}
|
|
5716
|
+
toggleWrapForMenuColumn() {
|
|
5717
|
+
const col = this.menuColumn;
|
|
5718
|
+
this.columnMenu().hide();
|
|
5719
|
+
if (!col?.field)
|
|
5720
|
+
return;
|
|
5721
|
+
if (this.columnWrap[col.field])
|
|
5722
|
+
delete this.columnWrap[col.field];
|
|
5723
|
+
else
|
|
5724
|
+
this.columnWrap[col.field] = true;
|
|
5725
|
+
this.persistColumnLayout();
|
|
5726
|
+
}
|
|
5727
|
+
/** Exposed for the template: actions columns carry no field and get no menu. */
|
|
5728
|
+
isDataColumn(col) {
|
|
5729
|
+
return SpiderlyDataTableComponent.isDataColumn(col);
|
|
5730
|
+
}
|
|
5731
|
+
/**
|
|
5732
|
+
* Read off the last REQUEST's sort, not `table._multiSortMeta`. On a first load with a declared
|
|
5733
|
+
* default that field is still null: `applyDefaultSortIfUnsorted` writes the default onto the
|
|
5734
|
+
* lazy-load event, and PrimeNG fills its own meta only once someone clicks a header. Reading the
|
|
5735
|
+
* table there showed no sort on exactly the tables that always have one. A field with no column
|
|
5736
|
+
* left falls back to its own name rather than vanishing.
|
|
5737
|
+
*/
|
|
5738
|
+
refreshSortKeys() {
|
|
5739
|
+
this.sortKeys = (this.lastLazyLoadEvent?.multiSortMeta ?? []).map((key) => ({
|
|
5740
|
+
label: this.cols.find((col) => col.field === key.field)?.name ?? key.field,
|
|
5741
|
+
descending: key.order === -1,
|
|
5742
|
+
}));
|
|
5743
|
+
}
|
|
5744
|
+
/** This column's share, override first, then its declared width, then the per-type default. */
|
|
5745
|
+
columnShare(col) {
|
|
5746
|
+
if (col.field && this.columnWidths[col.field] != null) {
|
|
5747
|
+
return this.columnWidths[col.field];
|
|
4550
5748
|
}
|
|
5749
|
+
if (col.width != null)
|
|
5750
|
+
return this.shareOfDeclaredWidth(col);
|
|
5751
|
+
return this.defaultShare(col);
|
|
5752
|
+
}
|
|
5753
|
+
/**
|
|
5754
|
+
* A declared width expressed in SHARE units. rem IS the share unit and px converts; any other
|
|
5755
|
+
* unit falls back to the per-type default rather than being read as a bare number.
|
|
5756
|
+
*
|
|
5757
|
+
* `parseFloat` alone shipped here first, so `width: '150px'` resolved to share 150 against
|
|
5758
|
+
* neighbours at 12 — one column eating the grid on the first drag or fit. Every declared width
|
|
5759
|
+
* in the consumer happens to be rem today, which is exactly why nothing would have caught it.
|
|
5760
|
+
*/
|
|
5761
|
+
shareOfDeclaredWidth(col) {
|
|
5762
|
+
const declared = col.width.trim();
|
|
5763
|
+
const value = parseFloat(declared);
|
|
5764
|
+
if (Number.isNaN(value))
|
|
5765
|
+
return this.defaultShare(col);
|
|
5766
|
+
if (declared.endsWith('rem'))
|
|
5767
|
+
return value;
|
|
5768
|
+
if (declared.endsWith('px'))
|
|
5769
|
+
return value / rootFontSizePx();
|
|
5770
|
+
return this.defaultShare(col);
|
|
5771
|
+
}
|
|
5772
|
+
defaultShare(col) {
|
|
5773
|
+
if (col.filterType)
|
|
5774
|
+
return DEFAULT_COLUMN_WIDTH_REM[col.filterType];
|
|
5775
|
+
// What is left declares no filterType: an actions column. It cannot shrink to fit any more,
|
|
5776
|
+
// so the icons need a reservation — each sits in a flex row whose gap is set beside them in
|
|
5777
|
+
// the template, inside the cell's own padding. 2.5, not 2.2, so the arithmetic stays exact in
|
|
5778
|
+
// binary and the string never reads `8.6000000001rem`.
|
|
5779
|
+
return 2 + (col.actions?.length ?? 0) * 2.5;
|
|
5780
|
+
}
|
|
5781
|
+
/**
|
|
5782
|
+
* Trades share between the column being dragged and the one on its right, so the table keeps
|
|
5783
|
+
* its total: widening a column never starts a horizontal scroll by itself, which only happens
|
|
5784
|
+
* once the SUM of the minimums stops fitting. The px delta is converted through the column's
|
|
5785
|
+
* own rendered width, which is the only place the share-to-pixel scale is actually known.
|
|
5786
|
+
*/
|
|
5787
|
+
startColumnResize(col, event, th) {
|
|
5788
|
+
event.preventDefault();
|
|
5789
|
+
event.stopPropagation();
|
|
5790
|
+
const neighbour = this.visibleCols[this.visibleCols.indexOf(col) + 1];
|
|
5791
|
+
if (!col.field || !neighbour?.field)
|
|
5792
|
+
return;
|
|
5793
|
+
const controller = new AbortController();
|
|
5794
|
+
this.resizeAbort?.abort();
|
|
5795
|
+
this.resizeAbort = controller;
|
|
5796
|
+
const startX = event.clientX;
|
|
5797
|
+
const startShare = this.columnShare(col);
|
|
5798
|
+
const neighbourShare = this.columnShare(neighbour);
|
|
5799
|
+
const sharePerPx = startShare / Math.max(th.offsetWidth, 1);
|
|
5800
|
+
// A column that reaches zero share disappears with no way back from the header it lost.
|
|
5801
|
+
const floor = 2;
|
|
5802
|
+
const neighbourCell = th.nextElementSibling;
|
|
5803
|
+
let applied = 0;
|
|
5804
|
+
const onMove = (move) => {
|
|
5805
|
+
const delta = (move.clientX - startX) * sharePerPx;
|
|
5806
|
+
applied = Math.max(Math.min(delta, neighbourShare - floor), floor - startShare);
|
|
5807
|
+
// The preview is written straight to the two header cells. Under the fixed layout their
|
|
5808
|
+
// widths are what lay the grid out, so this IS the live drag — and it costs no change
|
|
5809
|
+
// detection, where routing it through `columnWidths` ran a full-grid pass per mouse move
|
|
5810
|
+
// (~60/sec) to move two cells. The bindings overwrite these on the next pass anyway.
|
|
5811
|
+
th.style.width = `${startShare + applied}rem`;
|
|
5812
|
+
if (neighbourCell) {
|
|
5813
|
+
neighbourCell.style.width = `${neighbourShare - applied}rem`;
|
|
5814
|
+
}
|
|
5815
|
+
};
|
|
5816
|
+
const onUp = () => {
|
|
5817
|
+
controller.abort();
|
|
5818
|
+
this.resizeAbort = null;
|
|
5819
|
+
this.zone.run(() => {
|
|
5820
|
+
this.columnWidths[col.field] = startShare + applied;
|
|
5821
|
+
this.columnWidths[neighbour.field] = neighbourShare - applied;
|
|
5822
|
+
this.persistColumnLayout();
|
|
5823
|
+
});
|
|
5824
|
+
// Sorting hangs off CLICK, which stopPropagation on mousedown never touched — so every
|
|
5825
|
+
// resize also reordered the grid. Swallowing the click on the grip is not enough either:
|
|
5826
|
+
// on a drag that ends away from it, the click fires on their common ancestor, the th.
|
|
5827
|
+
// One capturing listener, once, on the document.
|
|
5828
|
+
const swallow = (click) => {
|
|
5829
|
+
click.stopPropagation();
|
|
5830
|
+
click.preventDefault();
|
|
5831
|
+
};
|
|
5832
|
+
document.addEventListener('click', swallow, {
|
|
5833
|
+
capture: true,
|
|
5834
|
+
once: true,
|
|
5835
|
+
});
|
|
5836
|
+
// Removed on the next tick rather than left to `once`. The click that follows a mouseup
|
|
5837
|
+
// fires synchronously, so this always catches it — while a drag that ends with no click at
|
|
5838
|
+
// all (the pointer leaves the window, the operator navigates away) would otherwise leave a
|
|
5839
|
+
// listener sitting on the document to eat someone's next click entirely.
|
|
5840
|
+
setTimeout(() => document.removeEventListener('click', swallow, { capture: true }));
|
|
5841
|
+
};
|
|
5842
|
+
// One controller for the pair, aborted by `onUp` AND by ngOnDestroy. Left to `removeEventListener`
|
|
5843
|
+
// inside `onUp` alone, navigating away mid-drag stranded `mousemove` on the document — patched
|
|
5844
|
+
// by zone.js, so it ticked the app on every mouse move for the rest of the session, with its
|
|
5845
|
+
// closure pinning the dead component and the page of rows it captured.
|
|
5846
|
+
this.zone.runOutsideAngular(() => {
|
|
5847
|
+
document.addEventListener('mousemove', onMove, {
|
|
5848
|
+
signal: controller.signal,
|
|
5849
|
+
});
|
|
5850
|
+
document.addEventListener('mouseup', onUp, { signal: controller.signal });
|
|
5851
|
+
});
|
|
5852
|
+
}
|
|
5853
|
+
/**
|
|
5854
|
+
* The column's declared {@link Column.width}, or the default for its filter type.
|
|
5855
|
+
*
|
|
5856
|
+
* A width, not a minimum: under the fixed layout {@link tableStyle} establishes, this is what
|
|
5857
|
+
* the column is sized from, and the table shares its surplus in PROPORTION to these numbers.
|
|
5858
|
+
* Columns declaring none would otherwise take an equal share, which throws away what the
|
|
5859
|
+
* per-type defaults say — a boolean holds "Da"/"Ne", a text column holds a name.
|
|
5860
|
+
*/
|
|
5861
|
+
getColWidth(col) {
|
|
5862
|
+
const override = col.field ? this.columnWidths[col.field] : undefined;
|
|
5863
|
+
if (override != null)
|
|
5864
|
+
return `${override}rem`;
|
|
5865
|
+
// A declared width passes through VERBATIM so a consumer keeps % and px; every other branch is
|
|
5866
|
+
// a share and renders in rem. Both halves resolve through `columnShare` otherwise, because
|
|
5867
|
+
// this chain and that one were written twice and had already drifted apart on the px case.
|
|
5868
|
+
if (col.width != null)
|
|
5869
|
+
return col.width;
|
|
5870
|
+
return `${this.defaultShare(col)}rem`;
|
|
4551
5871
|
}
|
|
4552
5872
|
/*
|
|
4553
5873
|
* Navigate to details page based on rowId and rowNavigationPath.
|
|
@@ -4647,6 +5967,8 @@ class SpiderlyDataTableComponent {
|
|
|
4647
5967
|
ngOnDestroy() {
|
|
4648
5968
|
this.destroy$.next();
|
|
4649
5969
|
this.destroy$.complete();
|
|
5970
|
+
this.selectionWidthObserver?.disconnect();
|
|
5971
|
+
this.resizeAbort?.abort();
|
|
4650
5972
|
}
|
|
4651
5973
|
get showSelectAllCheckbox() {
|
|
4652
5974
|
return (this.selectionMode === 'multiple' &&
|
|
@@ -4660,9 +5982,37 @@ class SpiderlyDataTableComponent {
|
|
|
4660
5982
|
this.isAllSelected = null;
|
|
4661
5983
|
this.fakeIsAllSelected = false;
|
|
4662
5984
|
}
|
|
5985
|
+
/**
|
|
5986
|
+
* A COMMITTED filter re-queries; a draft does not. Re-fetching per keystroke would be the same
|
|
5987
|
+
* lie the chip bar exists to prevent, only paid for in requests rather than in credibility.
|
|
5988
|
+
*
|
|
5989
|
+
* Back to page one first, and off FRESH metadata rather than `reload()`: that one replays the
|
|
5990
|
+
* cached lazy-load event, whose `first` was copied when the event was built, so a filter applied
|
|
5991
|
+
* from page 3 would keep asking the server to skip 50 rows a narrower result set does not have.
|
|
5992
|
+
* The grid comes back empty for a value that is definitely in it.
|
|
5993
|
+
*
|
|
5994
|
+
* The first run is PrimeNG's: it fires the initial lazy load itself, and re-querying here would
|
|
5995
|
+
* make every table load twice.
|
|
5996
|
+
*/
|
|
5997
|
+
requeryOnAppliedFilters() {
|
|
5998
|
+
let isFirstRun = true;
|
|
5999
|
+
effect(() => {
|
|
6000
|
+
this.filters.applied();
|
|
6001
|
+
if (isFirstRun) {
|
|
6002
|
+
isFirstRun = false;
|
|
6003
|
+
return;
|
|
6004
|
+
}
|
|
6005
|
+
this.persistAppliedFilters();
|
|
6006
|
+
this.table.first = 0;
|
|
6007
|
+
this.table.firstChange.emit(0);
|
|
6008
|
+
this.lazyLoad(this.table.createLazyLoadMetadata());
|
|
6009
|
+
}, { injector: this.injector });
|
|
6010
|
+
}
|
|
4663
6011
|
reload() {
|
|
4664
|
-
|
|
4665
|
-
this.
|
|
6012
|
+
// Nothing to replay before the table's own initial load, which is already on its way.
|
|
6013
|
+
if (this.lastLazyLoadEvent == null)
|
|
6014
|
+
return;
|
|
6015
|
+
// Not special: same refetch as a page flip, so lazyLoad owns both the flag and `items`.
|
|
4666
6016
|
this.lazyLoad(this.lastLazyLoadEvent);
|
|
4667
6017
|
}
|
|
4668
6018
|
showActions() {
|
|
@@ -4717,9 +6067,8 @@ class SpiderlyDataTableComponent {
|
|
|
4717
6067
|
const local = typeof raw === 'string' ? parseDateOnlyLocal(raw) : null;
|
|
4718
6068
|
return formatDate(local ?? raw, 'shortDate', this.locale);
|
|
4719
6069
|
case 'multiselect': {
|
|
4720
|
-
// Translate through the column's own option list
|
|
4721
|
-
//
|
|
4722
|
-
// column reads `1` while its filter offers "Info", and an FK column reads the id.
|
|
6070
|
+
// Translate through the column's own option list. Without this the cell shows the stored
|
|
6071
|
+
// value, so an enum column reads `1` instead of "Info", and an FK column reads the id.
|
|
4723
6072
|
//
|
|
4724
6073
|
// Match on `code`, not `value`: that is the property PrimengOption carries (see the note
|
|
4725
6074
|
// on the class). The raw value is the fallback rather than an empty cell, because the
|
|
@@ -4758,7 +6107,13 @@ class SpiderlyDataTableComponent {
|
|
|
4758
6107
|
filter.additionalFilterIdLong = this.additionalFilterIdLong;
|
|
4759
6108
|
exportListToExcel(this.exportListToExcelObservableMethod, filter);
|
|
4760
6109
|
}
|
|
6110
|
+
/**
|
|
6111
|
+
* Clears everything that narrows the grid. With a store supplied it has to reach BOTH — the
|
|
6112
|
+
* store holds the constraints and PrimeNG holds the persisted state and sort — or the bar goes
|
|
6113
|
+
* empty while a reload brings the old filters back.
|
|
6114
|
+
*/
|
|
4761
6115
|
clear(table) {
|
|
6116
|
+
this.filters?.clear();
|
|
4762
6117
|
table.clear();
|
|
4763
6118
|
table.clearState();
|
|
4764
6119
|
}
|
|
@@ -4933,8 +6288,8 @@ class SpiderlyDataTableComponent {
|
|
|
4933
6288
|
return null;
|
|
4934
6289
|
}
|
|
4935
6290
|
}
|
|
4936
|
-
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: LOCALE_ID }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
4937
|
-
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 [class]=\"\n showCardWrapper ? 'card responsive-card-padding overflow-auto' : ''\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 [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"items === undefined || loading === true\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n (onFilter)=\"filter($event)\"\n sortMode=\"multiple\"\n [multiSortMeta]=\"initialMultiSortMeta\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\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 <th style=\"width: 0rem\" *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]=\"getColHeaderWidth(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 <p-columnFilter\n *ngIf=\"col.filterType != null && col.filterType !== 'blob'\"\n [type]=\"col.filterType\"\n [field]=\"col.filterField ?? col.field\"\n display=\"menu\"\n [placeholder]=\"col.filterPlaceholder\"\n [showOperator]=\"false\"\n [showMatchModes]=\"col.showMatchModes\"\n [showAddButton]=\"col.showAddButton\"\n [matchModeOptions]=\"getColMatchModeOptions(col.filterType)\"\n [matchMode]=\"getColMatchMode(col.filterType)\"\n >\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 {{ displayValue }}\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 <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=\"loadingbody\">\n <tr>\n <td\n [attr.colspan]=\"\n visibleCols.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"Loading\") }}...\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}@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 ::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 ::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.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"] }] }); }
|
|
6291
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyDataTableComponent, deps: [{ token: i3$2.Router }, { token: i1$5.DialogService }, { token: i3$2.ActivatedRoute }, { token: SpiderlyMessageService }, { token: i1.TranslocoService }, { token: ConfigServiceBase }, { token: i0.ErrorHandler }, { token: LOCALE_ID }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6292
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.2.13", type: SpiderlyDataTableComponent, isStandalone: true, selector: "spiderly-data-table", inputs: { tableTitle: "tableTitle", tableIcon: "tableIcon", items: "items", rows: "rows", rowsPerPageOptions: "rowsPerPageOptions", cols: "cols", filters: "filters", views: "views", showPaginator: "showPaginator", showCardWrapper: "showCardWrapper", readonly: "readonly", idField: "idField", getPaginatedListObservableMethod: "getPaginatedListObservableMethod", exportListToExcelObservableMethod: "exportListToExcelObservableMethod", deleteItemFromTableObservableMethod: "deleteItemFromTableObservableMethod", deleteListFromTableObservableMethod: "deleteListFromTableObservableMethod", newlySelectedItems: "newlySelectedItems", unselectedItems: "unselectedItems", selectionMode: "selectionMode", selectedLazyLoadObservableMethod: "selectedLazyLoadObservableMethod", additionalFilterIdLong: "additionalFilterIdLong", showAddButton: "showAddButton", showExportToExcelButton: "showExportToExcelButton", showReloadTableButton: "showReloadTableButton", getFormArrayItems: "getFormArrayItems", hasLazyLoad: "hasLazyLoad", stateKey: "stateKey", stateStorage: "stateStorage", defaultSortField: "defaultSortField", defaultSortOrder: "defaultSortOrder", getAlreadySelectedItemIds: "getAlreadySelectedItemIds", getAlreadySelectedItems: "getAlreadySelectedItems", getFormControl: "getFormControl", additionalIndexes: "additionalIndexes", navigateOnRowClick: "navigateOnRowClick", rowNavigationPath: "rowNavigationPath" }, outputs: { onTotalRecordsChange: "onTotalRecordsChange", onLazyLoad: "onLazyLoad", onIsAllSelectedChange: "onIsAllSelectedChange", onRowSelect: "onRowSelect", onRowUnselect: "onRowUnselect" }, queries: [{ propertyName: "actionsTemplate", first: true, predicate: SpiderlyDataTableActionsDirective, descendants: true, read: TemplateRef }, { propertyName: "cellTemplates", predicate: SpiderlyCellTemplateDirective }, { propertyName: "filterTemplates", predicate: SpiderlyFilterTemplateDirective }], viewQueries: [{ propertyName: "columnMenu", first: true, predicate: ["columnMenu"], descendants: true, isSignal: true }, { propertyName: "filterBar", first: true, predicate: SpiderlyFilterBarComponent, descendants: true, isSignal: true }, { propertyName: "selectionHeader", first: true, predicate: ["selectionHeader"], descendants: true, isSignal: true }, { propertyName: "table", first: true, predicate: ["dt"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div\n #tableContainer\n class=\"spiderly-table-container\"\n [attr.aria-busy]=\"isPending\"\n [ngClass]=\"{\n 'card responsive-card-padding overflow-auto': showCardWrapper\n }\"\n >\n <p-table\n #dt\n [value]=\"items\"\n [rows]=\"rows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [rowHover]=\"true\"\n [paginator]=\"showPaginator\"\n responsiveLayout=\"scroll\"\n [lazy]=\"hasLazyLoad\"\n (onLazyLoad)=\"lazyLoad($event)\"\n (onPage)=\"onPageChange(tableContainer)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"isPending\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n sortMode=\"multiple\"\n [multiSortMeta]=\"initialMultiSortMeta\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\n [tableStyle]=\"tableStyle\"\n >\n <ng-template pTemplate=\"caption\">\n <div class=\"table-header overflow-auto\">\n <div style=\"display: flex; align-items: center; gap: 8px\">\n <i class=\"{{ tableIcon }}\" style=\"font-size: 20px\"></i>\n <div style=\"margin: 0px; font-size: 17.5px\">{{ tableTitle }}</div>\n </div>\n <div style=\"display: flex; gap: 8px\">\n <ng-container\n *ngIf=\"actionsTemplate\"\n [ngTemplateOutlet]=\"actionsTemplate\"\n ></ng-container>\n <button\n pButton\n *ngIf=\"hasLazyLoad\"\n data-testid=\"column-chooser-button\"\n [label]=\"t('Columns')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-sliders-h\"\n (click)=\"columnChooser.toggle($event)\"\n ></button>\n <button\n pButton\n *ngIf=\"showExportToExcelButton\"\n [label]=\"t('ExportToExcel')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-download\"\n (click)=\"exportListToExcel()\"\n ></button>\n <button\n pButton\n *ngIf=\"showReloadTableButton\"\n [label]=\"t('Reload')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-refresh\"\n (click)=\"reload()\"\n ></button>\n <button\n pButton\n *ngIf=\"deleteListFromTableObservableMethod && rowsSelectedNumber > 0\"\n [label]=\"t('DeleteSelected') + ' (' + rowsSelectedNumber + ')'\"\n class=\"p-button-danger\"\n style=\"flex: none\"\n icon=\"pi pi-trash\"\n (click)=\"deleteSelectedObjects()\"\n ></button>\n </div>\n </div>\n <!-- ONE menu for every column, with the target held in `menuColumn`. Rendering its items\n inside an @if keyed on the column would put them inside the popover's projected\n content, and PrimeNG does not re-project content an @if has destroyed \u2014 the filter\n bar's editor lost a whole afternoon to exactly that. -->\n <p-popover #columnMenu>\n <div class=\"column-menu\" role=\"menu\">\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-hide\"\n [disabled]=\"!menuColumn || !canToggleColumn(menuColumn)\"\n (click)=\"hideMenuColumn()\"\n >\n <i class=\"pi pi-eye-slash\"></i>\n <span class=\"column-menu-item-label\">{{ t('HideColumn') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-filter\"\n [disabled]=\"!menuColumn || !canFilterColumn(menuColumn)\"\n (click)=\"filterMenuColumn()\"\n >\n <i class=\"pi pi-filter\"></i>\n <span class=\"column-menu-item-label\">{{ t('Filter') }}\u2026</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-asc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(1)\"\n >\n <i class=\"pi pi-sort-amount-up-alt\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortAscending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-desc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(-1)\"\n >\n <i class=\"pi pi-sort-amount-down\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortDescending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-left\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, -1)\"\n (click)=\"moveMenuColumn(-1)\"\n >\n <i class=\"pi pi-arrow-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveLeft') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-right\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, 1)\"\n (click)=\"moveMenuColumn(1)\"\n >\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveRight') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-fit\"\n (click)=\"fitMenuColumn()\"\n >\n <i class=\"pi pi-arrows-h\"></i>\n <span class=\"column-menu-item-label\">{{ t('FitWidth') }}</span>\n </button>\n <!-- menuitemcheckbox, not menuitem: this one TOGGLES, and a label reading the same\n in both states leaves the operator to find out by clicking \u2014 which on a wide\n column means re-laying the grid out to learn what it was already set to. The\n check is the state; the label stays put, which is what the role is for.\n Disabled for an editable column: its cell holds an input, not clamped text, so\n the toggle could only persist a preference that changes nothing. -->\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-wrap\"\n [disabled]=\"!menuColumn || menuColumn.editable === true\"\n [attr.aria-checked]=\"isMenuColumnWrapped\"\n (click)=\"toggleWrapForMenuColumn()\"\n >\n <i class=\"pi pi-align-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('WrapText') }}</span>\n <i *ngIf=\"isMenuColumnWrapped\" class=\"pi pi-check\"></i>\n </button>\n </div>\n </p-popover>\n\n <div class=\"table-views\" *ngIf=\"views?.length\" role=\"tablist\">\n <button\n *ngFor=\"let view of views\"\n type=\"button\"\n role=\"tab\"\n class=\"table-view\"\n data-testid=\"table-view\"\n [class.table-view-active]=\"activeViewId === view.id\"\n [attr.aria-selected]=\"activeViewId === view.id\"\n (click)=\"selectView(view)\"\n >\n {{ view.label }}\n </button>\n </div>\n\n <spiderly-filter-bar\n *ngIf=\"filters\"\n [filters]=\"filters\"\n [totalRecords]=\"totalRecords\"\n [sort]=\"sortKeys\"\n [controlTemplates]=\"filterTemplates\"\n (clearAll)=\"clear(dt)\"\n ></spiderly-filter-bar>\n </ng-template>\n <ng-template pTemplate=\"header\">\n <tr>\n <!-- Sized for the checkbox plus the counter beside it, which shows `totalRecords` on a\n select-all and so reaches five digits on a large table. Not `0rem` (see CLAUDE.md \u2192\n Column widths), and not routed through getColWidth: this column has no `Column`. -->\n <th\n #selectionHeader\n class=\"frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.width]=\"selectionColumnWidth\"\n [style.left]=\"'0'\"\n *ngIf=\"selectionMode == 'multiple'\"\n >\n <div style=\"display: flex; gap: 8px\">\n <p-checkbox\n *ngIf=\"showSelectAllCheckbox\"\n [disabled]=\"readonly\"\n (onChange)=\"selectAll($event.checked)\"\n [(ngModel)]=\"fakeIsAllSelected\"\n [binary]=\"true\"\n ></p-checkbox>\n <span class=\"selection-count\">({{ rowsSelectedNumber }})</span>\n </div>\n </th>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <th\n #headerCell\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"!isColumnSortable(col)\"\n [style.width]=\"getColWidth(col)\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n [attr.draggable]=\"hasLazyLoad && !col.lockVisible ? true : null\"\n (mousedown)=\"onHeaderMouseDown($event)\"\n (dragstart)=\"onHeaderDragStart(col, $event)\"\n (dragover)=\"onHeaderDragOver(col, $event)\"\n (drop)=\"onHeaderDrop(col, $event)\"\n >\n <!-- Ours, not PrimeNG's: enabling [resizableColumns] makes it persist widths by\n th:nth-child INDEX into our own stateKey, which slides every stored width onto\n the wrong column the first time the chooser hides one (CLAUDE.md -> decision\n 5). We take the gesture and keep the state. -->\n <span\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n class=\"column-resizer\"\n data-testid=\"column-resizer\"\n (mousedown)=\"startColumnResize(col, $event, headerCell)\"\n ></span>\n <div\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n \"\n >\n <span style=\"display: flex; align-items: center; gap: 4px\">\n {{ col.name }}\n <p-sortIcon\n *ngIf=\"isColumnSortable(col)\"\n [field]=\"col.field!\"\n ></p-sortIcon>\n <!-- stopPropagation is load-bearing: the th carries pSortableColumn, so a click\n that reaches it sorts the grid. Opening a menu must not reorder the table on\n the way. -->\n <button\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n type=\"button\"\n class=\"column-menu-button\"\n data-testid=\"column-menu\"\n [attr.aria-label]=\"t('Columns')\"\n (click)=\"$event.stopPropagation(); openColumnMenu(col, $event)\"\n >\n <i class=\"pi pi-chevron-down\"></i>\n </button>\n </span>\n </div>\n </th>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template\n pTemplate=\"body\"\n let-rowData\n let-index=\"rowIndex\"\n let-editing=\"editing\"\n >\n <!-- The cell rendering every column gets unless it projects a spiderlyCellTemplate. Declared\n once here rather than inside the column loop, and fed the same context the projected\n template gets, so getRowData is evaluated exactly once per cell either way. -->\n <ng-template #defaultCell let-col=\"col\" let-displayValue=\"displayValue\">\n <ng-container *ngIf=\"col.filterType === 'blob'\">\n <img width=\"45\" [src]=\"displayValue\" alt=\"\" />\n </ng-container>\n <ng-container *ngIf=\"col.filterType !== 'blob'\">\n <span spiderlyOverflowTitle class=\"cell-text\">{{\n displayValue\n }}</span>\n </ng-container>\n </ng-template>\n <tr\n [class.clickable]=\"navigateOnRowClick\"\n (click)=\"onRowClick(rowData, $event)\"\n >\n <!-- row-interactive: a selection gesture must never double as row navigation (onRowClick). -->\n <td\n *ngIf=\"selectionMode == 'multiple'\"\n class=\"selection-cell row-interactive frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.left]=\"'0'\"\n (mousedown)=\"onSelectionCellMouseDown($event, rowData[idField])\"\n >\n <p-checkbox\n [disabled]=\"readonly\"\n (onChange)=\"selectRow(rowData[idField], rowData.index)\"\n [ngModel]=\"isRowSelected(rowData[idField])\"\n [binary]=\"true\"\n ></p-checkbox>\n </td>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <td\n [style]=\"getStyleForBodyColumn(col)\"\n [class.clickable]=\"col.onCellClick\"\n [class.row-interactive]=\"col.onCellClick\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [class.cell-wrap]=\"isColumnWrapped(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n (click)=\"onCellClick(col, rowData, $event)\"\n *ngIf=\"!col.editable\"\n >\n <!-- getColWidth reserves this column's width from `actions.length`; this gap is\n the geometry it reserves for, so changing it belongs in the same edit. -->\n <div\n style=\"\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 18px;\n \"\n >\n <ng-container\n *ngFor=\"let action of col.actions; trackBy: actionTrackByFn\"\n >\n <span\n [pTooltip]=\"action.name\"\n [class]=\"getClassForAction(action) + ' row-interactive'\"\n [style]=\"getStyleForAction(action)\"\n (click)=\"getMethodForAction(action, rowData, $event)\"\n ></span>\n </ng-container>\n </div>\n <ng-container\n [ngTemplateOutlet]=\"getCellTemplate(col) ?? defaultCell\"\n [ngTemplateOutletContext]=\"{\n $implicit: rowData,\n value: rowData[col.field],\n displayValue: getRowData(rowData, col),\n col: col\n }\"\n ></ng-container>\n </td>\n <td\n *ngIf=\"col.editable\"\n class=\"row-interactive\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n >\n <spiderly-number\n [control]=\"getFormArrayControlByIndex(col.field, rowData.index)\"\n [showLabel]=\"false\"\n ></spiderly-number>\n </td>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n <tr>\n <td\n [attr.colspan]=\"\n visibleCols.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"NoRecordsFound\") }}\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template pTemplate=\"paginatorright\">\n <div style=\"display: flex; justify-content: end; gap: 10px\">\n <spiderly-button\n *ngIf=\"showAddButton\"\n [label]=\"t('AddNew')\"\n icon=\"pi pi-plus\"\n (onClick)=\"navigateToDetails(0)\"\n ></spiderly-button>\n </div>\n </ng-template>\n </p-table>\n <p-popover #columnChooser>\n <div class=\"column-chooser\">\n <label\n *ngFor=\"let col of chooserCols; trackBy: colTrackByFn\"\n data-testid=\"column-chooser-option\"\n class=\"column-chooser-option\"\n >\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isColumnVisible(col)\"\n [disabled]=\"!canToggleColumn(col)\"\n (onChange)=\"toggleColumn(col, $event.checked)\"\n ></p-checkbox>\n <span>{{ col.name }}</span>\n </label>\n <button\n type=\"button\"\n data-testid=\"column-chooser-reset\"\n class=\"column-chooser-reset\"\n (click)=\"resetColumnLayout()\"\n >\n {{ t('ResetToDefault') }}\n </button>\n </div>\n </p-popover>\n </div>\n</ng-container>\n", styles: [".table-header{display:flex;justify-content:space-between;align-items:center}@media (max-width: 640px){.table-header{display:flex;flex-direction:column;align-items:start;gap:14px}}.spiderly-table .p-paginator{padding:1rem}@media (min-width: 1400px){.spiderly-table .p-paginator-left-content{position:absolute;padding:14px;left:0}}@media (min-width: 1400px){.spiderly-table .p-paginator-right-content{position:absolute;padding:14px;right:0}}.column-chooser{display:flex;flex-direction:column;gap:8px}.column-chooser-option{display:flex;align-items:center;gap:8px;cursor:pointer}.column-chooser-reset{border:none;background:none;color:inherit;font:inherit;cursor:pointer;padding:0;margin-top:4px;color:var(--p-primary-color);align-self:flex-start;font-size:.875rem}.column-chooser-reset:hover{text-decoration:underline}.column-menu{display:flex;flex-direction:column;min-width:11rem}.column-menu-item{display:flex;align-items:center;gap:8px;border:none;background:none;color:inherit;font:inherit;cursor:pointer;text-align:left;padding:6px 10px;border-radius:var(--p-content-border-radius)}.column-menu-item .column-menu-item-label{flex:1}.column-menu-item:hover:not(:disabled){background:var(--p-content-hover-background)}.column-menu-item:disabled{color:var(--p-text-muted-color);cursor:default}:host .spiderly-table-container{scroll-margin-top:calc(var(--spiderly-viewport-top-inset, 0px) + 1rem)}:host ::ng-deep .clickable{cursor:pointer}:host ::ng-deep td.clickable:hover{text-decoration:underline}:host .column-menu-button{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:0 2px;opacity:0;font-size:.75rem}:host th:hover .column-menu-button,:host .column-menu-button:focus-visible{opacity:1}:host .table-views{display:flex;flex-wrap:wrap;gap:4px;padding-top:8px}:host .table-view{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:4px 10px;border-radius:var(--p-content-border-radius)}:host .table-view:hover{background:var(--p-content-hover-background)}:host .table-view-active{color:var(--p-primary-color);font-weight:600;background:var(--p-content-hover-background)}:host .frozen-column{position:sticky;z-index:1}:host td.frozen-column{background:var(--p-datatable-row-background, var(--p-content-background));transition:background-color var(--p-transition-duration, .2s)}:host tr:hover>td.frozen-column{background:var(--p-datatable-row-hover-background, inherit)}:host .frozen-column-last{box-shadow:2px 0 4px -2px #00000026}:host th{position:relative}:host .column-resizer{position:absolute;top:0;right:0;width:8px;height:100%;cursor:col-resize;-webkit-user-select:none;user-select:none}:host .selection-count{font-size:.75rem;color:var(--p-text-muted-color)}:host td.selection-cell{-webkit-user-select:none;user-select:none}:host td.cell-wrap{--spiderly-cell-overflow: visible;--spiderly-cell-white-space: normal}:host .cell-text{display:block;overflow:var(--spiderly-cell-overflow, hidden);text-overflow:ellipsis;white-space:var(--spiderly-cell-white-space, nowrap)}:host ::ng-deep .p-datatable-mask{background:color-mix(in srgb,var(--p-content-background, #ffffff) 65%,transparent);color:var(--p-primary-color, #334155);align-items:flex-start;padding-top:6rem}:host ::ng-deep .p-datatable-thead{position:unset!important}:host ::ng-deep .p-datatable-header{border-radius:6px 6px 0 0}:host ::ng-deep .p-datatable{border-radius:var(--p-content-border-radius);border:1px solid var(--p-datatable-border-color)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: SpiderlyControlsModule }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "component", type: SpiderlyNumberComponent, selector: "spiderly-number", inputs: ["prefix", "showButtons", "decimal", "maxFractionDigits"] }, { kind: "ngmodule", type: TableModule }, { kind: "component", type: i10.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "style", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "scrollDirection", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "responsive", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "autoLayout", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "virtualRowHeight", "selectAll"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "directive", type: i1$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "directive", type: i10.SortableColumn, selector: "[pSortableColumn]", inputs: ["pSortableColumn", "pSortableColumnDisabled"] }, { kind: "component", type: i10.SortIcon, selector: "p-sortIcon", inputs: ["field"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3$3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "fluid", "label", "icon", "buttonProps"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i8.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["value", "name", "disabled", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "inputStyle", "styleClass", "inputClass", "indeterminate", "size", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "component", type: SpiderlyFilterBarComponent, selector: "spiderly-filter-bar", inputs: ["filters", "totalRecords", "sort", "controlTemplates"], outputs: ["clearAll"] }, { kind: "directive", type: SpiderlyOverflowTitleDirective, selector: "[spiderlyOverflowTitle]" }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }] }); }
|
|
4938
6293
|
}
|
|
4939
6294
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyDataTableComponent, decorators: [{
|
|
4940
6295
|
type: Component,
|
|
@@ -4945,13 +6300,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
4945
6300
|
SpiderlyControlsModule,
|
|
4946
6301
|
TableModule,
|
|
4947
6302
|
ButtonModule,
|
|
4948
|
-
MultiSelectModule,
|
|
4949
6303
|
PopoverModule,
|
|
4950
|
-
DatePickerModule,
|
|
4951
6304
|
CheckboxModule,
|
|
6305
|
+
SpiderlyFilterBarComponent,
|
|
6306
|
+
SpiderlyOverflowTitleDirective,
|
|
4952
6307
|
TooltipModule,
|
|
4953
|
-
], template: "<ng-container *transloco=\"let t\">\n <div\n [class]=\"\n showCardWrapper ? 'card responsive-card-padding overflow-auto' : ''\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 [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"items === undefined || loading === true\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n (onFilter)=\"filter($event)\"\n sortMode=\"multiple\"\n [multiSortMeta]=\"initialMultiSortMeta\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\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 <th style=\"width: 0rem\" *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]=\"getColHeaderWidth(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 <p-columnFilter\n *ngIf=\"col.filterType != null && col.filterType !== 'blob'\"\n [type]=\"col.filterType\"\n [field]=\"col.filterField ?? col.field\"\n display=\"menu\"\n [placeholder]=\"col.filterPlaceholder\"\n [showOperator]=\"false\"\n [showMatchModes]=\"col.showMatchModes\"\n [showAddButton]=\"col.showAddButton\"\n [matchModeOptions]=\"getColMatchModeOptions(col.filterType)\"\n [matchMode]=\"getColMatchMode(col.filterType)\"\n >\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 {{ displayValue }}\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 <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=\"loadingbody\">\n <tr>\n <td\n [attr.colspan]=\"\n visibleCols.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"Loading\") }}...\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}@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 ::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 ::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"] }]
|
|
4954
|
-
}], ctorParameters: () => [{ type: i3$2.Router }, { type: i1$5.DialogService }, { type: i3$2.ActivatedRoute }, { type: SpiderlyMessageService }, { type: i1.TranslocoService }, { type: ConfigServiceBase }, { type: undefined, decorators: [{
|
|
6308
|
+
], template: "<ng-container *transloco=\"let t\">\n <div\n #tableContainer\n class=\"spiderly-table-container\"\n [attr.aria-busy]=\"isPending\"\n [ngClass]=\"{\n 'card responsive-card-padding overflow-auto': showCardWrapper\n }\"\n >\n <p-table\n #dt\n [value]=\"items\"\n [rows]=\"rows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [rowHover]=\"true\"\n [paginator]=\"showPaginator\"\n responsiveLayout=\"scroll\"\n [lazy]=\"hasLazyLoad\"\n (onLazyLoad)=\"lazyLoad($event)\"\n (onPage)=\"onPageChange(tableContainer)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"isPending\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n sortMode=\"multiple\"\n [multiSortMeta]=\"initialMultiSortMeta\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\n [tableStyle]=\"tableStyle\"\n >\n <ng-template pTemplate=\"caption\">\n <div class=\"table-header overflow-auto\">\n <div style=\"display: flex; align-items: center; gap: 8px\">\n <i class=\"{{ tableIcon }}\" style=\"font-size: 20px\"></i>\n <div style=\"margin: 0px; font-size: 17.5px\">{{ tableTitle }}</div>\n </div>\n <div style=\"display: flex; gap: 8px\">\n <ng-container\n *ngIf=\"actionsTemplate\"\n [ngTemplateOutlet]=\"actionsTemplate\"\n ></ng-container>\n <button\n pButton\n *ngIf=\"hasLazyLoad\"\n data-testid=\"column-chooser-button\"\n [label]=\"t('Columns')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-sliders-h\"\n (click)=\"columnChooser.toggle($event)\"\n ></button>\n <button\n pButton\n *ngIf=\"showExportToExcelButton\"\n [label]=\"t('ExportToExcel')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-download\"\n (click)=\"exportListToExcel()\"\n ></button>\n <button\n pButton\n *ngIf=\"showReloadTableButton\"\n [label]=\"t('Reload')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-refresh\"\n (click)=\"reload()\"\n ></button>\n <button\n pButton\n *ngIf=\"deleteListFromTableObservableMethod && rowsSelectedNumber > 0\"\n [label]=\"t('DeleteSelected') + ' (' + rowsSelectedNumber + ')'\"\n class=\"p-button-danger\"\n style=\"flex: none\"\n icon=\"pi pi-trash\"\n (click)=\"deleteSelectedObjects()\"\n ></button>\n </div>\n </div>\n <!-- ONE menu for every column, with the target held in `menuColumn`. Rendering its items\n inside an @if keyed on the column would put them inside the popover's projected\n content, and PrimeNG does not re-project content an @if has destroyed \u2014 the filter\n bar's editor lost a whole afternoon to exactly that. -->\n <p-popover #columnMenu>\n <div class=\"column-menu\" role=\"menu\">\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-hide\"\n [disabled]=\"!menuColumn || !canToggleColumn(menuColumn)\"\n (click)=\"hideMenuColumn()\"\n >\n <i class=\"pi pi-eye-slash\"></i>\n <span class=\"column-menu-item-label\">{{ t('HideColumn') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-filter\"\n [disabled]=\"!menuColumn || !canFilterColumn(menuColumn)\"\n (click)=\"filterMenuColumn()\"\n >\n <i class=\"pi pi-filter\"></i>\n <span class=\"column-menu-item-label\">{{ t('Filter') }}\u2026</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-asc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(1)\"\n >\n <i class=\"pi pi-sort-amount-up-alt\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortAscending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-sort-desc\"\n [disabled]=\"!menuColumn || !isColumnSortable(menuColumn)\"\n (click)=\"sortMenuColumn(-1)\"\n >\n <i class=\"pi pi-sort-amount-down\"></i>\n <span class=\"column-menu-item-label\">{{ t('SortDescending') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-left\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, -1)\"\n (click)=\"moveMenuColumn(-1)\"\n >\n <i class=\"pi pi-arrow-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveLeft') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-right\"\n [disabled]=\"!menuColumn || !canMoveColumn(menuColumn, 1)\"\n (click)=\"moveMenuColumn(1)\"\n >\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"column-menu-item-label\">{{ t('MoveRight') }}</span>\n </button>\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-fit\"\n (click)=\"fitMenuColumn()\"\n >\n <i class=\"pi pi-arrows-h\"></i>\n <span class=\"column-menu-item-label\">{{ t('FitWidth') }}</span>\n </button>\n <!-- menuitemcheckbox, not menuitem: this one TOGGLES, and a label reading the same\n in both states leaves the operator to find out by clicking \u2014 which on a wide\n column means re-laying the grid out to learn what it was already set to. The\n check is the state; the label stays put, which is what the role is for.\n Disabled for an editable column: its cell holds an input, not clamped text, so\n the toggle could only persist a preference that changes nothing. -->\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n class=\"column-menu-item\"\n data-testid=\"column-menu-wrap\"\n [disabled]=\"!menuColumn || menuColumn.editable === true\"\n [attr.aria-checked]=\"isMenuColumnWrapped\"\n (click)=\"toggleWrapForMenuColumn()\"\n >\n <i class=\"pi pi-align-left\"></i>\n <span class=\"column-menu-item-label\">{{ t('WrapText') }}</span>\n <i *ngIf=\"isMenuColumnWrapped\" class=\"pi pi-check\"></i>\n </button>\n </div>\n </p-popover>\n\n <div class=\"table-views\" *ngIf=\"views?.length\" role=\"tablist\">\n <button\n *ngFor=\"let view of views\"\n type=\"button\"\n role=\"tab\"\n class=\"table-view\"\n data-testid=\"table-view\"\n [class.table-view-active]=\"activeViewId === view.id\"\n [attr.aria-selected]=\"activeViewId === view.id\"\n (click)=\"selectView(view)\"\n >\n {{ view.label }}\n </button>\n </div>\n\n <spiderly-filter-bar\n *ngIf=\"filters\"\n [filters]=\"filters\"\n [totalRecords]=\"totalRecords\"\n [sort]=\"sortKeys\"\n [controlTemplates]=\"filterTemplates\"\n (clearAll)=\"clear(dt)\"\n ></spiderly-filter-bar>\n </ng-template>\n <ng-template pTemplate=\"header\">\n <tr>\n <!-- Sized for the checkbox plus the counter beside it, which shows `totalRecords` on a\n select-all and so reaches five digits on a large table. Not `0rem` (see CLAUDE.md \u2192\n Column widths), and not routed through getColWidth: this column has no `Column`. -->\n <th\n #selectionHeader\n class=\"frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.width]=\"selectionColumnWidth\"\n [style.left]=\"'0'\"\n *ngIf=\"selectionMode == 'multiple'\"\n >\n <div style=\"display: flex; gap: 8px\">\n <p-checkbox\n *ngIf=\"showSelectAllCheckbox\"\n [disabled]=\"readonly\"\n (onChange)=\"selectAll($event.checked)\"\n [(ngModel)]=\"fakeIsAllSelected\"\n [binary]=\"true\"\n ></p-checkbox>\n <span class=\"selection-count\">({{ rowsSelectedNumber }})</span>\n </div>\n </th>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <th\n #headerCell\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"!isColumnSortable(col)\"\n [style.width]=\"getColWidth(col)\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n [attr.draggable]=\"hasLazyLoad && !col.lockVisible ? true : null\"\n (mousedown)=\"onHeaderMouseDown($event)\"\n (dragstart)=\"onHeaderDragStart(col, $event)\"\n (dragover)=\"onHeaderDragOver(col, $event)\"\n (drop)=\"onHeaderDrop(col, $event)\"\n >\n <!-- Ours, not PrimeNG's: enabling [resizableColumns] makes it persist widths by\n th:nth-child INDEX into our own stateKey, which slides every stored width onto\n the wrong column the first time the chooser hides one (CLAUDE.md -> decision\n 5). We take the gesture and keep the state. -->\n <span\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n class=\"column-resizer\"\n data-testid=\"column-resizer\"\n (mousedown)=\"startColumnResize(col, $event, headerCell)\"\n ></span>\n <div\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n \"\n >\n <span style=\"display: flex; align-items: center; gap: 4px\">\n {{ col.name }}\n <p-sortIcon\n *ngIf=\"isColumnSortable(col)\"\n [field]=\"col.field!\"\n ></p-sortIcon>\n <!-- stopPropagation is load-bearing: the th carries pSortableColumn, so a click\n that reaches it sorts the grid. Opening a menu must not reorder the table on\n the way. -->\n <button\n *ngIf=\"hasLazyLoad && isDataColumn(col)\"\n type=\"button\"\n class=\"column-menu-button\"\n data-testid=\"column-menu\"\n [attr.aria-label]=\"t('Columns')\"\n (click)=\"$event.stopPropagation(); openColumnMenu(col, $event)\"\n >\n <i class=\"pi pi-chevron-down\"></i>\n </button>\n </span>\n </div>\n </th>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template\n pTemplate=\"body\"\n let-rowData\n let-index=\"rowIndex\"\n let-editing=\"editing\"\n >\n <!-- The cell rendering every column gets unless it projects a spiderlyCellTemplate. Declared\n once here rather than inside the column loop, and fed the same context the projected\n template gets, so getRowData is evaluated exactly once per cell either way. -->\n <ng-template #defaultCell let-col=\"col\" let-displayValue=\"displayValue\">\n <ng-container *ngIf=\"col.filterType === 'blob'\">\n <img width=\"45\" [src]=\"displayValue\" alt=\"\" />\n </ng-container>\n <ng-container *ngIf=\"col.filterType !== 'blob'\">\n <span spiderlyOverflowTitle class=\"cell-text\">{{\n displayValue\n }}</span>\n </ng-container>\n </ng-template>\n <tr\n [class.clickable]=\"navigateOnRowClick\"\n (click)=\"onRowClick(rowData, $event)\"\n >\n <!-- row-interactive: a selection gesture must never double as row navigation (onRowClick). -->\n <td\n *ngIf=\"selectionMode == 'multiple'\"\n class=\"selection-cell row-interactive frozen-column\"\n [class.frozen-column-last]=\"isSelectionColumnLastFrozen\"\n [style.left]=\"'0'\"\n (mousedown)=\"onSelectionCellMouseDown($event, rowData[idField])\"\n >\n <p-checkbox\n [disabled]=\"readonly\"\n (onChange)=\"selectRow(rowData[idField], rowData.index)\"\n [ngModel]=\"isRowSelected(rowData[idField])\"\n [binary]=\"true\"\n ></p-checkbox>\n </td>\n <ng-container *ngFor=\"let col of visibleCols; trackBy: colTrackByFn\">\n <td\n [style]=\"getStyleForBodyColumn(col)\"\n [class.clickable]=\"col.onCellClick\"\n [class.row-interactive]=\"col.onCellClick\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [class.cell-wrap]=\"isColumnWrapped(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n (click)=\"onCellClick(col, rowData, $event)\"\n *ngIf=\"!col.editable\"\n >\n <!-- getColWidth reserves this column's width from `actions.length`; this gap is\n the geometry it reserves for, so changing it belongs in the same edit. -->\n <div\n style=\"\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 18px;\n \"\n >\n <ng-container\n *ngFor=\"let action of col.actions; trackBy: actionTrackByFn\"\n >\n <span\n [pTooltip]=\"action.name\"\n [class]=\"getClassForAction(action) + ' row-interactive'\"\n [style]=\"getStyleForAction(action)\"\n (click)=\"getMethodForAction(action, rowData, $event)\"\n ></span>\n </ng-container>\n </div>\n <ng-container\n [ngTemplateOutlet]=\"getCellTemplate(col) ?? defaultCell\"\n [ngTemplateOutletContext]=\"{\n $implicit: rowData,\n value: rowData[col.field],\n displayValue: getRowData(rowData, col),\n col: col\n }\"\n ></ng-container>\n </td>\n <td\n *ngIf=\"col.editable\"\n class=\"row-interactive\"\n [class.frozen-column]=\"isColumnFrozen(col)\"\n [class.frozen-column-last]=\"isLastFrozenColumn(col)\"\n [style.left]=\"isColumnFrozen(col) ? frozenColumnOffset() : null\"\n >\n <spiderly-number\n [control]=\"getFormArrayControlByIndex(col.field, rowData.index)\"\n [showLabel]=\"false\"\n ></spiderly-number>\n </td>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n <tr>\n <td\n [attr.colspan]=\"\n visibleCols.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"NoRecordsFound\") }}\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template pTemplate=\"paginatorright\">\n <div style=\"display: flex; justify-content: end; gap: 10px\">\n <spiderly-button\n *ngIf=\"showAddButton\"\n [label]=\"t('AddNew')\"\n icon=\"pi pi-plus\"\n (onClick)=\"navigateToDetails(0)\"\n ></spiderly-button>\n </div>\n </ng-template>\n </p-table>\n <p-popover #columnChooser>\n <div class=\"column-chooser\">\n <label\n *ngFor=\"let col of chooserCols; trackBy: colTrackByFn\"\n data-testid=\"column-chooser-option\"\n class=\"column-chooser-option\"\n >\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isColumnVisible(col)\"\n [disabled]=\"!canToggleColumn(col)\"\n (onChange)=\"toggleColumn(col, $event.checked)\"\n ></p-checkbox>\n <span>{{ col.name }}</span>\n </label>\n <button\n type=\"button\"\n data-testid=\"column-chooser-reset\"\n class=\"column-chooser-reset\"\n (click)=\"resetColumnLayout()\"\n >\n {{ t('ResetToDefault') }}\n </button>\n </div>\n </p-popover>\n </div>\n</ng-container>\n", styles: [".table-header{display:flex;justify-content:space-between;align-items:center}@media (max-width: 640px){.table-header{display:flex;flex-direction:column;align-items:start;gap:14px}}.spiderly-table .p-paginator{padding:1rem}@media (min-width: 1400px){.spiderly-table .p-paginator-left-content{position:absolute;padding:14px;left:0}}@media (min-width: 1400px){.spiderly-table .p-paginator-right-content{position:absolute;padding:14px;right:0}}.column-chooser{display:flex;flex-direction:column;gap:8px}.column-chooser-option{display:flex;align-items:center;gap:8px;cursor:pointer}.column-chooser-reset{border:none;background:none;color:inherit;font:inherit;cursor:pointer;padding:0;margin-top:4px;color:var(--p-primary-color);align-self:flex-start;font-size:.875rem}.column-chooser-reset:hover{text-decoration:underline}.column-menu{display:flex;flex-direction:column;min-width:11rem}.column-menu-item{display:flex;align-items:center;gap:8px;border:none;background:none;color:inherit;font:inherit;cursor:pointer;text-align:left;padding:6px 10px;border-radius:var(--p-content-border-radius)}.column-menu-item .column-menu-item-label{flex:1}.column-menu-item:hover:not(:disabled){background:var(--p-content-hover-background)}.column-menu-item:disabled{color:var(--p-text-muted-color);cursor:default}:host .spiderly-table-container{scroll-margin-top:calc(var(--spiderly-viewport-top-inset, 0px) + 1rem)}:host ::ng-deep .clickable{cursor:pointer}:host ::ng-deep td.clickable:hover{text-decoration:underline}:host .column-menu-button{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:0 2px;opacity:0;font-size:.75rem}:host th:hover .column-menu-button,:host .column-menu-button:focus-visible{opacity:1}:host .table-views{display:flex;flex-wrap:wrap;gap:4px;padding-top:8px}:host .table-view{border:none;background:none;color:inherit;font:inherit;cursor:pointer;color:var(--p-text-muted-color);padding:4px 10px;border-radius:var(--p-content-border-radius)}:host .table-view:hover{background:var(--p-content-hover-background)}:host .table-view-active{color:var(--p-primary-color);font-weight:600;background:var(--p-content-hover-background)}:host .frozen-column{position:sticky;z-index:1}:host td.frozen-column{background:var(--p-datatable-row-background, var(--p-content-background));transition:background-color var(--p-transition-duration, .2s)}:host tr:hover>td.frozen-column{background:var(--p-datatable-row-hover-background, inherit)}:host .frozen-column-last{box-shadow:2px 0 4px -2px #00000026}:host th{position:relative}:host .column-resizer{position:absolute;top:0;right:0;width:8px;height:100%;cursor:col-resize;-webkit-user-select:none;user-select:none}:host .selection-count{font-size:.75rem;color:var(--p-text-muted-color)}:host td.selection-cell{-webkit-user-select:none;user-select:none}:host td.cell-wrap{--spiderly-cell-overflow: visible;--spiderly-cell-white-space: normal}:host .cell-text{display:block;overflow:var(--spiderly-cell-overflow, hidden);text-overflow:ellipsis;white-space:var(--spiderly-cell-white-space, nowrap)}:host ::ng-deep .p-datatable-mask{background:color-mix(in srgb,var(--p-content-background, #ffffff) 65%,transparent);color:var(--p-primary-color, #334155);align-items:flex-start;padding-top:6rem}:host ::ng-deep .p-datatable-thead{position:unset!important}:host ::ng-deep .p-datatable-header{border-radius:6px 6px 0 0}:host ::ng-deep .p-datatable{border-radius:var(--p-content-border-radius);border:1px solid var(--p-datatable-border-color)}\n"] }]
|
|
6309
|
+
}], 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: [{
|
|
4955
6310
|
type: Inject,
|
|
4956
6311
|
args: [LOCALE_ID]
|
|
4957
6312
|
}] }], propDecorators: { table: [{
|
|
@@ -4975,6 +6330,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
4975
6330
|
type: Input
|
|
4976
6331
|
}], cols: [{
|
|
4977
6332
|
type: Input
|
|
6333
|
+
}], filters: [{
|
|
6334
|
+
type: Input
|
|
6335
|
+
}], views: [{
|
|
6336
|
+
type: Input
|
|
4978
6337
|
}], showPaginator: [{
|
|
4979
6338
|
type: Input
|
|
4980
6339
|
}], showCardWrapper: [{
|
|
@@ -5041,6 +6400,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
5041
6400
|
type: Input
|
|
5042
6401
|
}], rowNavigationPath: [{
|
|
5043
6402
|
type: Input
|
|
6403
|
+
}], filterTemplates: [{
|
|
6404
|
+
type: ContentChildren,
|
|
6405
|
+
args: [SpiderlyFilterTemplateDirective]
|
|
5044
6406
|
}] } });
|
|
5045
6407
|
class Action {
|
|
5046
6408
|
constructor({ name, field, icon, style, styleClass, onClick, } = {}) {
|
|
@@ -5053,14 +6415,12 @@ class Action {
|
|
|
5053
6415
|
}
|
|
5054
6416
|
}
|
|
5055
6417
|
class Column {
|
|
5056
|
-
constructor({ name, field,
|
|
6418
|
+
constructor({ name, field, filterType, filterId, showMatchModes, dropdownOrMultiselectValues, actions, editable, showTime, decimalPlaces, sortable, visible, lockVisible, width, onCellClick, } = {}) {
|
|
5057
6419
|
this.name = name;
|
|
5058
6420
|
this.field = field;
|
|
5059
|
-
this.filterField = filterField;
|
|
5060
6421
|
this.filterType = filterType;
|
|
5061
|
-
this.
|
|
6422
|
+
this.filterId = filterId;
|
|
5062
6423
|
this.showMatchModes = showMatchModes;
|
|
5063
|
-
this.showAddButton = showAddButton;
|
|
5064
6424
|
this.dropdownOrMultiselectValues = dropdownOrMultiselectValues;
|
|
5065
6425
|
this.actions = actions;
|
|
5066
6426
|
this.editable = editable;
|
|
@@ -5069,7 +6429,7 @@ class Column {
|
|
|
5069
6429
|
this.sortable = sortable;
|
|
5070
6430
|
this.visible = visible;
|
|
5071
6431
|
this.lockVisible = lockVisible;
|
|
5072
|
-
this.
|
|
6432
|
+
this.width = width;
|
|
5073
6433
|
this.onCellClick = onCellClick;
|
|
5074
6434
|
}
|
|
5075
6435
|
}
|
|
@@ -5087,6 +6447,449 @@ class AllClickEvent {
|
|
|
5087
6447
|
}
|
|
5088
6448
|
}
|
|
5089
6449
|
|
|
6450
|
+
var MatchModeCodes;
|
|
6451
|
+
(function (MatchModeCodes) {
|
|
6452
|
+
MatchModeCodes["StartsWith"] = "startsWith";
|
|
6453
|
+
MatchModeCodes["Contains"] = "contains";
|
|
6454
|
+
MatchModeCodes["Equals"] = "equals";
|
|
6455
|
+
MatchModeCodes["LessThan"] = "lessThan";
|
|
6456
|
+
MatchModeCodes["GreaterThan"] = "greaterThan";
|
|
6457
|
+
MatchModeCodes["In"] = "in";
|
|
6458
|
+
})(MatchModeCodes || (MatchModeCodes = {}));
|
|
6459
|
+
|
|
6460
|
+
/**
|
|
6461
|
+
* What each value type may be asked — the ONE operator table, mirroring what the generated
|
|
6462
|
+
* paginator implements per type (`PaginatedResultGenerator`): `In` on a string column answers
|
|
6463
|
+
* with InvalidMatchMode, PrimeNG's own text list adds three modes the backend 400s, and so on.
|
|
6464
|
+
*
|
|
6465
|
+
* Deliberately a LEAF module (imports nothing but the codes) so both consumers can derive from
|
|
6466
|
+
* it without a cycle: the store's runtime validation and offered lists (`filter-store.ts`), and
|
|
6467
|
+
* the compile-time `FilterRule.matchMode` union (`entities/filter-rule.ts`). These used to be
|
|
6468
|
+
* hand-kept copies; edit the table here and every surface follows.
|
|
6469
|
+
*
|
|
6470
|
+
* Tuple order is DISPLAY order for every operator picker (Equals, then "less/before", then
|
|
6471
|
+
* "more/after" — the order the header dropdowns always shipped).
|
|
6472
|
+
*/
|
|
6473
|
+
const ALLOWED_OPERATORS = {
|
|
6474
|
+
text: [
|
|
6475
|
+
MatchModeCodes.StartsWith,
|
|
6476
|
+
MatchModeCodes.Contains,
|
|
6477
|
+
MatchModeCodes.Equals,
|
|
6478
|
+
],
|
|
6479
|
+
number: [
|
|
6480
|
+
MatchModeCodes.Equals,
|
|
6481
|
+
MatchModeCodes.LessThan,
|
|
6482
|
+
MatchModeCodes.GreaterThan,
|
|
6483
|
+
MatchModeCodes.In,
|
|
6484
|
+
],
|
|
6485
|
+
boolean: [MatchModeCodes.Equals],
|
|
6486
|
+
date: [
|
|
6487
|
+
MatchModeCodes.Equals,
|
|
6488
|
+
MatchModeCodes.LessThan,
|
|
6489
|
+
MatchModeCodes.GreaterThan,
|
|
6490
|
+
],
|
|
6491
|
+
};
|
|
6492
|
+
|
|
6493
|
+
const OPERATOR_WORDS = {
|
|
6494
|
+
text: {
|
|
6495
|
+
[MatchModeCodes.StartsWith]: {
|
|
6496
|
+
pickerKey: 'StartsWith',
|
|
6497
|
+
phraseKey: 'FilterChipStartsWith',
|
|
6498
|
+
},
|
|
6499
|
+
[MatchModeCodes.Contains]: {
|
|
6500
|
+
pickerKey: 'Contains',
|
|
6501
|
+
phraseKey: 'FilterChipContains',
|
|
6502
|
+
},
|
|
6503
|
+
[MatchModeCodes.Equals]: {
|
|
6504
|
+
pickerKey: 'Equals',
|
|
6505
|
+
phraseKey: 'FilterChipEquals',
|
|
6506
|
+
},
|
|
6507
|
+
// No `In` for text: ALLOWED_OPERATORS.text does not carry it, so the entry was unreachable —
|
|
6508
|
+
// and it made `textFilter({ options })` look supported when the store throws on it.
|
|
6509
|
+
},
|
|
6510
|
+
number: {
|
|
6511
|
+
[MatchModeCodes.Equals]: {
|
|
6512
|
+
pickerKey: 'Equals',
|
|
6513
|
+
phraseKey: 'FilterChipEquals',
|
|
6514
|
+
},
|
|
6515
|
+
[MatchModeCodes.GreaterThan]: {
|
|
6516
|
+
pickerKey: 'MoreThan',
|
|
6517
|
+
phraseKey: 'FilterChipGreaterThan',
|
|
6518
|
+
},
|
|
6519
|
+
[MatchModeCodes.LessThan]: {
|
|
6520
|
+
pickerKey: 'LessThan',
|
|
6521
|
+
phraseKey: 'FilterChipLessThan',
|
|
6522
|
+
},
|
|
6523
|
+
// The picker never renders for a pick-list (one operator), but the CHIP always does.
|
|
6524
|
+
[MatchModeCodes.In]: { pickerKey: 'In', phraseKey: 'FilterChipIn' },
|
|
6525
|
+
},
|
|
6526
|
+
boolean: {
|
|
6527
|
+
[MatchModeCodes.Equals]: {
|
|
6528
|
+
pickerKey: 'Equals',
|
|
6529
|
+
phraseKey: 'FilterChipEquals',
|
|
6530
|
+
},
|
|
6531
|
+
},
|
|
6532
|
+
date: {
|
|
6533
|
+
[MatchModeCodes.Equals]: {
|
|
6534
|
+
pickerKey: 'OnDate',
|
|
6535
|
+
phraseKey: 'FilterChipEquals',
|
|
6536
|
+
},
|
|
6537
|
+
[MatchModeCodes.LessThan]: {
|
|
6538
|
+
pickerKey: 'DatesBefore',
|
|
6539
|
+
phraseKey: 'FilterChipBefore',
|
|
6540
|
+
},
|
|
6541
|
+
[MatchModeCodes.GreaterThan]: {
|
|
6542
|
+
pickerKey: 'DatesAfter',
|
|
6543
|
+
phraseKey: 'FilterChipAfter',
|
|
6544
|
+
},
|
|
6545
|
+
},
|
|
6546
|
+
};
|
|
6547
|
+
/**
|
|
6548
|
+
* `In` and options are the same fact seen from two sides: `In` needs a list of values, and the
|
|
6549
|
+
* editor only draws one when the filter declares options. So options mean `In` and nothing else,
|
|
6550
|
+
* and their absence rules `In` out — offering it on a plain number filter would hand the
|
|
6551
|
+
* operator a mode with no control behind it.
|
|
6552
|
+
*
|
|
6553
|
+
* Widened first: the per-kind tuples are literal, so `boolean`'s `[Equals]` makes the `In`
|
|
6554
|
+
* comparison a "no overlap" error rather than an empty result.
|
|
6555
|
+
*/
|
|
6556
|
+
function baseOperators(kind, hasOptions) {
|
|
6557
|
+
const accepted = ALLOWED_OPERATORS[kind];
|
|
6558
|
+
return accepted.filter((value) => hasOptions ? value === MatchModeCodes.In : value !== MatchModeCodes.In);
|
|
6559
|
+
}
|
|
6560
|
+
function toOperatorOptions(kind, operators) {
|
|
6561
|
+
return operators.map((value) => ({
|
|
6562
|
+
value,
|
|
6563
|
+
labelKey: OPERATOR_WORDS[kind][value]?.pickerKey ?? value,
|
|
6564
|
+
}));
|
|
6565
|
+
}
|
|
6566
|
+
/**
|
|
6567
|
+
* The operator applied when nobody picked one, which has to be what a person MEANS by filling in
|
|
6568
|
+
* the control. `Contains` for a fragment typed into a box; `GreaterThan` for a date, never
|
|
6569
|
+
* `Equals` — a date equality against a TIMESTAMP matches only the row written in that exact
|
|
6570
|
+
* second, and answers with an empty grid rather than an error (PACMS ran into this on all three
|
|
6571
|
+
* of its date columns, Filip 2026-08-28).
|
|
6572
|
+
*/
|
|
6573
|
+
const DEFAULT_OPERATOR = {
|
|
6574
|
+
text: MatchModeCodes.Contains,
|
|
6575
|
+
number: MatchModeCodes.Equals,
|
|
6576
|
+
boolean: MatchModeCodes.Equals,
|
|
6577
|
+
date: MatchModeCodes.GreaterThan,
|
|
6578
|
+
};
|
|
6579
|
+
function textFilter(config) {
|
|
6580
|
+
return {
|
|
6581
|
+
kind: 'text',
|
|
6582
|
+
label: config.label,
|
|
6583
|
+
operators: config.operators,
|
|
6584
|
+
offered: config.offered,
|
|
6585
|
+
};
|
|
6586
|
+
}
|
|
6587
|
+
/**
|
|
6588
|
+
* Only a NUMBER filter accepts options, and the asymmetry is forced rather than chosen: options
|
|
6589
|
+
* mean `In`, and `In` is the one operator ALLOWED_OPERATORS grants to numbers alone — because the
|
|
6590
|
+
* generated paginator answers it with InvalidMatchMode on a string column. A text pick-list used
|
|
6591
|
+
* to type-check and then throw at the first `set`.
|
|
6592
|
+
*/
|
|
6593
|
+
function numberFilter(config) {
|
|
6594
|
+
return {
|
|
6595
|
+
kind: 'number',
|
|
6596
|
+
label: config.label,
|
|
6597
|
+
options: config.options,
|
|
6598
|
+
operators: config.operators,
|
|
6599
|
+
offered: config.offered,
|
|
6600
|
+
};
|
|
6601
|
+
}
|
|
6602
|
+
function booleanFilter(config) {
|
|
6603
|
+
return {
|
|
6604
|
+
kind: 'boolean',
|
|
6605
|
+
label: config.label,
|
|
6606
|
+
operators: config.operators,
|
|
6607
|
+
offered: config.offered,
|
|
6608
|
+
};
|
|
6609
|
+
}
|
|
6610
|
+
function dateFilter(config) {
|
|
6611
|
+
return {
|
|
6612
|
+
kind: 'date',
|
|
6613
|
+
label: config.label,
|
|
6614
|
+
operators: config.operators,
|
|
6615
|
+
offered: config.offered,
|
|
6616
|
+
};
|
|
6617
|
+
}
|
|
6618
|
+
/** The shape `Date.toJSON` writes, which is the only value here JSON cannot round-trip itself. */
|
|
6619
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
6620
|
+
function reviveValue(value) {
|
|
6621
|
+
if (typeof value === 'string' && ISO_DATE.test(value))
|
|
6622
|
+
return new Date(value);
|
|
6623
|
+
if (Array.isArray(value))
|
|
6624
|
+
return value.map(reviveValue);
|
|
6625
|
+
return value;
|
|
6626
|
+
}
|
|
6627
|
+
/**
|
|
6628
|
+
* A blanked value is NOT a constraint. `contains ''` matches every row, so emitting one would
|
|
6629
|
+
* narrow nothing while the bar claims the grid is filtered.
|
|
6630
|
+
*/
|
|
6631
|
+
function isBlank(value) {
|
|
6632
|
+
if (value === null || value === undefined)
|
|
6633
|
+
return true;
|
|
6634
|
+
if (typeof value === 'string')
|
|
6635
|
+
return value.trim() === '';
|
|
6636
|
+
if (Array.isArray(value))
|
|
6637
|
+
return value.length === 0;
|
|
6638
|
+
// A half-typed datepicker yields an Invalid Date, which JSON.stringify writes as `null`.
|
|
6639
|
+
if (value instanceof Date)
|
|
6640
|
+
return Number.isNaN(value.getTime());
|
|
6641
|
+
return false;
|
|
6642
|
+
}
|
|
6643
|
+
/**
|
|
6644
|
+
* Whether two constraint values are the same QUESTION. Identity is not it: every `set` builds a
|
|
6645
|
+
* fresh draft, a multiselect hands back a fresh array per change, and two Dates at one instant
|
|
6646
|
+
* are one constraint. This is what `commit` bails on, so a paste-over-itself or a double Apply
|
|
6647
|
+
* cannot spend a request — the rule PACMS's order list used to hand-roll as `lastCommittedTerm`.
|
|
6648
|
+
*/
|
|
6649
|
+
function valueEquals(a, b) {
|
|
6650
|
+
if (a === b)
|
|
6651
|
+
return true;
|
|
6652
|
+
if (a instanceof Date && b instanceof Date)
|
|
6653
|
+
return a.getTime() === b.getTime();
|
|
6654
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
6655
|
+
return a.length === b.length && a.every((item, i) => valueEquals(item, b[i]));
|
|
6656
|
+
}
|
|
6657
|
+
return false;
|
|
6658
|
+
}
|
|
6659
|
+
function constraintEquals(a, b) {
|
|
6660
|
+
return a != null && a.operator === b.operator && valueEquals(a.value, b.value);
|
|
6661
|
+
}
|
|
6662
|
+
/**
|
|
6663
|
+
* The filter engine. It knows nothing about columns: a filter has an id, a value type and a
|
|
6664
|
+
* constraint, and that is the entire vocabulary.
|
|
6665
|
+
*/
|
|
6666
|
+
function createFilterStore(definitions) {
|
|
6667
|
+
// Per-filter offered operators, resolved ONCE per declaration — so an invalid narrowing is
|
|
6668
|
+
// said once, not on every editor open — and re-resolved only where the inputs can actually
|
|
6669
|
+
// change: `setOptions`, because options' PRESENCE flips a filter between pick-list (`In`) and
|
|
6670
|
+
// plain operators. A per-store Map rather than the module-level WeakMap this first shipped
|
|
6671
|
+
// as: that memo was keyed on a definition object consumers mutate, so late-filled options
|
|
6672
|
+
// could freeze a filter's operators at the wrong answer.
|
|
6673
|
+
const resolveOffered = (id) => {
|
|
6674
|
+
const definition = definitions[id];
|
|
6675
|
+
const base = baseOperators(definition.kind, definition.options != null);
|
|
6676
|
+
let codes = base;
|
|
6677
|
+
if (definition.operators?.length) {
|
|
6678
|
+
const survivors = definition.operators.filter((operator) => base.includes(operator));
|
|
6679
|
+
// Same polarity as the column path's computeMatchModes: a bad entry is dropped and said
|
|
6680
|
+
// out loud, and a narrowing that leaves nothing falls back to the full list — an editor
|
|
6681
|
+
// with no operators would be unusable, which is worse than an unwanted one. Mostly
|
|
6682
|
+
// unreachable now that FilterConfig types `operators` per kind; kept for dynamically
|
|
6683
|
+
// built arrays.
|
|
6684
|
+
if (survivors.length < definition.operators.length) {
|
|
6685
|
+
console.error(`Filter '${definition.label}' narrows to operators its ${definition.kind} kind does not ` +
|
|
6686
|
+
`offer (${definition.operators.join(', ')}; offered: ${base.join(', ')}). ` +
|
|
6687
|
+
`Unsupported entries are ignored${survivors.length ? '' : '; falling back to the full list'}.`);
|
|
6688
|
+
}
|
|
6689
|
+
codes = survivors.length ? survivors : base;
|
|
6690
|
+
}
|
|
6691
|
+
return { codes, options: toOperatorOptions(definition.kind, codes) };
|
|
6692
|
+
};
|
|
6693
|
+
const offered = new Map();
|
|
6694
|
+
for (const id of Object.keys(definitions)) {
|
|
6695
|
+
offered.set(id, resolveOffered(id));
|
|
6696
|
+
}
|
|
6697
|
+
/**
|
|
6698
|
+
* The operator applied when nobody picked one. Options mean `In`, whatever the kind's default;
|
|
6699
|
+
* a narrowed filter defaults to its FIRST declared operator.
|
|
6700
|
+
*/
|
|
6701
|
+
const defaultOperatorFor = (id) => {
|
|
6702
|
+
const definition = definitions[id];
|
|
6703
|
+
if (definition.options != null)
|
|
6704
|
+
return MatchModeCodes.In;
|
|
6705
|
+
return definition.operators?.length
|
|
6706
|
+
? offered.get(id).codes[0]
|
|
6707
|
+
: DEFAULT_OPERATOR[definition.kind];
|
|
6708
|
+
};
|
|
6709
|
+
// Two stores, and the split is the whole point: `set` writes a DRAFT, `commit` publishes it.
|
|
6710
|
+
// A chip drawn off a draft would repeat the mistake the header's filter icon already shipped —
|
|
6711
|
+
// claiming the grid is narrowed on the first keystroke. Controls that apply on change
|
|
6712
|
+
// (multiselect, boolean, date) call both at once; a text box commits on Enter or blur.
|
|
6713
|
+
const drafts = signal(new Map());
|
|
6714
|
+
const writeDraft = (id, constraint) => {
|
|
6715
|
+
const next = new Map(drafts());
|
|
6716
|
+
if (constraint === null)
|
|
6717
|
+
next.delete(id);
|
|
6718
|
+
else
|
|
6719
|
+
next.set(id, constraint);
|
|
6720
|
+
drafts.set(next);
|
|
6721
|
+
};
|
|
6722
|
+
const committed = signal(new Map());
|
|
6723
|
+
const applied = computed(() => [...committed()].map(([id, constraint]) => ({
|
|
6724
|
+
id: id,
|
|
6725
|
+
label: definitions[id].label,
|
|
6726
|
+
kind: definitions[id].kind,
|
|
6727
|
+
operator: constraint.operator,
|
|
6728
|
+
operatorPhraseKey: OPERATOR_WORDS[definitions[id].kind][constraint.operator]?.phraseKey ??
|
|
6729
|
+
constraint.operator,
|
|
6730
|
+
value: constraint.value,
|
|
6731
|
+
})));
|
|
6732
|
+
const store = {
|
|
6733
|
+
definitions,
|
|
6734
|
+
applied,
|
|
6735
|
+
/**
|
|
6736
|
+
* A handle bound to one filter. This is the PLACEMENT api: it needs nothing from the component
|
|
6737
|
+
* tree, so the same filter can be driven from the bar, from a header cell, or from a drawer
|
|
6738
|
+
* rendered at app root. `value` is the DRAFT — a control shows what was typed, not what was
|
|
6739
|
+
* applied; `applied()` is the other question and the bar asks that one.
|
|
6740
|
+
*/
|
|
6741
|
+
get(id) {
|
|
6742
|
+
return {
|
|
6743
|
+
id: id,
|
|
6744
|
+
label: definitions[id].label,
|
|
6745
|
+
kind: definitions[id].kind,
|
|
6746
|
+
value: computed(() => drafts().get(id)?.value),
|
|
6747
|
+
operator: computed(() => drafts().get(id)?.operator),
|
|
6748
|
+
// Getters, not snapshots: options can land AFTER a handle is taken — the bar's editor
|
|
6749
|
+
// holds one for the life of the popover, and PACMS fills its lookups asynchronously —
|
|
6750
|
+
// and a snapshot left an already-open pick-list empty until it was reopened. The
|
|
6751
|
+
// operator list rides along, since setOptions can flip it to `In`.
|
|
6752
|
+
get operators() {
|
|
6753
|
+
return offered.get(id).options;
|
|
6754
|
+
},
|
|
6755
|
+
get defaultOperator() {
|
|
6756
|
+
return defaultOperatorFor(id);
|
|
6757
|
+
},
|
|
6758
|
+
get options() {
|
|
6759
|
+
return definitions[id].options;
|
|
6760
|
+
},
|
|
6761
|
+
set: (constraint) => store.set(id, constraint),
|
|
6762
|
+
commit: () => store.commit(id),
|
|
6763
|
+
reset: () => store.reset(id),
|
|
6764
|
+
};
|
|
6765
|
+
},
|
|
6766
|
+
/**
|
|
6767
|
+
* The ONE seam for choices that arrive after the store is built (PACMS fills its pick-lists
|
|
6768
|
+
* from lookups that race a deploy). Never write `definitions[id].options` by hand: options'
|
|
6769
|
+
* PRESENCE is what makes a filter a pick-list, so the setter also re-resolves the offered
|
|
6770
|
+
* operators — a hand write would leave a late-filled filter offering `Equals` on a control
|
|
6771
|
+
* that emits a list.
|
|
6772
|
+
*/
|
|
6773
|
+
setOptions(id, options) {
|
|
6774
|
+
definitions[id].options = options;
|
|
6775
|
+
offered.set(id, resolveOffered(id));
|
|
6776
|
+
},
|
|
6777
|
+
set(id, constraint) {
|
|
6778
|
+
const kind = definitions[id].kind;
|
|
6779
|
+
// Throws rather than logging and ignoring, which is what `computeMatchModes` does for a
|
|
6780
|
+
// column's OFFERED match modes. Different stakes: that one narrows a dropdown, so a bad
|
|
6781
|
+
// entry merely fails to show, while this one assembles a QUERY — dropping it silently would
|
|
6782
|
+
// leave the operator looking at unfiltered rows believing they are filtered.
|
|
6783
|
+
if (!ALLOWED_OPERATORS[kind].includes(constraint.operator)) {
|
|
6784
|
+
throw new Error(`Filter '${String(id)}' is ${kind}, which does not accept the '${constraint.operator}' ` +
|
|
6785
|
+
`operator (allowed: ${ALLOWED_OPERATORS[kind].join(', ')}). The generated paginator ` +
|
|
6786
|
+
`answers an unsupported match mode with InvalidMatchMode.`);
|
|
6787
|
+
}
|
|
6788
|
+
writeDraft(id, constraint);
|
|
6789
|
+
},
|
|
6790
|
+
/**
|
|
6791
|
+
* `set` + `commit` in one breath — the shape every PROGRAMMATIC write takes (a view's
|
|
6792
|
+
* `apply`, the sheet flow). The split API stays for controls, where draft-on-type /
|
|
6793
|
+
* publish-on-Apply is the point; but a bare `set` whose `commit` is forgotten (or written
|
|
6794
|
+
* with the wrong id — the pair repeats the string twice) fails silently: the draft is
|
|
6795
|
+
* written, nothing is published, and a view narrows by nothing under a labelled tab.
|
|
6796
|
+
*/
|
|
6797
|
+
setAndCommit(id, constraint) {
|
|
6798
|
+
store.set(id, constraint);
|
|
6799
|
+
store.commit(id);
|
|
6800
|
+
},
|
|
6801
|
+
/**
|
|
6802
|
+
* Publishes the draft. A blanked draft REMOVES the constraint rather than committing an empty
|
|
6803
|
+
* one, so `applied()` and the payload agree: no chip, no key, no `contains ''`.
|
|
6804
|
+
*/
|
|
6805
|
+
commit(id) {
|
|
6806
|
+
const draft = drafts().get(id);
|
|
6807
|
+
const current = committed();
|
|
6808
|
+
const shouldRemove = draft === undefined || isBlank(draft.value);
|
|
6809
|
+
// A new Map is a new identity even with identical contents, and everything downstream
|
|
6810
|
+
// reacts to identity — so a no-op commit would spend a request. The bail compares the
|
|
6811
|
+
// CONSTRAINT, not the draft object: every set() builds a fresh draft, and the case that
|
|
6812
|
+
// matters is an identical value re-committed (see valueEquals).
|
|
6813
|
+
if (shouldRemove ? !current.has(id) : constraintEquals(current.get(id), draft))
|
|
6814
|
+
return;
|
|
6815
|
+
const next = new Map(current);
|
|
6816
|
+
if (shouldRemove)
|
|
6817
|
+
next.delete(id);
|
|
6818
|
+
else
|
|
6819
|
+
next.set(id, draft);
|
|
6820
|
+
committed.set(next);
|
|
6821
|
+
},
|
|
6822
|
+
/**
|
|
6823
|
+
* The chip's `x`. Drops the DRAFT as well as the committed constraint: a surviving draft
|
|
6824
|
+
* leaves the control still showing the cleared text, and the next commit would restore a
|
|
6825
|
+
* filter nobody re-typed.
|
|
6826
|
+
*/
|
|
6827
|
+
reset(id) {
|
|
6828
|
+
if (drafts().has(id))
|
|
6829
|
+
writeDraft(id, null);
|
|
6830
|
+
if (!committed().has(id))
|
|
6831
|
+
return;
|
|
6832
|
+
const next = new Map(committed());
|
|
6833
|
+
next.delete(id);
|
|
6834
|
+
committed.set(next);
|
|
6835
|
+
},
|
|
6836
|
+
/**
|
|
6837
|
+
* The applied set, plain enough to store. Drafts are deliberately NOT included: a half-typed
|
|
6838
|
+
* value is not a state anyone should come back to, and the bar's whole claim is that what it
|
|
6839
|
+
* shows is what narrows the grid.
|
|
6840
|
+
*/
|
|
6841
|
+
snapshot() {
|
|
6842
|
+
const snapshot = {};
|
|
6843
|
+
for (const [id, constraint] of committed()) {
|
|
6844
|
+
snapshot[id] = {
|
|
6845
|
+
operator: constraint.operator,
|
|
6846
|
+
value: constraint.value,
|
|
6847
|
+
};
|
|
6848
|
+
}
|
|
6849
|
+
return snapshot;
|
|
6850
|
+
},
|
|
6851
|
+
/**
|
|
6852
|
+
* Replaces the applied set. The drafts are written too, so reopening a restored chip shows
|
|
6853
|
+
* the value it is filtering by rather than an empty control.
|
|
6854
|
+
*
|
|
6855
|
+
* Ids the store no longer declares are dropped rather than carried: a filter removed from the
|
|
6856
|
+
* declaration in a later release would otherwise ride every request from anyone whose storage
|
|
6857
|
+
* still held it, with no chip able to name it and no way to clear it from the UI.
|
|
6858
|
+
*/
|
|
6859
|
+
restore(snapshot) {
|
|
6860
|
+
const next = new Map();
|
|
6861
|
+
for (const [id, constraint] of Object.entries(snapshot ?? {})) {
|
|
6862
|
+
if (!(id in definitions))
|
|
6863
|
+
continue;
|
|
6864
|
+
next.set(id, {
|
|
6865
|
+
operator: constraint.operator,
|
|
6866
|
+
value: reviveValue(constraint.value),
|
|
6867
|
+
});
|
|
6868
|
+
}
|
|
6869
|
+
drafts.set(new Map(next));
|
|
6870
|
+
committed.set(next);
|
|
6871
|
+
},
|
|
6872
|
+
/** The bar's "Clear all". `reset` for every filter, drafts included. */
|
|
6873
|
+
clear() {
|
|
6874
|
+
if (drafts().size > 0)
|
|
6875
|
+
drafts.set(new Map());
|
|
6876
|
+
if (committed().size > 0)
|
|
6877
|
+
committed.set(new Map());
|
|
6878
|
+
},
|
|
6879
|
+
/** The `filters` half of `Filter` — what the generated paginator reads. */
|
|
6880
|
+
toFilterPayload() {
|
|
6881
|
+
const payload = {};
|
|
6882
|
+
for (const [id, constraint] of committed()) {
|
|
6883
|
+
payload[id] = [
|
|
6884
|
+
{ matchMode: constraint.operator, value: constraint.value },
|
|
6885
|
+
];
|
|
6886
|
+
}
|
|
6887
|
+
return payload;
|
|
6888
|
+
},
|
|
6889
|
+
};
|
|
6890
|
+
return store;
|
|
6891
|
+
}
|
|
6892
|
+
|
|
5090
6893
|
class SpiderlyDataViewComponent {
|
|
5091
6894
|
constructor(translocoService, locale) {
|
|
5092
6895
|
this.translocoService = translocoService;
|
|
@@ -5142,7 +6945,24 @@ class SpiderlyDataViewComponent {
|
|
|
5142
6945
|
},
|
|
5143
6946
|
];
|
|
5144
6947
|
}
|
|
6948
|
+
// Paging semantics and the scroll-margin contract are documented once, on the data table:
|
|
6949
|
+
// spiderly-data-table/CLAUDE.md → "Scroll back to the table on page change".
|
|
6950
|
+
onPageChange(container) {
|
|
6951
|
+
scrollElementIntoViewIfAboveViewport(container);
|
|
6952
|
+
}
|
|
6953
|
+
/**
|
|
6954
|
+
* The one pending predicate: PrimeNG's overlay and the container's `aria-busy` must never
|
|
6955
|
+
* disagree. Mirrors the data table's, including why it is not also testing `items`; the
|
|
6956
|
+
* rationale lives there.
|
|
6957
|
+
*/
|
|
6958
|
+
get isPending() {
|
|
6959
|
+
return this.loading;
|
|
6960
|
+
}
|
|
5145
6961
|
lazyLoad(event) {
|
|
6962
|
+
// Every refetch is pending, not just the first load. `items` is deliberately left alone so
|
|
6963
|
+
// the previous page stays readable under the overlay. Full reasoning: the data table's
|
|
6964
|
+
// CLAUDE.md → "Pending state".
|
|
6965
|
+
this.loading = true;
|
|
5146
6966
|
this.lastLazyLoadEvent = event;
|
|
5147
6967
|
const transformedFilter = {};
|
|
5148
6968
|
for (const key in event.filters) {
|
|
@@ -5163,7 +6983,7 @@ class SpiderlyDataViewComponent {
|
|
|
5163
6983
|
tableFilter.filters = transformedFilter;
|
|
5164
6984
|
this.onLazyLoad.next(tableFilter);
|
|
5165
6985
|
this.getPaginatedListObservableMethod(tableFilter).subscribe({
|
|
5166
|
-
next:
|
|
6986
|
+
next: (res) => {
|
|
5167
6987
|
this.items = res.data;
|
|
5168
6988
|
this.totalRecords = res.totalRecords;
|
|
5169
6989
|
this.loading = false;
|
|
@@ -5207,8 +7027,10 @@ class SpiderlyDataViewComponent {
|
|
|
5207
7027
|
}
|
|
5208
7028
|
}
|
|
5209
7029
|
reload() {
|
|
5210
|
-
|
|
5211
|
-
this.
|
|
7030
|
+
// Nothing to replay before the initial load, which is already on its way.
|
|
7031
|
+
if (this.lastLazyLoadEvent == null)
|
|
7032
|
+
return;
|
|
7033
|
+
// Not special: same refetch as a page flip, so lazyLoad owns both the flag and `items`.
|
|
5212
7034
|
this.lazyLoad(this.lastLazyLoadEvent);
|
|
5213
7035
|
}
|
|
5214
7036
|
colTrackByFn(index, item) {
|
|
@@ -5221,7 +7043,7 @@ class SpiderlyDataViewComponent {
|
|
|
5221
7043
|
this.table.clear();
|
|
5222
7044
|
}
|
|
5223
7045
|
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 }); }
|
|
5224
|
-
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 [class]=\"\n showCardWrapper ? 'card responsive-card-padding overflow-auto' : ''\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 [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"items === undefined || loading === true\"\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: ["::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.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 }] }); }
|
|
7046
|
+
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 }] }); }
|
|
5225
7047
|
}
|
|
5226
7048
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyDataViewComponent, decorators: [{
|
|
5227
7049
|
type: Component,
|
|
@@ -5239,7 +7061,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
5239
7061
|
InputTextModule,
|
|
5240
7062
|
InputNumberModule,
|
|
5241
7063
|
SelectModule,
|
|
5242
|
-
], template: "<ng-container *transloco=\"let t\">\n <div\n [
|
|
7064
|
+
], 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"] }]
|
|
5243
7065
|
}], ctorParameters: () => [{ type: i1.TranslocoService }, { type: undefined, decorators: [{
|
|
5244
7066
|
type: Inject,
|
|
5245
7067
|
args: [LOCALE_ID]
|
|
@@ -5358,12 +7180,9 @@ class Filter extends BaseEntity {
|
|
|
5358
7180
|
* The `FilterRule` class is a generic structure that defines a single filtering condition,
|
|
5359
7181
|
* including the match mode (comparison operator), the value to compare, and an optional logical operator.
|
|
5360
7182
|
*
|
|
5361
|
-
* The allowed match modes are determined by the type parameter `T
|
|
5362
|
-
*
|
|
5363
|
-
*
|
|
5364
|
-
* - For `Date`: supports `Equals`, `GreaterThan`, and `LessThan`.
|
|
5365
|
-
* - For `number`: supports `Equals`, `GreaterThan`, `LessThan`, and `In`.
|
|
5366
|
-
* - For other types: allows any value from `MatchModeCodes`.
|
|
7183
|
+
* The allowed match modes are determined by the type parameter `T`, each union derived from the
|
|
7184
|
+
* one operator table in `filters/allowed-operators.ts` (for other types, any `MatchModeCodes`
|
|
7185
|
+
* value is allowed).
|
|
5367
7186
|
*
|
|
5368
7187
|
* @template T The type of the value to filter by.
|
|
5369
7188
|
*/
|
|
@@ -5765,5 +7584,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
5765
7584
|
* Generated bundle index. Do not edit.
|
|
5766
7585
|
*/
|
|
5767
7586
|
|
|
5768
|
-
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, selectedTab, singleOrDefault, splitPascalCase, toCommaSeparatedString, unauthorizedInterceptor, validatePrecisionScale };
|
|
7587
|
+
export { ALLOWED_OPERATORS, Action, AllClickEvent, ApiErrorCodes, ApiSecurityService, AppSidebarComponent, AuthCardComponent, AuthGuard, AuthResult, AuthResultWithCookies, AuthServiceBase, BaseAutocompleteControl, BaseControl, BaseDropdownControl, BaseEntity, BaseFormComponent, BaseFormService, CardSkeletonComponent, Codebook, Column, ConfigServiceBase, DEFAULT_EXTERNAL_PROVIDER_ICONS, DEFAULT_OPERATOR, ExternalLoginComponent, ExternalProvider, ExternalProviderPublic, Filter, FilterRule, FilterSortMeta, IndexCardComponent, InfoCardComponent, InitCompanyAuthDialogDetails, InitTopBarData, IsAuthorizedForSaveEvent, LastMenuIconIndexClicked, LayoutServiceBase, LazyLoadSelectedIdsResult, Login, LoginVerificationComponent, LoginVerificationToken, MatchModeCodes, MenuChangeEvent, MenuitemComponent, Namebook, NotAuthGuard, NotFoundComponent, PROPS_KEY, PaginatedResult, PanelBodyComponent, PanelFooterComponent, PanelHeaderComponent, PrimengOption, ProfileAvatarComponent, ReflectProp, RefreshTokenRequest, RequiredComponent, RowClickEvent, SecurityPermissionCodes, SendLoginVerificationEmailResult, SideMenuTopBarComponent, SidebarMenuComponent, SimpleSaveResult, SpiderlyAutocompleteComponent, SpiderlyButtonBaseComponent, SpiderlyButtonComponent, SpiderlyCalendarComponent, SpiderlyCardComponent, SpiderlyCellTemplateDirective, SpiderlyCheckboxComponent, SpiderlyColorPickerComponent, SpiderlyControlsModule, SpiderlyDataTableActionsDirective, SpiderlyDataTableComponent, SpiderlyDataViewComponent, SpiderlyDeleteConfirmationComponent, SpiderlyDropdownComponent, SpiderlyEditorComponent, SpiderlyErrorHandler, SpiderlyFileComponent, SpiderlyFileSelectEvent, SpiderlyFilterBarComponent, SpiderlyFilterTemplateDirective, SpiderlyFormArray, SpiderlyFormControl, SpiderlyFormGroup, SpiderlyLayoutComponent, SpiderlyLoginComponent, SpiderlyMarkdownComponent, SpiderlyMessageService, SpiderlyMultiAutocompleteComponent, SpiderlyMultiSelectComponent, SpiderlyNumberComponent, SpiderlyOverflowTitleDirective, SpiderlyPanelComponent, SpiderlyPanelsModule, SpiderlyPasswordComponent, SpiderlyReturnButtonComponent, SpiderlySplitButtonComponent, SpiderlyTab, SpiderlyTemplateTypeDirective, SpiderlyTextareaComponent, SpiderlyTextboxComponent, SpiderlyTranslocoLoader, TopBarComponent, UserBase, UserRole, ValidatorAbstractService, VerificationTokenRequest, VerificationTypeCodes, VerificationWrapperComponent, adjustColor, authInitializer, booleanFilter, capitalizeFirstChar, createFilterStore, dateFilter, deleteAction, exportListToExcel, firstCharToUpper, foldForSearch, getFileNameFromContentDisposition, getFilterOptions, getHtmlImgDisplayString64, getImageDimensions, getMimeTypeForFileName, getMonth, getParentUrl, getPrimengAutocompleteCodebookOptions, getPrimengAutocompleteNamebookOptions, getPrimengDropdownCodebookOptions, getPrimengDropdownNamebookOptions, getPrimengNamebookOptions, httpLoadingInterceptor, isExcelFileType, isFileImageType, isNullOrEmpty, jsonHttpInterceptor, jwtInterceptor, kebabToTitleCase, nameOf, nameof, numberFilter, parseDateOnlyLocal, pushAction, saveResponseAsFile, scrollElementIntoViewIfAboveViewport, selectedTab, singleOrDefault, splitPascalCase, textFilter, toCommaSeparatedString, unauthorizedInterceptor, validatePrecisionScale };
|
|
5769
7588
|
//# sourceMappingURL=spiderly.mjs.map
|