@mintplayer/ng-spark 22.23.0 → 22.24.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.
Files changed (68) hide show
  1. package/README.md +181 -0
  2. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs +100 -47
  3. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs.map +1 -1
  4. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +308 -329
  5. package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
  6. package/fesm2022/mintplayer-ng-spark-column-filter.mjs +175 -135
  7. package/fesm2022/mintplayer-ng-spark-column-filter.mjs.map +1 -1
  8. package/fesm2022/mintplayer-ng-spark-grid.mjs +2259 -1119
  9. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  10. package/fesm2022/mintplayer-ng-spark-history.mjs +611 -0
  11. package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -0
  12. package/fesm2022/mintplayer-ng-spark-icon.mjs +64 -27
  13. package/fesm2022/mintplayer-ng-spark-icon.mjs.map +1 -1
  14. package/fesm2022/mintplayer-ng-spark-models.mjs +366 -762
  15. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  16. package/fesm2022/mintplayer-ng-spark-moderation.mjs +1212 -0
  17. package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -0
  18. package/fesm2022/mintplayer-ng-spark-panels.mjs +52 -0
  19. package/fesm2022/mintplayer-ng-spark-panels.mjs.map +1 -0
  20. package/fesm2022/mintplayer-ng-spark-pipes.mjs +1122 -765
  21. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  22. package/fesm2022/mintplayer-ng-spark-po-create.mjs +273 -169
  23. package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
  24. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +786 -410
  25. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  26. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +292 -284
  27. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  28. package/fesm2022/mintplayer-ng-spark-po-form.mjs +2187 -1225
  29. package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
  30. package/fesm2022/mintplayer-ng-spark-query-list.mjs +516 -303
  31. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  32. package/fesm2022/mintplayer-ng-spark-renderers.mjs +17 -55
  33. package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
  34. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +216 -177
  35. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
  36. package/fesm2022/mintplayer-ng-spark-routes.mjs +24 -28
  37. package/fesm2022/mintplayer-ng-spark-routes.mjs.map +1 -1
  38. package/fesm2022/mintplayer-ng-spark-services.mjs +625 -640
  39. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  40. package/fesm2022/mintplayer-ng-spark-shell.mjs +1013 -397
  41. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  42. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +294 -0
  43. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs.map +1 -0
  44. package/fesm2022/mintplayer-ng-spark.mjs +13 -50
  45. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  46. package/package.json +17 -1
  47. package/types/mintplayer-ng-spark-attribute-description.d.ts +25 -18
  48. package/types/mintplayer-ng-spark-client-operations.d.ts +133 -160
  49. package/types/mintplayer-ng-spark-column-filter.d.ts +73 -55
  50. package/types/mintplayer-ng-spark-grid.d.ts +885 -448
  51. package/types/mintplayer-ng-spark-history.d.ts +223 -0
  52. package/types/mintplayer-ng-spark-icon.d.ts +15 -13
  53. package/types/mintplayer-ng-spark-models.d.ts +624 -573
  54. package/types/mintplayer-ng-spark-moderation.d.ts +402 -0
  55. package/types/mintplayer-ng-spark-panels.d.ts +108 -0
  56. package/types/mintplayer-ng-spark-pipes.d.ts +120 -149
  57. package/types/mintplayer-ng-spark-po-create.d.ts +46 -37
  58. package/types/mintplayer-ng-spark-po-detail.d.ts +177 -117
  59. package/types/mintplayer-ng-spark-po-edit.d.ts +64 -65
  60. package/types/mintplayer-ng-spark-po-form.d.ts +507 -371
  61. package/types/mintplayer-ng-spark-query-list.d.ts +118 -82
  62. package/types/mintplayer-ng-spark-renderers.d.ts +79 -85
  63. package/types/mintplayer-ng-spark-retry-action-modal.d.ts +31 -34
  64. package/types/mintplayer-ng-spark-routes.d.ts +7 -11
  65. package/types/mintplayer-ng-spark-services.d.ts +377 -261
  66. package/types/mintplayer-ng-spark-shell.d.ts +209 -128
  67. package/types/mintplayer-ng-spark-soft-delete.d.ts +90 -0
  68. package/types/mintplayer-ng-spark.d.ts +7 -12
@@ -1,1133 +1,2273 @@
1
- import { DatatableSettings, BsDatatableComponent, BsDatatableColumnDirective, BsDatatableFilterPanelDirective, BsRowTemplateDirective } from '@mintplayer/ng-bootstrap/datatable';
2
- import { hasShowedOnFlag, ShowedOn, valueFor, selectionModeFor, parseSelectionRule, filterQueryActions } from '@mintplayer/ng-spark/models';
3
- import * as i0 from '@angular/core';
4
- import { inject, Injectable, TemplateRef, input, Directive, computed, ChangeDetectionStrategy, Component, model, output, signal, effect, untracked, contentChildren, viewChild } from '@angular/core';
5
- import { SPARK_ATTRIBUTE_RENDERERS, cellValue, withDeclaredInputs } from '@mintplayer/ng-spark/renderers';
6
- import { SparkService, SparkLanguageService } from '@mintplayer/ng-spark/services';
7
- import * as i1 from '@angular/common';
8
- import { CommonModule, NgComponentOutlet } from '@angular/common';
9
- import * as i2 from '@angular/router';
10
- import { RouterModule } from '@angular/router';
11
- import { Color } from '@mintplayer/ng-bootstrap';
12
- import { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';
13
- import { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';
14
- import { SparkQueryRefreshService } from '@mintplayer/ng-spark/client-operations';
15
- import { ResolveTranslationPipe, QueryCellValuePipe, QueryReferenceChipsPipe, TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';
16
- import { SparkAttributeDescriptionComponent } from '@mintplayer/ng-spark/attribute-description';
17
- import { SparkColumnFilterPanelComponent } from '@mintplayer/ng-spark/column-filter';
18
- import { BsCardComponent, BsCardHeaderComponent } from '@mintplayer/ng-bootstrap/card';
19
- import { BsPriorityNavComponent, BsPriorityNavItemDirective } from '@mintplayer/ng-bootstrap/priority-nav';
20
-
21
- /** Page sizes offered by every Spark grid. */
22
- const SPARK_GRID_PAGE_SIZES = [10, 25, 50];
23
- /**
24
- * The attributes a grid shows, in display order.
25
- *
26
- * Shared so the two grids cannot disagree about what "visible" means — they each had their own
27
- * copy of this expression, which is the kind of thing that stays identical right up until it
28
- * doesn't.
29
- */
1
+ import { BsDatatableColumnDirective, BsDatatableComponent, BsDatatableFilterPanelDirective, BsRowTemplateDirective, DatatableSettings } from "@mintplayer/ng-bootstrap/datatable";
2
+ import { ShowedOn, defaultQueryActions, filterQueryActions, hasShowedOnFlag, parseSelectionRule, selectionModeFor, valueFor } from "@mintplayer/ng-spark/models";
3
+ import * as i0 from "@angular/core";
4
+ import { ChangeDetectionStrategy, Component, DestroyRef, Directive, Injectable, TemplateRef, computed, contentChildren, effect, inject, input, linkedSignal, model, output, signal, untracked, viewChild } from "@angular/core";
5
+ import { SPARK_ATTRIBUTE_RENDERERS, cellValue, withDeclaredInputs } from "@mintplayer/ng-spark/renderers";
6
+ import { SPARK_RETURN_URL_STATE_KEY, SparkLanguageService, SparkService } from "@mintplayer/ng-spark/services";
7
+ import * as i1$1 from "@angular/common";
8
+ import { CommonModule, NgComponentOutlet } from "@angular/common";
9
+ import { toSignal } from "@angular/core/rxjs-interop";
10
+ import * as i2 from "@angular/router";
11
+ import { NavigationEnd, Router, RouterModule } from "@angular/router";
12
+ import { filter, map } from "rxjs";
13
+ import { Color } from "@mintplayer/ng-bootstrap";
14
+ import { BsAlertComponent } from "@mintplayer/ng-bootstrap/alert";
15
+ import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
16
+ import { BsBadgeComponent } from "@mintplayer/ng-bootstrap/badge";
17
+ import { CdkConnectedOverlay, CdkOverlayOrigin } from "@angular/cdk/overlay";
18
+ import { BsDropdownItemDirective, BsDropdownMenuComponent } from "@mintplayer/ng-bootstrap/dropdown-menu";
19
+ import { SparkIconComponent } from "@mintplayer/ng-spark/icon";
20
+ import { SparkQueryRefreshService } from "@mintplayer/ng-spark/client-operations";
21
+ import { QueryCellValuePipe, QueryReferenceChipsPipe, ResolveTranslationPipe, TranslateKeyPipe } from "@mintplayer/ng-spark/pipes";
22
+ import { SparkAttributeDescriptionComponent } from "@mintplayer/ng-spark/attribute-description";
23
+ import { SparkColumnFilterPanelComponent } from "@mintplayer/ng-spark/column-filter";
24
+ import { BsCardComponent, BsCardHeaderComponent } from "@mintplayer/ng-bootstrap/card";
25
+ import { BsPriorityNavComponent, BsPriorityNavItemDirective } from "@mintplayer/ng-bootstrap/priority-nav";
26
+ import * as i1 from "@angular/forms";
27
+ import { FormsModule } from "@angular/forms";
28
+ import { BsFormComponent, BsFormControlDirective } from "@mintplayer/ng-bootstrap/form";
29
+ import { BsInputGroupComponent } from "@mintplayer/ng-bootstrap/input-group";
30
+ const SPARK_GRID_PAGE_SIZES = [
31
+ 10,
32
+ 25,
33
+ 50
34
+ ];
30
35
  function visibleGridAttributes(entityType) {
31
- return entityType?.attributes
32
- .filter(a => a.isVisible && hasShowedOnFlag(a.showedOn, ShowedOn.Query))
33
- .sort((a, b) => a.order - b.order) ?? [];
36
+ return entityType?.attributes.filter((a) => a.isVisible && hasShowedOnFlag(a.showedOn, ShowedOn.Query)).sort((a, b) => a.order - b.order) ?? [];
34
37
  }
35
- /**
36
- * Initial datatable settings for a query, seeded with the query's declared sort.
37
- *
38
- * The datatable owns paging and sorting from here on and calls the fetch callback per page; this
39
- * only decides where it starts.
40
- */
41
38
  function initialGridSettings(query) {
42
- // Nullable because one caller resolves its query from a route param and may not have one yet;
43
- // an unsorted grid is the right fallback, not a crash.
44
- const sortColumns = (query?.sortColumns || []).map(sc => ({
45
- property: sc.property,
46
- direction: sc.direction === 'desc' ? 'descending' : 'ascending',
47
- }));
48
- return new DatatableSettings({
49
- perPage: { values: SPARK_GRID_PAGE_SIZES, selected: SPARK_GRID_PAGE_SIZES[0] },
50
- page: { values: [1], selected: 1 },
51
- sortColumns,
52
- });
39
+ const sortColumns = (query?.sortColumns || []).map((sc) => ({
40
+ property: sc.property,
41
+ direction: sc.direction === "desc" ? "descending" : "ascending"
42
+ }));
43
+ return new DatatableSettings({
44
+ perPage: {
45
+ values: SPARK_GRID_PAGE_SIZES,
46
+ selected: SPARK_GRID_PAGE_SIZES[0]
47
+ },
48
+ page: {
49
+ values: [1],
50
+ selected: 1
51
+ },
52
+ sortColumns
53
+ });
53
54
  }
54
- /** Whether a query renders as a virtual-scrolling grid rather than a paged one. */
55
55
  function isVirtualScrollingQuery(query) {
56
- return query?.renderMode === 'VirtualScrolling';
57
- }
58
-
59
- /**
60
- * Renderer lookup and lookup-reference loading for a Spark grid.
61
- *
62
- * This was extracted when `spark-query-list` and `spark-sub-query` were two components holding
63
- * byte-identical copies of it — around 120 lines between them. That duplication was not a
64
- * tidiness complaint: it produced drift. The two copies disagreed about `[indeterminate]`, about
65
- * resetting permission state, about whether a fetch failure surfaces or is swallowed, and about
66
- * virtual-scroll sizing — four user-visible bugs, each fixed on one side and not the other.
67
- *
68
- * There is now one grid, {@link SparkQueryGridComponent}, so the drift cannot recur. This stays
69
- * separate because it is stateless service-shaped logic rather than view state, and because it is
70
- * what a custom grid would need in order to render Spark cells at all.
71
- */
72
- class SparkGridRenderers {
73
- registry = inject(SPARK_ATTRIBUTE_RENDERERS);
74
- sparkService = inject(SparkService);
75
- /** The registered column component for a column, or null to fall back to the default cell. */
76
- columnComponentFor(column) {
77
- if (!column.renderer)
78
- return null;
79
- return this.registry.find(r => r.name === column.renderer)?.columnComponent ?? null;
80
- }
81
- /** The registered filter-label function for a column, or null to use the server-computed text. */
82
- filterLabelFor(column) {
83
- if (!column.renderer)
84
- return null;
85
- return this.registry.find(r => r.name === column.renderer)?.filterLabel ?? null;
86
- }
87
- /**
88
- * Inputs for a query-grid cell renderer, filtered to what the component actually declares —
89
- * `NgComponentOutlet` throws on an input the target does not have, which is what lets every
90
- * member of the renderer contract be optional.
91
- */
92
- columnInputsFor(component, item, column) {
93
- return this.cellInputsFor(component, cellValue(valueFor(item, column.name)), column, item);
94
- }
95
- /**
96
- * The same contract for a row that is not a query result — an AsDetail sub-table row, which is
97
- * a plain dictionary of embedded values with no id and no column metadata of its own, so its
98
- * value cannot be read the way {@link columnInputsFor} reads one.
99
- *
100
- * Only the extraction differs; the renderer contract is identical, and stating it once is what
101
- * stops the two from drifting the way the cell markup already had.
102
- */
103
- cellInputsFor(component, value, column, item) {
104
- return withDeclaredInputs(component, {
105
- value,
106
- column,
107
- options: column.rendererOptions,
108
- item,
109
- });
110
- }
111
- /**
112
- * Loads every lookup reference the visible columns need, in one pass.
113
- *
114
- * Returns an empty map rather than throwing when there are none, so callers never branch on it.
115
- */
116
- async loadLookupOptions(columns) {
117
- const lookupColumns = columns.filter(c => c.lookupReferenceType);
118
- if (lookupColumns.length === 0)
119
- return {};
120
- const names = [...new Set(lookupColumns.map(c => c.lookupReferenceType))];
121
- const entries = await Promise.all(names.map(async (name) => [name, await this.sparkService.getLookupReference(name)]));
122
- return entries.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {});
123
- }
124
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkGridRenderers, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
125
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkGridRenderers, providedIn: 'root' });
56
+ return query?.renderMode === "VirtualScrolling";
126
57
  }
127
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkGridRenderers, decorators: [{
128
- type: Injectable,
129
- args: [{ providedIn: 'root' }]
130
- }] });
131
-
132
- /**
133
- * Header slots for `<spark-query-card>`.
134
- *
135
- * Each directive marks a template that REPLACES one region of the card header. Supplying one
136
- * leaves the other two alone — that is the whole point of having three rather than the single
137
- * whole-header template they replace, where changing the icon meant re-implementing the caption
138
- * and the action bar as well.
139
- *
140
- * An omitted slot is not an empty slot: the card renders its default. This is what keeps the
141
- * auto-rendered sub-query working. A sub-query is rendered from `EntityTypeDefinition.Queries`
142
- * with no host to project into, so a mechanism that only ever REPLACED chrome would leave that
143
- * call site with nothing — which is why an earlier design had the query declare its own chrome
144
- * server-side. Overriding a default needs no host; replacing one does.
145
- *
146
- * The two ends of that are visible in the defaults themselves. Neither `SparkQuery` nor
147
- * `EntityType` carries an icon, so the server has nothing to say and the slot is the only
148
- * source. Actions are the mirror image: the server declares them per type, so the default is
149
- * the real answer and the slot is a rare override.
150
- *
151
- * Naming follows ng-bootstrap's `*bsDatatableColumn` convention — prefix, component, slot —
152
- * with `spark`, since these are ng-spark directives and `bs` is another package's prefix. These
153
- * are the first attribute directives in ng-spark; every existing selector is an element.
154
- *
155
- * ## Targeting one query
156
- *
157
- * Each slot takes an optional query alias or id as its value. A detail page renders one card per
158
- * entry in `EntityTypeDefinition.Queries`, so a host that supplies a bare slot would decorate
159
- * every one of them identically — rarely what is wanted. A targeted slot applies to that query;
160
- * an untargeted one is the fallback for the rest.
161
- *
162
- * ```html
163
- * <span *sparkQueryIcon="'cars'"><spark-icon name="car-front" /></span>
164
- * <span *sparkQueryIcon><spark-icon name="table" /></span>
165
- * ```
166
- *
167
- * Matching is case-insensitive, against the query's alias and then its id. This is the
168
- * `bsPriorityNavItem` shape: a value input aliased to the selector, collected with
169
- * `contentChildren`.
170
- */
171
- /** Resolves a slot list to the one that applies to a query: targeted first, then untargeted. */
58
+ var SparkGridRenderers = class SparkGridRenderers {
59
+ registry = inject(SPARK_ATTRIBUTE_RENDERERS);
60
+ sparkService = inject(SparkService);
61
+ columnComponentFor(column) {
62
+ if (!column.renderer) return null;
63
+ return this.registry.find((r) => r.name === column.renderer)?.columnComponent ?? null;
64
+ }
65
+ filterLabelFor(column) {
66
+ if (!column.renderer) return null;
67
+ return this.registry.find((r) => r.name === column.renderer)?.filterLabel ?? null;
68
+ }
69
+ columnInputsFor(component, item, column) {
70
+ return this.cellInputsFor(component, cellValue(valueFor(item, column.name)), column, item);
71
+ }
72
+ cellInputsFor(component, value, column, item) {
73
+ return withDeclaredInputs(component, {
74
+ value,
75
+ column,
76
+ options: column.rendererOptions,
77
+ item
78
+ });
79
+ }
80
+ async loadLookupOptions(columns) {
81
+ const lookupColumns = columns.filter((c) => c.lookupReferenceType);
82
+ if (lookupColumns.length === 0) return {};
83
+ const names = [...new Set(lookupColumns.map((c) => c.lookupReferenceType))];
84
+ return (await Promise.all(names.map(async (name) => [name, await this.sparkService.getLookupReference(name)]))).reduce((acc, [k, v]) => ({
85
+ ...acc,
86
+ [k]: v
87
+ }), {});
88
+ }
89
+ static ɵfac = i0.ɵɵngDeclareFactory({
90
+ minVersion: "12.0.0",
91
+ version: "22.2.0",
92
+ ngImport: i0,
93
+ type: SparkGridRenderers,
94
+ deps: [],
95
+ target: i0.ɵɵFactoryTarget.Injectable
96
+ });
97
+ static ɵprov = i0.ɵɵngDeclareInjectable({
98
+ minVersion: "12.0.0",
99
+ version: "22.2.0",
100
+ ngImport: i0,
101
+ type: SparkGridRenderers,
102
+ providedIn: "root"
103
+ });
104
+ };
105
+ i0.ɵɵngDeclareClassMetadata({
106
+ minVersion: "12.0.0",
107
+ version: "22.2.0",
108
+ ngImport: i0,
109
+ type: SparkGridRenderers,
110
+ decorators: [{
111
+ type: Injectable,
112
+ args: [{ providedIn: "root" }]
113
+ }]
114
+ });
172
115
  function slotFor(slots, query) {
173
- if (!slots.length)
174
- return null;
175
- const matches = (target) => !!target && (target.localeCompare(query?.alias ?? '', undefined, { sensitivity: 'accent' }) === 0 ||
176
- target.localeCompare(query?.id ?? '', undefined, { sensitivity: 'accent' }) === 0);
177
- // A targeted slot wins over the catch-all even when it is declared later: "this one query"
178
- // is the more specific statement, and declaration order in a template is not a priority.
179
- return slots.find(s => matches(s.forQuery())) ?? slots.find(s => !s.forQuery()) ?? null;
180
- }
181
- /**
182
- * The icon at the header's leading edge. No default: nothing in the model describes one.
183
- *
184
- * ```html
185
- * <spark-query-card [queryId]="'cars'">
186
- * <spark-icon *sparkQueryIcon name="car-front" />
187
- * </spark-query-card>
188
- * ```
189
- */
190
- class SparkQueryIconDirective {
191
- templateRef = inject(TemplateRef);
192
- /** Query alias or id this slot applies to. Empty targets every query on the page. */
193
- forQuery = input('', { ...(ngDevMode ? { debugName: "forQuery" } : /* istanbul ignore next */ {}), alias: 'sparkQueryIcon' });
194
- static ngTemplateContextGuard(_dir, ctx) {
195
- return true;
196
- }
197
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
198
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: SparkQueryIconDirective, isStandalone: true, selector: "[sparkQueryIcon]", inputs: { forQuery: { classPropertyName: "forQuery", publicName: "sparkQueryIcon", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
199
- }
200
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryIconDirective, decorators: [{
201
- type: Directive,
202
- args: [{ selector: '[sparkQueryIcon]' }]
203
- }], propDecorators: { forQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkQueryIcon", required: false }] }] } });
204
- /**
205
- * The header caption. Defaults to the query's translated description, falling back to its name.
206
- *
207
- * The context carries that resolved default as `$implicit`, so a host can decorate it — add a
208
- * count, a badge — without re-resolving the translation itself.
209
- */
210
- class SparkQueryCaptionDirective {
211
- templateRef = inject(TemplateRef);
212
- forQuery = input('', { ...(ngDevMode ? { debugName: "forQuery" } : /* istanbul ignore next */ {}), alias: 'sparkQueryCaption' });
213
- static ngTemplateContextGuard(_dir, ctx) {
214
- return true;
215
- }
216
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryCaptionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
217
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: SparkQueryCaptionDirective, isStandalone: true, selector: "[sparkQueryCaption]", inputs: { forQuery: { classPropertyName: "forQuery", publicName: "sparkQueryCaption", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
218
- }
219
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryCaptionDirective, decorators: [{
220
- type: Directive,
221
- args: [{ selector: '[sparkQueryCaption]' }]
222
- }], propDecorators: { forQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkQueryCaption", required: false }] }] } });
223
- /**
224
- * The buttons at the header's trailing edge. Defaults to the server-declared custom actions.
225
- *
226
- * The context carries those actions and the current selection, so a host that wants its own
227
- * buttons ALONGSIDE the server's can render both rather than choosing. Without that, overriding
228
- * this slot to add one button would silently drop every action the type declares — and the
229
- * server's actions are the ones carrying `selectionRule` and the permission filter.
230
- *
231
- * ```html
232
- * <ng-container *sparkQueryActions="let actions; selection as rows">
233
- * <button (click)="export(rows)">Export</button>
234
- * </ng-container>
235
- * ```
236
- */
237
- class SparkQueryActionsDirective {
238
- templateRef = inject(TemplateRef);
239
- forQuery = input('', { ...(ngDevMode ? { debugName: "forQuery" } : /* istanbul ignore next */ {}), alias: 'sparkQueryActions' });
240
- static ngTemplateContextGuard(_dir, ctx) {
241
- return true;
242
- }
243
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryActionsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
244
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: SparkQueryActionsDirective, isStandalone: true, selector: "[sparkQueryActions]", inputs: { forQuery: { classPropertyName: "forQuery", publicName: "sparkQueryActions", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
116
+ if (!slots.length) return null;
117
+ const matches = (target) => !!target && (target.localeCompare(query?.alias ?? "", void 0, { sensitivity: "accent" }) === 0 || target.localeCompare(query?.id ?? "", void 0, { sensitivity: "accent" }) === 0);
118
+ return slots.find((s) => matches(s.forQuery())) ?? slots.find((s) => !s.forQuery()) ?? null;
245
119
  }
246
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryActionsDirective, decorators: [{
247
- type: Directive,
248
- args: [{ selector: '[sparkQueryActions]' }]
249
- }], propDecorators: { forQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkQueryActions", required: false }] }] } });
250
- /** Context for the icon slot: the query whose card is being rendered. */
251
- class SparkQuerySlotContext {
252
- $implicit = null;
253
- }
254
- class SparkQueryCaptionContext {
255
- /** The caption the card would have rendered: description, or the query name. */
256
- $implicit = '';
257
- query = null;
258
- }
259
- class SparkQueryActionsContext {
260
- /** The custom actions the card would have rendered, already filtered for this query. */
261
- $implicit = [];
262
- /** Rows currently ticked. Empty unless an action is selection-gated. */
263
- selection = [];
264
- query = null;
265
- }
266
-
267
- /**
268
- * One cell of a Spark table, wherever that table is.
269
- *
270
- * ## Why this exists
271
- *
272
- * The cell markup was written out three times: twice in the query grids (since merged into
273
- * {@link SparkQueryGridComponent}) and once more in the AsDetail table on the PO detail page. The
274
- * third copy was never counted, and it had already drifted — a `boolean` column rendered the text
275
- * `"true"` there while the grid rendered a checkbox, a `color` rendered its hex string rather than
276
- * a swatch, and a custom renderer was dispatched by a second, hand-copied lookup.
277
- *
278
- * ## What it does and does not own
279
- *
280
- * It owns **presentation**: which control a `dataType` becomes, and dispatching a declared custom
281
- * renderer. It does not own **value resolution**, and deliberately so — the two callers read from
282
- * genuinely different row models. A query row is a `PersistentObject` with an attribute list and
283
- * an id; an AsDetail row is a plain dictionary of embedded values with neither. Trying to unify
284
- * that would put a second row-access path inside this component and buy nothing.
285
- *
286
- * So the caller resolves the value with whichever pipe fits its row model, and passes the result.
287
- *
288
- * ## The two links are not the same link, and only one is here
289
- *
290
- * `link` is for a cell whose own value points at another entity — an AsDetail reference column.
291
- * The query grid's first-column link is a different rule (it points at the row's own detail page,
292
- * gated on `Read`), and it stays in the grid, wrapping this component. Passing both would nest
293
- * anchors, which is invalid HTML; the grid therefore leaves `link` unset.
294
- */
295
- class SparkGridCellComponent {
296
- renderers = inject(SparkGridRenderers);
297
- /** The column being rendered. Its `dataType` and `renderer` decide everything below. */
298
- column = input.required(/* @ts-ignore */
299
- ...(ngDevMode ? [{ debugName: "column" }] : /* istanbul ignore next */ []));
300
- /**
301
- * What the caller's value pipe produced: the display text for most columns, and the raw value
302
- * for `boolean` and `color`, which need the value rather than a rendering of it.
303
- */
304
- display = input(null, /* @ts-ignore */
305
- ...(ngDevMode ? [{ debugName: "display" }] : /* istanbul ignore next */ []));
306
- /**
307
- * The value handed to a declared custom renderer.
308
- *
309
- * Separate from {@link display} because they are not the same thing: a renderer receives the
310
- * underlying value — for AsDetail, the nested object itself — while `display` has already been
311
- * flattened to something printable.
312
- */
313
- rendererValue = input(null, /* @ts-ignore */
314
- ...(ngDevMode ? [{ debugName: "rendererValue" }] : /* istanbul ignore next */ []));
315
- /** The row, passed through to a custom renderer as `item`. */
316
- item = input(null, /* @ts-ignore */
317
- ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
318
- /**
319
- * Pre-resolved chips for a reference-array column, or empty when the column is not one.
320
- *
321
- * Resolved by the caller because the label source differs: a query row carries a per-id
322
- * breadcrumb map, while an AsDetail row's `__sparkBreadcrumbs` is keyed by column name only and
323
- * so cannot label the members of an array. Rather than render a row of raw ids, an AsDetail
324
- * caller passes nothing and the cell falls through to text.
325
- */
326
- chips = input([], /* @ts-ignore */
327
- ...(ngDevMode ? [{ debugName: "chips" }] : /* istanbul ignore next */ []));
328
- /** Route for a cell whose value references another entity. See the class comment. */
329
- link = input(null, /* @ts-ignore */
330
- ...(ngDevMode ? [{ debugName: "link" }] : /* istanbul ignore next */ []));
331
- rendererComponent = computed(() => this.renderers.columnComponentFor(this.column()), /* @ts-ignore */
332
- ...(ngDevMode ? [{ debugName: "rendererComponent" }] : /* istanbul ignore next */ []));
333
- rendererInputs = computed(() => {
334
- const component = this.rendererComponent();
335
- return component
336
- ? this.renderers.cellInputsFor(component, this.rendererValue(), this.column(), this.item())
337
- : {};
338
- }, /* @ts-ignore */
339
- ...(ngDevMode ? [{ debugName: "rendererInputs" }] : /* istanbul ignore next */ []));
340
- /** Null and undefined are distinct from `false` here — see the template. */
341
- isUnsetBoolean = computed(() => this.display() == null, /* @ts-ignore */
342
- ...(ngDevMode ? [{ debugName: "isUnsetBoolean" }] : /* istanbul ignore next */ []));
343
- /**
344
- * {@link display} when it really is a `Date`, and `null` otherwise.
345
- *
346
- * Angular's `DatePipe` does not degrade on a non-date: it **throws**
347
- * `NG02100: InvalidPipeArgument`, which would take down the whole cell. Both cell pipes
348
- * deliberately fall back to the raw text for an unparseable date rather than hiding the stored
349
- * value, so that text reaches this component and must not be piped. A template cannot express
350
- * `instanceof`, hence the computed.
351
- */
352
- dateValue = computed(() => {
353
- const value = this.display();
354
- return value instanceof Date && !Number.isNaN(value.getTime()) ? value : null;
355
- }, /* @ts-ignore */
356
- ...(ngDevMode ? [{ debugName: "dateValue" }] : /* istanbul ignore next */ []));
357
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkGridCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
358
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkGridCellComponent, isStandalone: true, selector: "spark-grid-cell", inputs: { column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, display: { classPropertyName: "display", publicName: "display", isSignal: true, isRequired: false, transformFunction: null }, rendererValue: { classPropertyName: "rendererValue", publicName: "rendererValue", isSignal: true, isRequired: false, transformFunction: null }, item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: false, transformFunction: null }, chips: { classPropertyName: "chips", publicName: "chips", isSignal: true, isRequired: false, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (rendererComponent(); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: rendererInputs()\"></ng-container>\n} @else if (column().dataType === 'boolean') {\n <!-- [indeterminate] matters: without it a null boolean renders as an unchecked box, which reads\n as an explicit `false`. One of the three copies of this cell bound it and the others had\n drifted, so the same column said different things on a detail page and in a grid. -->\n <input type=\"checkbox\"\n [checked]=\"display() === true\"\n [indeterminate]=\"isUnsetBoolean()\"\n disabled\n onclick=\"return false;\">\n} @else if (column().dataType === 'date' || column().dataType === 'datetime') {\n <!--\n Formatted in ONE place for both grids. Before this, a date column printed whatever its pipe\n produced, which was the raw ISO string \u2014 `2026-09-07T13:25:57.3119825Z` in a table cell. Both\n cell pipes now hand over a real Date, and `date:` accepts a string too, so an unparseable value\n that fell back to text still renders as that text rather than blank.\n -->\n @if (dateValue(); as when) {\n {{ when | date:(column().dataType === 'date' ? 'mediumDate' : 'short') }}\n } @else {\n {{ display() }}\n }\n} @else if (column().dataType === 'color') {\n @if (display(); as colorVal) {\n <!-- Shares `.spark-color-swatch` with the detail page, which is where the sizing already\n lived. It used to be inline here because the grid rendered inside <mp-datatable>'s\n shadow root, where neither this component's scoped rules nor Bootstrap's arrived;\n ng-bootstrap 22.18.0 moved the datatable to the light DOM, so the class reaches it. -->\n <span class=\"spark-color-swatch d-inline-block align-middle border rounded\"\n [style.background-color]=\"colorVal\"></span>\n }\n} @else if (column().dataType === 'image') {\n @if (display(); as src) {\n <!-- `alt` is empty on purpose \u2014 the cell is decorative next to the row's own labelled\n columns, and a filename read aloud is worse than nothing. -->\n <img [src]=\"src\" alt=\"\" loading=\"lazy\" class=\"spark-grid-image align-middle rounded\">\n }\n} @else if (column().dataType === 'url') {\n @if (display(); as href) {\n <!-- rel is not optional with target=_blank: without noopener the opened page gets a handle on\n this one through window.opener, and these hrefs are data. -->\n <a [href]=\"href\" target=\"_blank\" rel=\"noopener noreferrer\">{{ href }}</a>\n }\n} @else if (chips().length) {\n <span class=\"d-inline-flex flex-wrap gap-1\">\n @for (chip of chips(); track chip.id) {\n <span class=\"spark-chip\">{{ chip.label }}</span>\n }\n </span>\n} @else if (link(); as route) {\n <a [routerLink]=\"route\">{{ display() }}</a>\n} @else {\n {{ display() }}\n}\n", styles: [".spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}.spark-grid-image{max-height:2.5em;max-width:100%;object-fit:contain}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "pipe", type: i1.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
359
- }
360
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkGridCellComponent, decorators: [{
361
- type: Component,
362
- args: [{ selector: 'spark-grid-cell', imports: [CommonModule, NgComponentOutlet, RouterModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (rendererComponent(); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: rendererInputs()\"></ng-container>\n} @else if (column().dataType === 'boolean') {\n <!-- [indeterminate] matters: without it a null boolean renders as an unchecked box, which reads\n as an explicit `false`. One of the three copies of this cell bound it and the others had\n drifted, so the same column said different things on a detail page and in a grid. -->\n <input type=\"checkbox\"\n [checked]=\"display() === true\"\n [indeterminate]=\"isUnsetBoolean()\"\n disabled\n onclick=\"return false;\">\n} @else if (column().dataType === 'date' || column().dataType === 'datetime') {\n <!--\n Formatted in ONE place for both grids. Before this, a date column printed whatever its pipe\n produced, which was the raw ISO string \u2014 `2026-09-07T13:25:57.3119825Z` in a table cell. Both\n cell pipes now hand over a real Date, and `date:` accepts a string too, so an unparseable value\n that fell back to text still renders as that text rather than blank.\n -->\n @if (dateValue(); as when) {\n {{ when | date:(column().dataType === 'date' ? 'mediumDate' : 'short') }}\n } @else {\n {{ display() }}\n }\n} @else if (column().dataType === 'color') {\n @if (display(); as colorVal) {\n <!-- Shares `.spark-color-swatch` with the detail page, which is where the sizing already\n lived. It used to be inline here because the grid rendered inside <mp-datatable>'s\n shadow root, where neither this component's scoped rules nor Bootstrap's arrived;\n ng-bootstrap 22.18.0 moved the datatable to the light DOM, so the class reaches it. -->\n <span class=\"spark-color-swatch d-inline-block align-middle border rounded\"\n [style.background-color]=\"colorVal\"></span>\n }\n} @else if (column().dataType === 'image') {\n @if (display(); as src) {\n <!-- `alt` is empty on purpose \u2014 the cell is decorative next to the row's own labelled\n columns, and a filename read aloud is worse than nothing. -->\n <img [src]=\"src\" alt=\"\" loading=\"lazy\" class=\"spark-grid-image align-middle rounded\">\n }\n} @else if (column().dataType === 'url') {\n @if (display(); as href) {\n <!-- rel is not optional with target=_blank: without noopener the opened page gets a handle on\n this one through window.opener, and these hrefs are data. -->\n <a [href]=\"href\" target=\"_blank\" rel=\"noopener noreferrer\">{{ href }}</a>\n }\n} @else if (chips().length) {\n <span class=\"d-inline-flex flex-wrap gap-1\">\n @for (chip of chips(); track chip.id) {\n <span class=\"spark-chip\">{{ chip.label }}</span>\n }\n </span>\n} @else if (link(); as route) {\n <a [routerLink]=\"route\">{{ display() }}</a>\n} @else {\n {{ display() }}\n}\n", styles: [".spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}.spark-grid-image{max-height:2.5em;max-width:100%;object-fit:contain}\n"] }]
363
- }], propDecorators: { column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], display: [{ type: i0.Input, args: [{ isSignal: true, alias: "display", required: false }] }], rendererValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "rendererValue", required: false }] }], item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: false }] }], chips: [{ type: i0.Input, args: [{ isSignal: true, alias: "chips", required: false }] }], link: [{ type: i0.Input, args: [{ isSignal: true, alias: "link", required: false }] }] } });
364
-
365
- /**
366
- * The one Spark grid: a `<bs-datatable>` rendering a query or a sub-query.
367
- *
368
- * This replaces two components that were the same grid written twice — and, counting
369
- * `spark-query-list`'s streaming and paged branches, the `<bs-datatable>` element itself written
370
- * three times. The duplication had already produced four user-visible bugs, each fixed on one
371
- * side and not the other: `[indeterminate]` on null booleans, permission state surviving a failed
372
- * reload, a swallowed fetch failure, and virtual-scroll sizing. `SparkGridRenderers` was
373
- * extracted to stop that and could only take the stateless helpers with it; this takes the rest.
374
- *
375
- * ## What it deliberately does NOT own: streaming
376
- *
377
- * `spark-grid-renderers.ts` records why — a websocket dependency graph must not reach the bundle
378
- * of every PO detail page, none of which stream. So rows can come from **either** side:
379
- *
380
- * - unbound `data` — the grid fetches for itself, paging and sorting server-side;
381
- * - bound `data` — the grid renders what it is given and never fetches.
382
- *
383
- * `spark-query-list` keeps the socket and feeds its snapshot in. This is the line `bs-datatable`
384
- * itself draws: `[data]` and `[fetch]` are mutually exclusive, and binding both makes `[fetch]`
385
- * win silently.
386
- *
387
- * ## Reading its state
388
- *
389
- * `query`, `entityType`, `customActions`, `selection`, `canRead`, `canCreate` and `resultCount`
390
- * are readable so a host can render chrome around the grid. Read them through a template
391
- * reference variable — `<spark-query-grid #grid>` … `grid.query()` — not `viewChild`: hosts wrap
392
- * grids in `@if`, where a `viewChild` is intermittently undefined.
393
- */
394
- class SparkQueryGridComponent {
395
- sparkService = inject(SparkService);
396
- gridRenderers = inject(SparkGridRenderers);
397
- queryRefresh = inject(SparkQueryRefreshService);
398
- lang = inject(SparkLanguageService);
399
- /** Query alias or id. */
400
- queryId = input.required(/* @ts-ignore */
401
- ...(ngDevMode ? [{ debugName: "queryId" }] : /* istanbul ignore next */ []));
402
- /**
403
- * The parent persistent object this query is scoped to, when it has one.
404
- *
405
- * Optional, because not every query is a detail of something: a page can host a grid that
406
- * stands on its own — "my accounts", a dashboard list — and the server already treats an absent
407
- * parent as "no parent" rather than as an error. Requiring these made that shape impossible to
408
- * express: the component simply never loaded, with no request, no error and no log.
409
- *
410
- * Pass both or neither. One without the other is ignored, matching `SparkService.executeQuery`,
411
- * which omits either param when it is falsy, and the execute endpoint, which resolves a parent
412
- * only when both are present.
413
- */
414
- parentId = input('', /* @ts-ignore */
415
- ...(ngDevMode ? [{ debugName: "parentId" }] : /* istanbul ignore next */ []));
416
- parentType = input('', /* @ts-ignore */
417
- ...(ngDevMode ? [{ debugName: "parentType" }] : /* istanbul ignore next */ []));
418
- /**
419
- * Rows supplied from outside. When bound, the grid renders these and runs no fetch.
420
- *
421
- * This is how streaming stays out of this component (see the class comment). `null` — the
422
- * default — means "fetch for yourself"; an empty array means "you have been given no rows",
423
- * which is a different statement and renders an empty grid rather than triggering a load.
424
- */
425
- data = input(null, /* @ts-ignore */
426
- ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
427
- /**
428
- * Server-side search term, passed through to the query.
429
- *
430
- * An input rather than a box, so the host owns the control and the grid owns the request. A
431
- * host with client-side rows filters them itself before binding `data`.
432
- */
433
- search = input('', /* @ts-ignore */
434
- ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
435
- /**
436
- * Column metadata for externally supplied rows. Required whenever `data` is bound, because a
437
- * projection cannot describe itself — there is no per-row metadata left to infer it from.
438
- */
439
- columns = input(null, /* @ts-ignore */
440
- ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
441
- /**
442
- * Change this to re-run the query. Any value works; only its identity matters.
443
- *
444
- * A declarative token rather than only a `reload()` method, because calling a method means
445
- * holding a component handle, and hosts wrap this grid in `@if`, where a `viewChild` is
446
- * intermittently undefined.
447
- *
448
- * This drives the CHEAP refresh (see {@link reload}). It deliberately does not feed the main
449
- * effect: re-running `loadData` would re-resolve the query, the entity types, the permissions
450
- * and the lookups, and reset the user's page and sort on every button press.
451
- */
452
- reloadToken = input(null, /* @ts-ignore */
453
- ...(ngDevMode ? [{ debugName: "reloadToken" }] : /* istanbul ignore next */ []));
454
- /**
455
- * Paging and sorting. Two-way, so a host driving `data` can sort those rows by whatever the
456
- * user actually clicked — the datatable writes the new sort back through here.
457
- */
458
- settings = model(new DatatableSettings({
459
- perPage: { values: SPARK_GRID_PAGE_SIZES, selected: SPARK_GRID_PAGE_SIZES[0] },
460
- page: { values: [1], selected: 1 },
461
- sortColumns: [],
462
- }), /* @ts-ignore */
463
- ...(ngDevMode ? [{ debugName: "settings" }] : /* istanbul ignore next */ []));
464
- /** Rows the user has ticked. Two-way so chrome outside the grid can read and clear them. */
465
- selection = model([], /* @ts-ignore */
466
- ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
467
- /**
468
- * Replaces where the first column's link points, per row.
469
- *
470
- * Return `null` to suppress the link for that row — useful when a grid mixes rows that have a
471
- * detail page with rows that do not.
472
- *
473
- * ⚠️ **It changes the destination, not the permission.** `canRead()` still gates whether any
474
- * link is rendered at all, and this function is never consulted when that gate is closed. It
475
- * cannot be used to reach a row the rights model withheld; it exists for the case where the
476
- * row's natural destination is not `/po/{type}/{id}` — a composed row that maps to a page of
477
- * its own, or a grid embedded in a route that owns its own child paths.
478
- */
479
- rowRoute = input(null, /* @ts-ignore */
480
- ...(ngDevMode ? [{ debugName: "rowRoute" }] : /* istanbul ignore next */ []));
481
- /** Emitted whenever a load or a page fetch fails, for a host in bespoke chrome. */
482
- error = output();
483
- rowClicked = output();
484
- customActionExecuted = output();
485
- colors = Color;
486
- /**
487
- * Where the first column's link points for this row: the host's `rowRoute` when it supplied one,
488
- * otherwise the row's own detail page.
489
- *
490
- * A method rather than a computed because the answer is per row, and the template already
491
- * iterates rows — a computed would have to be a map keyed by id, rebuilt on every page.
492
- */
493
- rowRouteFor(row) {
494
- const custom = this.rowRoute();
495
- if (custom)
496
- return custom(row);
497
- const type = this.entityType();
498
- // ⚠️ A composed type has no per-row detail page by construction: its rows are computed, and
499
- // `{Name}Actions.OnLoadAsync` serves ONE page which is free to ignore the id it was given. So
500
- // /po/{type}/{rowId} does not 404 — it silently renders that same page, which is worse, because
501
- // nothing looks wrong. This branch is live in DemoApp today: Read/StartPage implies
502
- // Query/StartPage, so the composed grid renders with canRead() true and every row linked.
503
- //
504
- // `clrType` is absent exactly for those types and is already on the wire; the grid simply never
505
- // consulted it. A host whose composed rows DO have a page says so with `rowRoute`.
506
- if (!type?.clrType)
507
- return null;
508
- return ['/po', type.alias || type.id, row.id];
509
- }
510
- query = signal(null, /* @ts-ignore */
511
- ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
512
- entityType = signal(null, /* @ts-ignore */
513
- ...(ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []));
514
- allEntityTypes = signal([], /* @ts-ignore */
515
- ...(ngDevMode ? [{ debugName: "allEntityTypes" }] : /* istanbul ignore next */ []));
516
- lookupReferenceOptions = signal({}, /* @ts-ignore */
517
- ...(ngDevMode ? [{ debugName: "lookupReferenceOptions" }] : /* istanbul ignore next */ []));
518
- loading = signal(true, /* @ts-ignore */
519
- ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
520
- canRead = signal(false, /* @ts-ignore */
521
- ...(ngDevMode ? [{ debugName: "canRead" }] : /* istanbul ignore next */ []));
522
- canCreate = signal(false, /* @ts-ignore */
523
- ...(ngDevMode ? [{ debugName: "canCreate" }] : /* istanbul ignore next */ []));
524
- resultCount = signal(null, /* @ts-ignore */
525
- ...(ngDevMode ? [{ debugName: "resultCount" }] : /* istanbul ignore next */ []));
526
- customActions = signal([], /* @ts-ignore */
527
- ...(ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []));
528
- /** Names the server withheld for this RESULT; see QueryResult.disabledActions. */
529
- disabledActions = signal([], /* @ts-ignore */
530
- ...(ngDevMode ? [{ debugName: "disabledActions" }] : /* istanbul ignore next */ []));
531
- /**
532
- * The actions actually offered for this result.
533
- *
534
- * The catalogue at `/spark/actions/{objectTypeId}` is per TYPE and is never told what an
535
- * execution returned, so an action that applies to only some results cannot be filtered there.
536
- * A custom query withholds what does not apply via `CustomQueryArgs.DisableActions(...)`, and
537
- * the answer arrives on the result.
538
- *
539
- * An affordance, not a permission -- the action handler still refuses on its own terms.
540
- */
541
- visibleCustomActions = computed(() => {
542
- const withheld = this.disabledActions();
543
- if (!withheld.length)
544
- return this.customActions();
545
- const lowered = new Set(withheld.map(name => name.toLowerCase()));
546
- return this.customActions().filter(action => !lowered.has(action.name.toLowerCase()));
547
- }, /* @ts-ignore */
548
- ...(ngDevMode ? [{ debugName: "visibleCustomActions" }] : /* istanbul ignore next */ []));
549
- fetchFn = signal(null, /* @ts-ignore */
550
- ...(ngDevMode ? [{ debugName: "fetchFn" }] : /* istanbul ignore next */ []));
551
- /**
552
- * Why the component renders its own failure instead of only reporting one.
553
- *
554
- * `SparkService` is a bare `firstValueFrom` passthrough with no interceptor, so every failure
555
- * surfaces here and nowhere else. A host embedding this grid cannot surface what it never sees,
556
- * and the default has to be visible with no host cooperation — hence a rendered alert, not just
557
- * an output.
558
- */
559
- errorMessage = signal(null, /* @ts-ignore */
560
- ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
561
- /** 'none' unless an action is selection-gated, so unaffected grids gain no checkbox column. */
562
- selectionMode = computed(() => selectionModeFor(this.customActions()), /* @ts-ignore */
563
- ...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
564
- isVirtualScrolling = computed(() => isVirtualScrollingQuery(this.query()), /* @ts-ignore */
565
- ...(ngDevMode ? [{ debugName: "isVirtualScrolling" }] : /* istanbul ignore next */ []));
566
- /**
567
- * Columns as sent with the most recent fetched page.
568
- *
569
- * A host binding `data` (streaming) supplies its own via the `columns` input instead — the
570
- * snapshot carries them once, exactly as a paged result does.
571
- */
572
- fetchedColumns = signal([], /* @ts-ignore */
573
- ...(ngDevMode ? [{ debugName: "fetchedColumns" }] : /* istanbul ignore next */ []));
574
- /** Every column on the wire, drawn or not — what renderers and lookups resolve against. */
575
- allColumns = computed(() => this.columns() ?? this.fetchedColumns(), /* @ts-ignore */
576
- ...(ngDevMode ? [{ debugName: "allColumns" }] : /* istanbul ignore next */ []));
577
- /**
578
- * The columns the grid draws. `isVisible: false` ships a value without a column, so a renderer
579
- * can read a sibling the grid does not show; undefined means visible, so a server that predates
580
- * the field still draws everything.
581
- */
582
- visibleColumns = computed(() => this.allColumns().filter(c => c.isVisible !== false), /* @ts-ignore */
583
- ...(ngDevMode ? [{ debugName: "visibleColumns" }] : /* istanbul ignore next */ []));
584
- /**
585
- * True when rows do not come from this component's own fetch.
586
- *
587
- * Either because the host bound `data`, or because the query streams — a streaming query's rows
588
- * arrive over a socket and `/execute` is not a way to get them. The grid recognises the second
589
- * case itself rather than waiting to be told, because it resolves the query before the host can
590
- * see it: left to `data` alone, every streaming grid would fire one pointless fetch on mount,
591
- * before the host had anything to bind.
592
- */
593
- hasExternalData = computed(() => this.data() !== null || this.query()?.isStreamingQuery === true, /* @ts-ignore */
594
- ...(ngDevMode ? [{ debugName: "hasExternalData" }] : /* istanbul ignore next */ []));
595
- /** Whether an action's selection rule is satisfied right now. The server checks it again. */
596
- isActionEnabled(action) {
597
- return parseSelectionRule(action.selectionRule)(this.selection().map(r => r.id).length);
598
- }
599
- constructor() {
600
- effect(() => {
601
- const qId = this.queryId();
602
- const pId = this.parentId();
603
- const pType = this.parentType();
604
- // Only the query id is required. Requiring a parent here is what made a standalone grid
605
- // silently render nothing.
606
- if (qId) {
607
- this.loadData(qId, pId, pType);
608
- }
609
- else {
610
- // No query id at all. `loading` starts true, so without this the component would spin
611
- // forever instead of saying anything.
612
- this.loading.set(false);
613
- }
614
- });
615
- // Separate effect, so the token drives the cheap refresh and never the full metadata reload.
616
- // `first` skips the initial run: the effect above has already fetched, and reacting to the
617
- // token's starting value would double-fetch on mount.
618
- let first = true;
619
- effect(() => {
620
- // Both the host's token and the server's refreshQuery drive the same cheap refresh.
621
- this.reloadToken();
622
- this.queryRefresh.tokenFor(this.queryId());
623
- if (first) {
624
- first = false;
625
- return;
626
- }
627
- untracked(() => this.reload());
628
- });
629
- // A new search term must refetch even when page, perPage and sort are unchanged — the
630
- // datatable dedupes reloads by exactly that triple, and a new fetch identity is what resets
631
- // its dedupe key. Skipping the first run keeps mount to a single request.
632
- let firstSearch = true;
633
- effect(() => {
634
- this.search();
635
- if (firstSearch) {
636
- firstSearch = false;
637
- return;
638
- }
639
- untracked(() => this.onSearchChanged());
640
- });
641
- }
642
- /**
643
- * Re-run the query, keeping the current page, sort and scroll position.
644
- *
645
- * Data-level on purpose: it re-seeds the fetch closure and nothing else. For a definition
646
- * change — new columns, a renamed query — the inputs themselves must change; that is the
647
- * expensive path. Inert when rows come from the host: there is nothing here to re-run.
648
- */
649
- reload() {
650
- if (this.hasExternalData())
651
- return;
652
- const q = this.query();
653
- if (q)
654
- this.fetchFn.set(this.makeFetch(q, this.parentId(), this.parentType()));
655
- }
656
- onSearchChanged() {
657
- if (this.hasExternalData())
658
- return;
659
- const s = this.settings();
660
- // Back to page 1: the old page number means nothing against a different result set.
661
- this.settings.set(new DatatableSettings({
662
- perPage: { values: s.perPage.values, selected: s.perPage.selected },
663
- page: { values: [1], selected: 1 },
664
- sortColumns: s.sortColumns,
665
- }));
666
- this.reload();
667
- }
668
- /**
669
- * Replaces the fetched columns only when they actually differ.
670
- *
671
- * Columns ship **once per result**, so every page fetch returns an identical set. Assigning it
672
- * unconditionally wrote a new array of new objects into the signal each time, re-rendering every
673
- * header and re-evaluating every `*bsDatatableColumn` input for no change in content.
674
- *
675
- * Compared by serialized value rather than by reference: the objects are freshly deserialized from
676
- * JSON on every response, so reference equality is always false and would defeat the check. A
677
- * dozen small objects per fetch is nothing beside the request that produced them.
678
- *
679
- * ⚠️ This was written believing it fixed a panel-teardown bug (#431 F7). Measured in a browser: it
680
- * does not. The columns were already identical on every fetch, so the guard hits every time and the
681
- * teardown had another cause entirely. Kept because avoiding a pointless re-render of every header
682
- * on every page is worth having on its own — but it is an optimization, not a fix, and the F7
683
- * question remains open.
684
- */
685
- setFetchedColumns(columns) {
686
- const current = this.fetchedColumns();
687
- if (current.length === columns.length && JSON.stringify(current) === JSON.stringify(columns))
688
- return;
689
- this.fetchedColumns.set(columns);
690
- }
691
- /**
692
- * The per-column filters currently applied (#431).
693
- *
694
- * Read inside `makeFetch`'s closure, like `search`, so a change refetches by producing a new fetch
695
- * identity rather than by being captured.
696
- */
697
- filters = signal([], /* @ts-ignore */
698
- ...(ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []));
699
- /**
700
- * The value source behind every column's panel.
701
- *
702
- * ⚠️ **Referentially stable, and that is the whole point.** It reads `filters()` when the datatable
703
- * *calls* it, not when it is built — so it always sees the current filters while never changing
704
- * identity.
705
- *
706
- * This was a `computed` first, on the reasoning that it had to close over the current filters
707
- * because `DistinctsRequest` carries only `{ column, search, signal }`. That was wrong, and the way
708
- * it was wrong is worth keeping: a new identity reassigns `[distincts]` on the element, and the
709
- * component aborts the in-flight request and bumps a generation counter — so a response already on
710
- * its way is discarded as stale and the list renders empty over a perfectly good answer.
711
- *
712
- * Reading the signal at call time removes the race entirely and is simpler besides.
713
- *
714
- * The asked-for column's own filter is excluded: a panel must offer the values you could still
715
- * pick, not only the ones you already picked.
716
- */
717
- /**
718
- * The last complete distinct list seen per column, for translating a `< none >` selection.
719
- *
720
- * ⚠️ Only stored when the server reported the list was NOT truncated (`hasMore === false`). The
721
- * translation below is the complement of what is listed, so a partial list would exclude the wrong
722
- * set — and silently, which is the failure mode this whole feature exists to remove.
723
- */
724
- completeDistincts = new Map();
725
- distinctsFn = async (request) => {
726
- const queryId = this.query()?.id;
727
- if (!queryId)
728
- return null;
729
- // Read at call time — never captured.
730
- const others = this.filters().filter(f => f.name !== request.column);
731
- // The server's shape IS the datatable's shape, so there is nothing to map.
732
- const result = await this.sparkService.getDistinctValues(queryId, request.column, {
733
- search: request.search,
734
- columns: others,
735
- parentId: this.parentId(),
736
- parentType: this.parentType(),
737
- });
738
- // A searched list is a subset by construction, so it is never a basis for a complement.
739
- if (result && !result.hasMore && !request.search)
740
- this.completeDistincts.set(request.column, result.matching.map(v => v.value));
741
- else
742
- this.completeDistincts.delete(request.column);
743
- return result;
744
- };
745
- /**
746
- * Translates one filter change into the wire shape and refetches.
747
- *
748
- * The event always arrives as the `'values'` shape because the panel only ever calls
749
- * `ctx.apply(...)`; comparison mode is never declared. `inverse` is a flag on the event and two
750
- * arrays on the wire — the translation is this method's whole job.
751
- */
752
- onFilterChange(detail) {
753
- if (detail.mode !== 'values')
754
- return;
755
- const values = detail.selected.map(v => v.value);
756
- const next = this.filters().filter(f => f.name !== detail.column);
757
- if (values.length > 0) {
758
- // ⚠️ `< none >` cannot be asked for directly. The server compares against the index term, and
759
- // RavenDB gives a field the document never wrote NO term at all — so `Col == null` matches a
760
- // field written as JSON null but NOT one that is absent, while `Col != null` puts the absent row
761
- // with the rows that HAVE a value. Both rows render blank, the panel offers one entry for the
762
- // pair, and no predicate selects that pair. Measured in AbsentVersusNullFieldTests.
763
- //
764
- // The complement is expressible, though: "none of the real values" catches absent and null
765
- // alike. So a selection containing the null entry is sent as an exclusion of everything NOT
766
- // selected, which is exactly the same set and needs no server change.
767
- const complete = this.completeDistincts.get(detail.column);
768
- const selectsNone = values.some(v => v === null || v === undefined);
769
- if (selectsNone && !detail.inverse && complete) {
770
- const unselected = complete.filter(v => v !== null && v !== undefined && !values.some(s => s === v));
771
- // Everything is selected, so the filter narrows nothing — drop it rather than sending an
772
- // empty exclusion, which would read as "exclude nothing" only by accident.
773
- if (unselected.length > 0)
774
- next.push({ name: detail.column, excludes: unselected });
775
- }
776
- else {
777
- next.push(detail.inverse
778
- ? { name: detail.column, excludes: values }
779
- : { name: detail.column, includes: values });
780
- }
781
- }
782
- this.filters.set(next);
783
- this.onFilterChanged();
784
- }
785
- /**
786
- * Back to page 1 and a fresh fetch identity, exactly as a new search term does.
787
- *
788
- * Both halves matter: the old page number means nothing against a different result set, and the
789
- * datatable dedupes reloads by `(page, perPage, sort)` — none of which a filter changes — so
790
- * without a new fetch identity the request is silently never made.
791
- */
792
- onFilterChanged() {
793
- if (this.hasExternalData())
794
- return;
795
- const s = this.settings();
796
- this.settings.set(new DatatableSettings({
797
- perPage: { values: s.perPage.values, selected: s.perPage.selected },
798
- page: { values: [1], selected: 1 },
799
- sortColumns: s.sortColumns,
800
- }));
801
- this.reload();
802
- }
803
- async onCustomAction(action) {
804
- if (action.confirmationMessageKey) {
805
- const message = this.lang.t(action.confirmationMessageKey) || 'Are you sure?';
806
- if (!confirm(message))
807
- return;
808
- }
809
- try {
810
- // The sub-query's container travels with the action (#327). Without it an action invoked
811
- // from a company's Cars tab could not tell it was on a company's page at all — the grid knew
812
- // (it filters the rows by that parent) and simply dropped the fact on the way out.
813
- //
814
- // Sent as parentId/parentType, NOT as the `parent` argument: that one means an object of this
815
- // action's OWN type and is loaded under it server-side, so handing it a Company id for a Car
816
- // action refuses — correctly, and confusingly.
817
- const pId = this.parentId();
818
- const pType = this.parentType();
819
- await this.sparkService.executeCustomAction(this.entityType().id, action.name, undefined, this.selection().map(r => r.id), pId && pType ? { id: pId, type: pType } : undefined,
820
- // The query too: the server re-runs it narrowed to these ids, so the action receives the
821
- // rows this grid rendered rather than a re-derivation from documents.
822
- this.query()?.id);
823
- this.customActionExecuted.emit({ action });
824
- if (action.refreshOnCompleted)
825
- this.reload();
826
- }
827
- catch (e) {
828
- const err = e;
829
- this.errorMessage.set(err.error?.error || err.message || this.lang.t('common.actionFailed') || 'Action failed');
830
- }
831
- }
832
- reportError(err) {
833
- this.errorMessage.set(this.describe(err));
834
- this.error.emit(err);
835
- }
836
- /**
837
- * A 404 is deliberately generic.
838
- *
839
- * `Endpoints/Queries/Get.cs` answers 404 with byte-identical bodies for "no such query" and
840
- * "you may not see it", so existence is not disclosed (security audit M-3). The component
841
- * therefore genuinely cannot tell them apart, and both "Not found" and "Access denied" would be
842
- * a guess — one of them leaking, the other misleading.
843
- */
844
- describe(err) {
845
- if (err?.status === 404) {
846
- return this.lang.t('spark.query.unavailable') || 'This list is not available.';
847
- }
848
- return err?.error?.error || err?.message
849
- || this.lang.t('common.unexpectedError') || 'An unexpected error occurred';
850
- }
851
- async loadData(queryId, parentId, parentType) {
852
- this.loading.set(true);
853
- this.errorMessage.set(null);
854
- this.fetchFn.set(null);
855
- // Reset everything derived from the previous query, not just the fetch. Leaving
856
- // `entityType`/`canRead` behind let a failed reload build a row link out of the PREVIOUS type
857
- // and the previous permission.
858
- this.query.set(null);
859
- this.entityType.set(null);
860
- this.canRead.set(false);
861
- this.canCreate.set(false);
862
- this.customActions.set([]);
863
- this.resultCount.set(null);
864
- // Ids from the previous query are meaningless against the next one, and would be POSTed as
865
- // though they belonged to it.
866
- this.selection.set([]);
867
- // A filter belongs to the query it was applied to. Surviving a query switch would POST it
868
- // against the next query, where the column may not exist or may mean something else entirely.
869
- this.filters.set([]);
870
- try {
871
- const [resolvedQuery, entityTypes] = await Promise.all([
872
- this.sparkService.getQuery(queryId),
873
- this.sparkService.getEntityTypes(),
874
- ]);
875
- this.query.set(resolvedQuery);
876
- this.allEntityTypes.set(entityTypes);
877
- const et = this.resolveEntityType(resolvedQuery, entityTypes);
878
- this.entityType.set(et);
879
- if (et) {
880
- const [permissions, actions] = await Promise.all([
881
- this.sparkService.getPermissions(et.id),
882
- this.sparkService.getCustomActions(et.id),
883
- ]);
884
- this.canRead.set(permissions.canRead);
885
- this.canCreate.set(permissions.canCreate);
886
- // 'query', not 'list'. The server model has always documented "detail" | "query" |
887
- // "both"; a filter testing for a value nothing emits renders the action NOWHERE.
888
- this.customActions.set(filterQueryActions(actions));
889
- }
890
- this.settings.set(initialGridSettings(resolvedQuery));
891
- // The datatable drives paging/sorting via [(settings)] and calls fetchFn per page. Virtual
892
- // scrolling is just the [virtualScroll] template flag.
893
- // `resolvedQuery`, not `hasExternalData()`: the query signal is set above, but reading the
894
- // computed here would depend on signal-read ordering inside an async method. Ask the value
895
- // directly.
896
- if (this.data() === null && !resolvedQuery?.isStreamingQuery) {
897
- this.fetchFn.set(this.makeFetch(resolvedQuery, parentId, parentType));
898
- }
899
- this.loadLookupReferenceOptions();
900
- }
901
- catch (e) {
902
- this.fetchFn.set(null);
903
- this.reportError(e);
904
- }
905
- finally {
906
- this.loading.set(false);
907
- }
908
- }
909
- /**
910
- * The query's entity type.
911
- *
912
- * The source-name fallback is not optional garnish: a `Database.*` query need not declare
913
- * `entityType`, and the sub-query grid — which matched on the declared name alone — rendered
914
- * those as an empty card with no columns, no rows and no error, while the identical query
915
- * rendered correctly as a page. Both grids are this one now, so there is one answer.
916
- */
917
- resolveEntityType(query, entityTypes) {
918
- if (!query)
919
- return null;
920
- if (query.entityType) {
921
- return entityTypes.find(t => t.name === query.entityType || t.alias === query.entityType?.toLowerCase()) ?? null;
922
- }
923
- const sourceName = extractSourceName(query.source);
924
- const singular = singularize(sourceName);
925
- // `name + 's'` because the "-es" rule over-strips (Vehicles -> "Vehicl", Roles -> "Rol"); the
926
- // page resolver in spark-query-list has always matched this way.
927
- return entityTypes.find(t => t.name === sourceName || t.name === singular || t.name + 's' === sourceName || t.clrType?.endsWith(singular)) ?? null;
928
- }
929
- makeFetch(query, parentId, parentType) {
930
- return (req) => this.sparkService.executeQuery(query.id, {
931
- sortColumns: req.sortColumns,
932
- skip: (req.page - 1) * req.perPage,
933
- take: req.perPage,
934
- search: this.search() || undefined,
935
- // Read inside the closure, exactly as `search` is: what forces a refetch is the fetch
936
- // identity `reload()` creates, not the value captured when this closure was built.
937
- columns: this.filters(),
938
- parentId, parentType,
939
- }).then(r => {
940
- this.errorMessage.set(null);
941
- this.resultCount.set(r.totalItems);
942
- // Columns come from the result now, not from the entity type: the server decides the query
943
- // surface (ShowedOn.Query) and is the same place the sort-column allow-list is checked.
944
- // ?? [] because a malformed or older response must render an empty grid, not throw inside a
945
- // computed — where the stack points at the column filter and not at the response that lacked them.
946
- this.setFetchedColumns(r.columns ?? []);
947
- // Per-result, so it is re-read on every page rather than latched from the first.
948
- this.disabledActions.set(r.disabledActions ?? []);
949
- return {
950
- data: r.items,
951
- totalRecords: r.totalItems,
952
- totalPages: Math.ceil(r.totalItems / req.perPage) || 1,
953
- perPage: req.perPage,
954
- page: req.page,
955
- };
956
- }).catch((e) => {
957
- // Report before returning the empty page, or a failed fetch is indistinguishable from a
958
- // query that legitimately has no rows.
959
- this.reportError(e);
960
- this.resultCount.set(0);
961
- return { data: [], totalRecords: 0, totalPages: 1, perPage: req.perPage, page: req.page };
962
- });
963
- }
964
- async loadLookupReferenceOptions() {
965
- // Over ALL columns, not just the drawn ones: an undrawn column's value can still be rendered
966
- // by a renderer reading it as a sibling, and it would resolve to a raw key without its options.
967
- this.lookupReferenceOptions.set(await this.gridRenderers.loadLookupOptions(this.allColumns()));
968
- }
969
- /**
970
- * The underlying value a custom renderer receives, which is not the printable one.
971
- *
972
- * A projection is flat, so this is the cell's own value rather than a display string. It is
973
- * deliberately not the attribute-shaped `rendererValue`: a query row has no nested object to
974
- * fall back to, and reaching for one would hand every renderer `undefined`.
975
- */
976
- rendererValueFor(item, column) {
977
- return cellValue(valueFor(item, column.name));
978
- }
979
- getColumnRendererComponent(column) {
980
- return this.gridRenderers.columnComponentFor(column);
981
- }
982
- /** The renderer's optional filter-label override for this column, or null for the server text. */
983
- filterLabelFor(column) {
984
- return this.gridRenderers.filterLabelFor(column);
985
- }
986
- getColumnRendererInputs(component, item, column) {
987
- return this.gridRenderers.columnInputsFor(component, item, column);
988
- }
989
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
990
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkQueryGridComponent, isStandalone: true, selector: "spark-query-grid", inputs: { queryId: { classPropertyName: "queryId", publicName: "queryId", isSignal: true, isRequired: true, transformFunction: null }, parentId: { classPropertyName: "parentId", publicName: "parentId", isSignal: true, isRequired: false, transformFunction: null }, parentType: { classPropertyName: "parentType", publicName: "parentType", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, search: { classPropertyName: "search", publicName: "search", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, reloadToken: { classPropertyName: "reloadToken", publicName: "reloadToken", isSignal: true, isRequired: false, transformFunction: null }, settings: { classPropertyName: "settings", publicName: "settings", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, rowRoute: { classPropertyName: "rowRoute", publicName: "rowRoute", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { settings: "settingsChange", selection: "selectionChange", error: "error", rowClicked: "rowClicked", customActionExecuted: "customActionExecuted" }, ngImport: i0, template: "<!--\n Three explicit states, in this order, and the order is load-bearing.\n\n This template used to open with `@if (query(); as q)` wrapping EVERYTHING, and `query()` is only\n set after loadData's awaits resolve. Three separate bugs fell out of that one line: the spinner\n was unreachable on a first load (it rendered inside the gate, while `loading` starts true and\n `query` starts null), a first-load failure rendered ZERO DOM \u2014 no card, no message, nothing to\n see or report \u2014 and a failed RELOAD left the previous query's rows on screen.\n\n So: `loading` is checked first and outside any dependency on `query`, and the final `@else`\n always renders something. Do not fold these back into one gate, and do not move the spinner\n inside the `query()` branch.\n-->\n@if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n} @else if (query()) {\n <!-- A page-fetch failure while the grid is already resolved. Without this the catch returns an\n empty page and a failed query is indistinguishable from one that legitimately has no rows. -->\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n\n <!--\n One datatable, two row sources. `[data]` and `[fetch]` are mutually exclusive in\n ng-bootstrap \u2014 binding both makes `[fetch]` win silently \u2014 so exactly one is ever non-null\n here: `fetchFn` is left null whenever `data` is bound. See the class comment for why rows can\n arrive from outside at all (streaming must not put a websocket in every detail page's bundle).\n -->\n <bs-datatable\n [selectionMode]=\"selectionMode()\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"data()\"\n [fetch]=\"fetchFn()\"\n [distincts]=\"distinctsFn\"\n (filterChange)=\"onFilterChange($event)\"\n [(settings)]=\"settings\">\n @for (col of visibleColumns(); track col.name) {\n <!--\n `canSort`/`canFilter` arrive already resolved by the server (query override \u2192 attribute \u2192\n true), so this only spells the polarity. `!== false` is needed for BOTH, for opposite\n reasons: absent means capable on our wire, while the datatable's own `filterable` defaults\n to false, unlike `sortable`.\n\n \u26A0\uFE0F `filterActive` and `filterSummary` are deliberately NOT bound. Any *bsDatatableColumn\n input change recomputes the column set, which retires the live filter view \u2014 while the web\n component's mount-once guard leaves the panel's nodes in place and never re-renders them.\n Binding them to state our own panel mutates would freeze the panel the instant a value is\n ticked, until it is closed and reopened. The cost is that a filtered column carries no\n visual marker yet, which is worth far less than a panel that dies on first use. See #431 F7.\n -->\n <div *bsDatatableColumn=\"col.name; sortable: col.canSort !== false; filterable: col.canFilter !== false\">\n {{ (col.label | resolveTranslation) || col.name }}\n <spark-attribute-description [description]=\"col.description\" />\n\n @if (col.canFilter !== false) {\n <ng-container *bsDatatableFilterPanel=\"let values; let ctx = ctx\">\n <spark-column-filter-panel\n [column]=\"col\"\n [values]=\"values()\"\n [ctx]=\"ctx\"\n [filterLabel]=\"filterLabelFor(col)\" />\n </ng-container>\n }\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (col of visibleColumns(); track col.name; let first = $first) {\n <td>\n @if (row) {\n <!-- The first column links to the row's detail page. Two things about this are\n routinely misread:\n\n 1. `cellContent` is projected INSIDE the anchor, so a custom `renderer` does NOT\n suppress this link \u2014 a renderer that emits its own <a> produces nested anchors\n (invalid HTML) rather than replacing this one.\n 2. `canRead()` is the whole gate, and it is the rights model: `Query` without\n `Read` lists the rows and withholds the link. Withhold `Read` for a query\n whose rows no detail page can load \u2014 a Custom.* query that fabricates rows in\n memory, say.\n 3. `rowRoute` replaces the DESTINATION only, never the gate: returning a route\n from it cannot make a row clickable that `canRead()` withheld, and returning\n null from it suppresses the link for that one row. -->\n @if (first && canRead() && rowRouteFor(row); as route) {\n <a [routerLink]=\"route\"\n (click)=\"rowClicked.emit(row)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n }\n } @else {\n &nbsp;\n }\n </td>\n }\n </ng-container>\n </bs-datatable>\n} @else {\n <!-- The query could not be resolved. Rendering something keeps the host's layout intact and,\n crucially, makes the failure visible: this branch used to render nothing at all. -->\n <bs-alert [type]=\"colors.danger\" class=\"mb-0\">\n {{ errorMessage() || ('spark.query.unavailable' | t) }}\n </bs-alert>\n}\n\n<ng-template #cellContent let-item let-col=\"col\">\n <!--\n Presentation lives in <spark-grid-cell>, shared with the AsDetail table on the PO detail page.\n Value resolution stays here: a query row is a PersistentObject with an attribute list, and the\n AsDetail table's rows are plain dictionaries, so only the rendering is common.\n\n `link` is deliberately not passed. The first-column link is the grid's own rule and wraps this\n template from outside; setting both would nest anchors.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(col | queryCellValue:item:lookupReferenceOptions())\"\n [rendererValue]=\"rendererValueFor(item, col)\"\n [item]=\"item\"\n [chips]=\"(col | queryReferenceChips:item)\" />\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: BsAlertComponent, selector: "bs-alert", inputs: ["type", "announce", "isVisible"], outputs: ["isVisibleChange", "afterOpenedOrClosed"] }, { kind: "component", type: BsDatatableComponent, selector: "bs-datatable", inputs: ["columns", "data", "fetch", "distincts", "labels", "settings", "selectionMode", "selectable", "selection", "rowKey", "resizableColumns", "pagination", "virtualScroll", "itemSize", "virtualBuffer", "isResponsive", "compareWith", "tree", "idKey", "childCountKey", "treeIndent", "expandedIds", "selectionStrategy"], outputs: ["settingsChange", "selectionChange", "rowClick", "rowDblClick", "rowContextMenu", "expandedIdsChange", "rowExpand", "rowCollapse", "filterChange"] }, { kind: "directive", type: BsDatatableColumnDirective, selector: "[bsDatatableColumn]", inputs: ["bsDatatableColumn", "bsDatatableColumnSortable", "bsDatatableColumnFilterable", "bsDatatableColumnFilterActive", "bsDatatableColumnFilterSummary", "bsDatatableColumnFilterSelection", "bsDatatableColumnFilterMode", "bsDatatableColumnFilterInputType", "bsDatatableColumnFilterOperators"] }, { kind: "directive", type: BsDatatableFilterPanelDirective, selector: "[bsDatatableFilterPanel]" }, { kind: "directive", type: BsRowTemplateDirective, selector: "[bsRowTemplate]" }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: SparkGridCellComponent, selector: "spark-grid-cell", inputs: ["column", "display", "rendererValue", "item", "chips", "link"] }, { kind: "component", type: SparkAttributeDescriptionComponent, selector: "spark-attribute-description", inputs: ["description", "position"] }, { kind: "component", type: SparkColumnFilterPanelComponent, selector: "spark-column-filter-panel", inputs: ["column", "values", "ctx", "filterLabel"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: QueryCellValuePipe, name: "queryCellValue" }, { kind: "pipe", type: QueryReferenceChipsPipe, name: "queryReferenceChips" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
991
- }
992
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryGridComponent, decorators: [{
993
- type: Component,
994
- args: [{ selector: 'spark-query-grid', imports: [CommonModule, RouterModule, BsAlertComponent, BsDatatableComponent, BsDatatableColumnDirective, BsDatatableFilterPanelDirective, BsRowTemplateDirective, BsSpinnerComponent, SparkGridCellComponent, ResolveTranslationPipe, QueryCellValuePipe, QueryReferenceChipsPipe, TranslateKeyPipe, SparkAttributeDescriptionComponent, SparkColumnFilterPanelComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n Three explicit states, in this order, and the order is load-bearing.\n\n This template used to open with `@if (query(); as q)` wrapping EVERYTHING, and `query()` is only\n set after loadData's awaits resolve. Three separate bugs fell out of that one line: the spinner\n was unreachable on a first load (it rendered inside the gate, while `loading` starts true and\n `query` starts null), a first-load failure rendered ZERO DOM \u2014 no card, no message, nothing to\n see or report \u2014 and a failed RELOAD left the previous query's rows on screen.\n\n So: `loading` is checked first and outside any dependency on `query`, and the final `@else`\n always renders something. Do not fold these back into one gate, and do not move the spinner\n inside the `query()` branch.\n-->\n@if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n} @else if (query()) {\n <!-- A page-fetch failure while the grid is already resolved. Without this the catch returns an\n empty page and a failed query is indistinguishable from one that legitimately has no rows. -->\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n\n <!--\n One datatable, two row sources. `[data]` and `[fetch]` are mutually exclusive in\n ng-bootstrap \u2014 binding both makes `[fetch]` win silently \u2014 so exactly one is ever non-null\n here: `fetchFn` is left null whenever `data` is bound. See the class comment for why rows can\n arrive from outside at all (streaming must not put a websocket in every detail page's bundle).\n -->\n <bs-datatable\n [selectionMode]=\"selectionMode()\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"data()\"\n [fetch]=\"fetchFn()\"\n [distincts]=\"distinctsFn\"\n (filterChange)=\"onFilterChange($event)\"\n [(settings)]=\"settings\">\n @for (col of visibleColumns(); track col.name) {\n <!--\n `canSort`/`canFilter` arrive already resolved by the server (query override \u2192 attribute \u2192\n true), so this only spells the polarity. `!== false` is needed for BOTH, for opposite\n reasons: absent means capable on our wire, while the datatable's own `filterable` defaults\n to false, unlike `sortable`.\n\n \u26A0\uFE0F `filterActive` and `filterSummary` are deliberately NOT bound. Any *bsDatatableColumn\n input change recomputes the column set, which retires the live filter view \u2014 while the web\n component's mount-once guard leaves the panel's nodes in place and never re-renders them.\n Binding them to state our own panel mutates would freeze the panel the instant a value is\n ticked, until it is closed and reopened. The cost is that a filtered column carries no\n visual marker yet, which is worth far less than a panel that dies on first use. See #431 F7.\n -->\n <div *bsDatatableColumn=\"col.name; sortable: col.canSort !== false; filterable: col.canFilter !== false\">\n {{ (col.label | resolveTranslation) || col.name }}\n <spark-attribute-description [description]=\"col.description\" />\n\n @if (col.canFilter !== false) {\n <ng-container *bsDatatableFilterPanel=\"let values; let ctx = ctx\">\n <spark-column-filter-panel\n [column]=\"col\"\n [values]=\"values()\"\n [ctx]=\"ctx\"\n [filterLabel]=\"filterLabelFor(col)\" />\n </ng-container>\n }\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (col of visibleColumns(); track col.name; let first = $first) {\n <td>\n @if (row) {\n <!-- The first column links to the row's detail page. Two things about this are\n routinely misread:\n\n 1. `cellContent` is projected INSIDE the anchor, so a custom `renderer` does NOT\n suppress this link \u2014 a renderer that emits its own <a> produces nested anchors\n (invalid HTML) rather than replacing this one.\n 2. `canRead()` is the whole gate, and it is the rights model: `Query` without\n `Read` lists the rows and withholds the link. Withhold `Read` for a query\n whose rows no detail page can load \u2014 a Custom.* query that fabricates rows in\n memory, say.\n 3. `rowRoute` replaces the DESTINATION only, never the gate: returning a route\n from it cannot make a row clickable that `canRead()` withheld, and returning\n null from it suppresses the link for that one row. -->\n @if (first && canRead() && rowRouteFor(row); as route) {\n <a [routerLink]=\"route\"\n (click)=\"rowClicked.emit(row)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n }\n } @else {\n &nbsp;\n }\n </td>\n }\n </ng-container>\n </bs-datatable>\n} @else {\n <!-- The query could not be resolved. Rendering something keeps the host's layout intact and,\n crucially, makes the failure visible: this branch used to render nothing at all. -->\n <bs-alert [type]=\"colors.danger\" class=\"mb-0\">\n {{ errorMessage() || ('spark.query.unavailable' | t) }}\n </bs-alert>\n}\n\n<ng-template #cellContent let-item let-col=\"col\">\n <!--\n Presentation lives in <spark-grid-cell>, shared with the AsDetail table on the PO detail page.\n Value resolution stays here: a query row is a PersistentObject with an attribute list, and the\n AsDetail table's rows are plain dictionaries, so only the rendering is common.\n\n `link` is deliberately not passed. The first-column link is the grid's own rule and wraps this\n template from outside; setting both would nest anchors.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(col | queryCellValue:item:lookupReferenceOptions())\"\n [rendererValue]=\"rendererValueFor(item, col)\"\n [item]=\"item\"\n [chips]=\"(col | queryReferenceChips:item)\" />\n</ng-template>\n" }]
995
- }], ctorParameters: () => [], propDecorators: { queryId: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryId", required: true }] }], parentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentId", required: false }] }], parentType: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentType", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], search: [{ type: i0.Input, args: [{ isSignal: true, alias: "search", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], reloadToken: [{ type: i0.Input, args: [{ isSignal: true, alias: "reloadToken", required: false }] }], settings: [{ type: i0.Input, args: [{ isSignal: true, alias: "settings", required: false }] }, { type: i0.Output, args: ["settingsChange"] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }, { type: i0.Output, args: ["selectionChange"] }], rowRoute: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowRoute", required: false }] }], error: [{ type: i0.Output, args: ["error"] }], rowClicked: [{ type: i0.Output, args: ["rowClicked"] }], customActionExecuted: [{ type: i0.Output, args: ["customActionExecuted"] }] } });
120
+ var SparkQueryIconDirective = class SparkQueryIconDirective {
121
+ templateRef = inject(TemplateRef);
122
+ forQuery = input("", {
123
+ ...ngDevMode ? { debugName: "forQuery" } : /* istanbul ignore next */ {},
124
+ alias: "sparkQueryIcon"
125
+ });
126
+ static ngTemplateContextGuard(_dir, ctx) {
127
+ return true;
128
+ }
129
+ static ɵfac = i0.ɵɵngDeclareFactory({
130
+ minVersion: "12.0.0",
131
+ version: "22.2.0",
132
+ ngImport: i0,
133
+ type: SparkQueryIconDirective,
134
+ deps: [],
135
+ target: i0.ɵɵFactoryTarget.Directive
136
+ });
137
+ static ɵdir = i0.ɵɵngDeclareDirective({
138
+ minVersion: "17.1.0",
139
+ version: "22.2.0",
140
+ type: SparkQueryIconDirective,
141
+ isStandalone: true,
142
+ selector: "[sparkQueryIcon]",
143
+ inputs: { forQuery: {
144
+ classPropertyName: "forQuery",
145
+ publicName: "sparkQueryIcon",
146
+ isSignal: true,
147
+ isRequired: false,
148
+ transformFunction: null
149
+ } },
150
+ ngImport: i0
151
+ });
152
+ };
153
+ i0.ɵɵngDeclareClassMetadata({
154
+ minVersion: "12.0.0",
155
+ version: "22.2.0",
156
+ ngImport: i0,
157
+ type: SparkQueryIconDirective,
158
+ decorators: [{
159
+ type: Directive,
160
+ args: [{ selector: "[sparkQueryIcon]" }]
161
+ }],
162
+ propDecorators: { forQuery: [{
163
+ type: i0.Input,
164
+ args: [{
165
+ isSignal: true,
166
+ alias: "sparkQueryIcon",
167
+ required: false
168
+ }]
169
+ }] }
170
+ });
171
+ var SparkQueryCaptionDirective = class SparkQueryCaptionDirective {
172
+ templateRef = inject(TemplateRef);
173
+ forQuery = input("", {
174
+ ...ngDevMode ? { debugName: "forQuery" } : /* istanbul ignore next */ {},
175
+ alias: "sparkQueryCaption"
176
+ });
177
+ static ngTemplateContextGuard(_dir, ctx) {
178
+ return true;
179
+ }
180
+ static ɵfac = i0.ɵɵngDeclareFactory({
181
+ minVersion: "12.0.0",
182
+ version: "22.2.0",
183
+ ngImport: i0,
184
+ type: SparkQueryCaptionDirective,
185
+ deps: [],
186
+ target: i0.ɵɵFactoryTarget.Directive
187
+ });
188
+ static ɵdir = i0.ɵɵngDeclareDirective({
189
+ minVersion: "17.1.0",
190
+ version: "22.2.0",
191
+ type: SparkQueryCaptionDirective,
192
+ isStandalone: true,
193
+ selector: "[sparkQueryCaption]",
194
+ inputs: { forQuery: {
195
+ classPropertyName: "forQuery",
196
+ publicName: "sparkQueryCaption",
197
+ isSignal: true,
198
+ isRequired: false,
199
+ transformFunction: null
200
+ } },
201
+ ngImport: i0
202
+ });
203
+ };
204
+ i0.ɵɵngDeclareClassMetadata({
205
+ minVersion: "12.0.0",
206
+ version: "22.2.0",
207
+ ngImport: i0,
208
+ type: SparkQueryCaptionDirective,
209
+ decorators: [{
210
+ type: Directive,
211
+ args: [{ selector: "[sparkQueryCaption]" }]
212
+ }],
213
+ propDecorators: { forQuery: [{
214
+ type: i0.Input,
215
+ args: [{
216
+ isSignal: true,
217
+ alias: "sparkQueryCaption",
218
+ required: false
219
+ }]
220
+ }] }
221
+ });
222
+ var SparkQueryActionsDirective = class SparkQueryActionsDirective {
223
+ templateRef = inject(TemplateRef);
224
+ forQuery = input("", {
225
+ ...ngDevMode ? { debugName: "forQuery" } : /* istanbul ignore next */ {},
226
+ alias: "sparkQueryActions"
227
+ });
228
+ static ngTemplateContextGuard(_dir, ctx) {
229
+ return true;
230
+ }
231
+ static ɵfac = i0.ɵɵngDeclareFactory({
232
+ minVersion: "12.0.0",
233
+ version: "22.2.0",
234
+ ngImport: i0,
235
+ type: SparkQueryActionsDirective,
236
+ deps: [],
237
+ target: i0.ɵɵFactoryTarget.Directive
238
+ });
239
+ static ɵdir = i0.ɵɵngDeclareDirective({
240
+ minVersion: "17.1.0",
241
+ version: "22.2.0",
242
+ type: SparkQueryActionsDirective,
243
+ isStandalone: true,
244
+ selector: "[sparkQueryActions]",
245
+ inputs: { forQuery: {
246
+ classPropertyName: "forQuery",
247
+ publicName: "sparkQueryActions",
248
+ isSignal: true,
249
+ isRequired: false,
250
+ transformFunction: null
251
+ } },
252
+ ngImport: i0
253
+ });
254
+ };
255
+ i0.ɵɵngDeclareClassMetadata({
256
+ minVersion: "12.0.0",
257
+ version: "22.2.0",
258
+ ngImport: i0,
259
+ type: SparkQueryActionsDirective,
260
+ decorators: [{
261
+ type: Directive,
262
+ args: [{ selector: "[sparkQueryActions]" }]
263
+ }],
264
+ propDecorators: { forQuery: [{
265
+ type: i0.Input,
266
+ args: [{
267
+ isSignal: true,
268
+ alias: "sparkQueryActions",
269
+ required: false
270
+ }]
271
+ }] }
272
+ });
273
+ var SparkQuerySlotContext = class {
274
+ $implicit = null;
275
+ };
276
+ var SparkQueryCaptionContext = class {
277
+ $implicit = "";
278
+ query = null;
279
+ };
280
+ var SparkQueryActionsContext = class {
281
+ $implicit = [];
282
+ selection = [];
283
+ query = null;
284
+ };
285
+ var SparkGridCellComponent = class SparkGridCellComponent {
286
+ renderers = inject(SparkGridRenderers);
287
+ column = input.required(...ngDevMode ? [{ debugName: "column" }] : /* istanbul ignore next */ []);
288
+ display = input(null, ...ngDevMode ? [{ debugName: "display" }] : /* istanbul ignore next */ []);
289
+ rendererValue = input(null, ...ngDevMode ? [{ debugName: "rendererValue" }] : /* istanbul ignore next */ []);
290
+ item = input(null, ...ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []);
291
+ chips = input([], ...ngDevMode ? [{ debugName: "chips" }] : /* istanbul ignore next */ []);
292
+ link = input(null, ...ngDevMode ? [{ debugName: "link" }] : /* istanbul ignore next */ []);
293
+ rendererComponent = computed(() => this.renderers.columnComponentFor(this.column()), ...ngDevMode ? [{ debugName: "rendererComponent" }] : /* istanbul ignore next */ []);
294
+ rendererInputs = computed(() => {
295
+ const component = this.rendererComponent();
296
+ return component ? this.renderers.cellInputsFor(component, this.rendererValue(), this.column(), this.item()) : {};
297
+ }, ...ngDevMode ? [{ debugName: "rendererInputs" }] : /* istanbul ignore next */ []);
298
+ isUnsetBoolean = computed(() => this.display() == null, ...ngDevMode ? [{ debugName: "isUnsetBoolean" }] : /* istanbul ignore next */ []);
299
+ dateValue = computed(() => {
300
+ const value = this.display();
301
+ return value instanceof Date && !Number.isNaN(value.getTime()) ? value : null;
302
+ }, ...ngDevMode ? [{ debugName: "dateValue" }] : /* istanbul ignore next */ []);
303
+ static ɵfac = i0.ɵɵngDeclareFactory({
304
+ minVersion: "12.0.0",
305
+ version: "22.2.0",
306
+ ngImport: i0,
307
+ type: SparkGridCellComponent,
308
+ deps: [],
309
+ target: i0.ɵɵFactoryTarget.Component
310
+ });
311
+ static ɵcmp = i0.ɵɵngDeclareComponent({
312
+ minVersion: "17.0.0",
313
+ version: "22.2.0",
314
+ type: SparkGridCellComponent,
315
+ isStandalone: true,
316
+ selector: "spark-grid-cell",
317
+ inputs: {
318
+ column: {
319
+ classPropertyName: "column",
320
+ publicName: "column",
321
+ isSignal: true,
322
+ isRequired: true,
323
+ transformFunction: null
324
+ },
325
+ display: {
326
+ classPropertyName: "display",
327
+ publicName: "display",
328
+ isSignal: true,
329
+ isRequired: false,
330
+ transformFunction: null
331
+ },
332
+ rendererValue: {
333
+ classPropertyName: "rendererValue",
334
+ publicName: "rendererValue",
335
+ isSignal: true,
336
+ isRequired: false,
337
+ transformFunction: null
338
+ },
339
+ item: {
340
+ classPropertyName: "item",
341
+ publicName: "item",
342
+ isSignal: true,
343
+ isRequired: false,
344
+ transformFunction: null
345
+ },
346
+ chips: {
347
+ classPropertyName: "chips",
348
+ publicName: "chips",
349
+ isSignal: true,
350
+ isRequired: false,
351
+ transformFunction: null
352
+ },
353
+ link: {
354
+ classPropertyName: "link",
355
+ publicName: "link",
356
+ isSignal: true,
357
+ isRequired: false,
358
+ transformFunction: null
359
+ }
360
+ },
361
+ ngImport: i0,
362
+ template: "@if (rendererComponent(); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: rendererInputs()\"></ng-container>\n} @else if (column().dataType === 'boolean') {\n <!-- [indeterminate] matters: without it a null boolean renders as an unchecked box, which reads\n as an explicit `false`. One of the three copies of this cell bound it and the others had\n drifted, so the same column said different things on a detail page and in a grid. -->\n <input type=\"checkbox\"\n [checked]=\"display() === true\"\n [indeterminate]=\"isUnsetBoolean()\"\n disabled\n onclick=\"return false;\">\n} @else if (column().dataType === 'date' || column().dataType === 'datetime') {\n <!--\n Formatted in ONE place for both grids. Before this, a date column printed whatever its pipe\n produced, which was the raw ISO string — `2026-09-07T13:25:57.3119825Z` in a table cell. Both\n cell pipes now hand over a real Date, and `date:` accepts a string too, so an unparseable value\n that fell back to text still renders as that text rather than blank.\n -->\n @if (dateValue(); as when) {\n {{ when | date:(column().dataType === 'date' ? 'mediumDate' : 'short') }}\n } @else {\n {{ display() }}\n }\n} @else if (column().dataType === 'color') {\n @if (display(); as colorVal) {\n <!-- Shares `.spark-color-swatch` with the detail page, which is where the sizing already\n lived. It used to be inline here because the grid rendered inside <mp-datatable>'s\n shadow root, where neither this component's scoped rules nor Bootstrap's arrived;\n ng-bootstrap 22.18.0 moved the datatable to the light DOM, so the class reaches it. -->\n <span class=\"spark-color-swatch d-inline-block align-middle border rounded\"\n [style.background-color]=\"colorVal\"></span>\n }\n} @else if (column().dataType === 'image') {\n @if (display(); as src) {\n <!-- `alt` is empty on purpose — the cell is decorative next to the row's own labelled\n columns, and a filename read aloud is worse than nothing. -->\n <img [src]=\"src\" alt=\"\" loading=\"lazy\" class=\"spark-grid-image align-middle rounded\">\n }\n} @else if (column().dataType === 'url') {\n @if (display(); as href) {\n <!-- rel is not optional with target=_blank: without noopener the opened page gets a handle on\n this one through window.opener, and these hrefs are data. -->\n <a [href]=\"href\" target=\"_blank\" rel=\"noopener noreferrer\">{{ href }}</a>\n }\n} @else if (chips().length) {\n <span class=\"d-inline-flex flex-wrap gap-1\">\n @for (chip of chips(); track chip.id) {\n <span class=\"spark-chip\">{{ chip.label }}</span>\n }\n </span>\n} @else if (link(); as route) {\n <a [routerLink]=\"route\">{{ display() }}</a>\n} @else {\n {{ display() }}\n}\n",
363
+ styles: [".spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}.spark-grid-image{max-height:2.5em;max-width:100%;object-fit:contain}\n"],
364
+ dependencies: [
365
+ {
366
+ kind: "ngmodule",
367
+ type: CommonModule
368
+ },
369
+ {
370
+ kind: "directive",
371
+ type: i1$1.NgComponentOutlet,
372
+ selector: "[ngComponentOutlet]",
373
+ inputs: [
374
+ "ngComponentOutlet",
375
+ "ngComponentOutletInputs",
376
+ "ngComponentOutletInjector",
377
+ "ngComponentOutletEnvironmentInjector",
378
+ "ngComponentOutletContent",
379
+ "ngComponentOutletNgModule"
380
+ ],
381
+ exportAs: ["ngComponentOutlet"]
382
+ },
383
+ {
384
+ kind: "ngmodule",
385
+ type: RouterModule
386
+ },
387
+ {
388
+ kind: "directive",
389
+ type: i2.RouterLink,
390
+ selector: "[routerLink]",
391
+ inputs: [
392
+ "target",
393
+ "queryParams",
394
+ "fragment",
395
+ "queryParamsHandling",
396
+ "state",
397
+ "info",
398
+ "relativeTo",
399
+ "preserveFragment",
400
+ "skipLocationChange",
401
+ "replaceUrl",
402
+ "browserUrl",
403
+ "routerLink"
404
+ ]
405
+ },
406
+ {
407
+ kind: "pipe",
408
+ type: i1$1.DatePipe,
409
+ name: "date"
410
+ }
411
+ ],
412
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
413
+ });
414
+ };
415
+ i0.ɵɵngDeclareClassMetadata({
416
+ minVersion: "12.0.0",
417
+ version: "22.2.0",
418
+ ngImport: i0,
419
+ type: SparkGridCellComponent,
420
+ decorators: [{
421
+ type: Component,
422
+ args: [{
423
+ selector: "spark-grid-cell",
424
+ imports: [
425
+ CommonModule,
426
+ NgComponentOutlet,
427
+ RouterModule
428
+ ],
429
+ changeDetection: ChangeDetectionStrategy.OnPush,
430
+ template: "@if (rendererComponent(); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: rendererInputs()\"></ng-container>\n} @else if (column().dataType === 'boolean') {\n <!-- [indeterminate] matters: without it a null boolean renders as an unchecked box, which reads\n as an explicit `false`. One of the three copies of this cell bound it and the others had\n drifted, so the same column said different things on a detail page and in a grid. -->\n <input type=\"checkbox\"\n [checked]=\"display() === true\"\n [indeterminate]=\"isUnsetBoolean()\"\n disabled\n onclick=\"return false;\">\n} @else if (column().dataType === 'date' || column().dataType === 'datetime') {\n <!--\n Formatted in ONE place for both grids. Before this, a date column printed whatever its pipe\n produced, which was the raw ISO string — `2026-09-07T13:25:57.3119825Z` in a table cell. Both\n cell pipes now hand over a real Date, and `date:` accepts a string too, so an unparseable value\n that fell back to text still renders as that text rather than blank.\n -->\n @if (dateValue(); as when) {\n {{ when | date:(column().dataType === 'date' ? 'mediumDate' : 'short') }}\n } @else {\n {{ display() }}\n }\n} @else if (column().dataType === 'color') {\n @if (display(); as colorVal) {\n <!-- Shares `.spark-color-swatch` with the detail page, which is where the sizing already\n lived. It used to be inline here because the grid rendered inside <mp-datatable>'s\n shadow root, where neither this component's scoped rules nor Bootstrap's arrived;\n ng-bootstrap 22.18.0 moved the datatable to the light DOM, so the class reaches it. -->\n <span class=\"spark-color-swatch d-inline-block align-middle border rounded\"\n [style.background-color]=\"colorVal\"></span>\n }\n} @else if (column().dataType === 'image') {\n @if (display(); as src) {\n <!-- `alt` is empty on purpose — the cell is decorative next to the row's own labelled\n columns, and a filename read aloud is worse than nothing. -->\n <img [src]=\"src\" alt=\"\" loading=\"lazy\" class=\"spark-grid-image align-middle rounded\">\n }\n} @else if (column().dataType === 'url') {\n @if (display(); as href) {\n <!-- rel is not optional with target=_blank: without noopener the opened page gets a handle on\n this one through window.opener, and these hrefs are data. -->\n <a [href]=\"href\" target=\"_blank\" rel=\"noopener noreferrer\">{{ href }}</a>\n }\n} @else if (chips().length) {\n <span class=\"d-inline-flex flex-wrap gap-1\">\n @for (chip of chips(); track chip.id) {\n <span class=\"spark-chip\">{{ chip.label }}</span>\n }\n </span>\n} @else if (link(); as route) {\n <a [routerLink]=\"route\">{{ display() }}</a>\n} @else {\n {{ display() }}\n}\n",
431
+ styles: [".spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}.spark-grid-image{max-height:2.5em;max-width:100%;object-fit:contain}\n"]
432
+ }]
433
+ }],
434
+ propDecorators: {
435
+ column: [{
436
+ type: i0.Input,
437
+ args: [{
438
+ isSignal: true,
439
+ alias: "column",
440
+ required: true
441
+ }]
442
+ }],
443
+ display: [{
444
+ type: i0.Input,
445
+ args: [{
446
+ isSignal: true,
447
+ alias: "display",
448
+ required: false
449
+ }]
450
+ }],
451
+ rendererValue: [{
452
+ type: i0.Input,
453
+ args: [{
454
+ isSignal: true,
455
+ alias: "rendererValue",
456
+ required: false
457
+ }]
458
+ }],
459
+ item: [{
460
+ type: i0.Input,
461
+ args: [{
462
+ isSignal: true,
463
+ alias: "item",
464
+ required: false
465
+ }]
466
+ }],
467
+ chips: [{
468
+ type: i0.Input,
469
+ args: [{
470
+ isSignal: true,
471
+ alias: "chips",
472
+ required: false
473
+ }]
474
+ }],
475
+ link: [{
476
+ type: i0.Input,
477
+ args: [{
478
+ isSignal: true,
479
+ alias: "link",
480
+ required: false
481
+ }]
482
+ }]
483
+ }
484
+ });
485
+ var SparkQueryGridComponent = class SparkQueryGridComponent {
486
+ sparkService = inject(SparkService);
487
+ gridRenderers = inject(SparkGridRenderers);
488
+ queryRefresh = inject(SparkQueryRefreshService);
489
+ lang = inject(SparkLanguageService);
490
+ queryId = input.required(...ngDevMode ? [{ debugName: "queryId" }] : /* istanbul ignore next */ []);
491
+ parentId = input("", ...ngDevMode ? [{ debugName: "parentId" }] : /* istanbul ignore next */ []);
492
+ parentType = input("", ...ngDevMode ? [{ debugName: "parentType" }] : /* istanbul ignore next */ []);
493
+ data = input(null, ...ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []);
494
+ search = input("", ...ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []);
495
+ columns = input(null, ...ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []);
496
+ reloadToken = input(null, ...ngDevMode ? [{ debugName: "reloadToken" }] : /* istanbul ignore next */ []);
497
+ deleted = input(null, ...ngDevMode ? [{ debugName: "deleted" }] : /* istanbul ignore next */ []);
498
+ effectiveDeleted = computed(() => {
499
+ const mode = this.deleted();
500
+ return mode && mode !== "exclude" ? mode : void 0;
501
+ }, ...ngDevMode ? [{ debugName: "effectiveDeleted" }] : /* istanbul ignore next */ []);
502
+ parentDeleted = input(false, ...ngDevMode ? [{ debugName: "parentDeleted" }] : /* istanbul ignore next */ []);
503
+ recycleBin = computed(() => this.effectiveDeleted() === "only" || this.parentDeleted(), ...ngDevMode ? [{ debugName: "recycleBin" }] : /* istanbul ignore next */ []);
504
+ rowQueryParams = computed(() => {
505
+ const mode = this.effectiveDeleted();
506
+ return mode ? { deleted: mode } : null;
507
+ }, ...ngDevMode ? [{ debugName: "rowQueryParams" }] : /* istanbul ignore next */ []);
508
+ router = inject(Router);
509
+ currentUrl = toSignal(this.router.events.pipe(filter((e) => e instanceof NavigationEnd), map(() => this.router.url)), { initialValue: this.router.url });
510
+ rowLinkState = computed(() => ({ [SPARK_RETURN_URL_STATE_KEY]: this.currentUrl() }), ...ngDevMode ? [{ debugName: "rowLinkState" }] : /* istanbul ignore next */ []);
511
+ onRowClick(event) {
512
+ if (this.selectionMode() === "none") this.openRow(event);
513
+ }
514
+ onRowDblClick(event) {
515
+ if (this.selectionMode() !== "none") this.openRow(event);
516
+ }
517
+ openRow(event) {
518
+ const row = event.row;
519
+ const original = event.originalEvent;
520
+ if ((original?.target)?.closest?.("a, button, input, select, textarea, label, [role=\"button\"]")) return;
521
+ if (original && (original.ctrlKey || original.metaKey || original.shiftKey || original.button === 1)) return;
522
+ if (!row || !this.canRead()) return;
523
+ const route = this.rowRouteFor(row);
524
+ if (!route) return;
525
+ this.rowClicked.emit(row);
526
+ this.router.navigate(route, {
527
+ queryParams: this.rowQueryParams() ?? void 0,
528
+ state: this.rowLinkState()
529
+ });
530
+ }
531
+ settings = model(new DatatableSettings({
532
+ perPage: {
533
+ values: SPARK_GRID_PAGE_SIZES,
534
+ selected: SPARK_GRID_PAGE_SIZES[0]
535
+ },
536
+ page: {
537
+ values: [1],
538
+ selected: 1
539
+ },
540
+ sortColumns: []
541
+ }), ...ngDevMode ? [{ debugName: "settings" }] : /* istanbul ignore next */ []);
542
+ selection = model([], ...ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []);
543
+ rowRoute = input(null, ...ngDevMode ? [{ debugName: "rowRoute" }] : /* istanbul ignore next */ []);
544
+ selectionModeSetting = input(null, ...ngDevMode ? [{ debugName: "selectionModeSetting" }] : /* istanbul ignore next */ []);
545
+ error = output();
546
+ rowClicked = output();
547
+ customActionExecuted = output();
548
+ createClicked = output();
549
+ colors = Color;
550
+ rowRouteFor(row) {
551
+ const custom = this.rowRoute();
552
+ if (custom) return custom(row);
553
+ const type = this.entityType();
554
+ if (!type?.clrType) return null;
555
+ return [
556
+ "/po",
557
+ type.alias || type.id,
558
+ row.id
559
+ ];
560
+ }
561
+ query = signal(null, ...ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []);
562
+ entityType = signal(null, ...ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []);
563
+ allEntityTypes = signal([], ...ngDevMode ? [{ debugName: "allEntityTypes" }] : /* istanbul ignore next */ []);
564
+ lookupReferenceOptions = signal({}, ...ngDevMode ? [{ debugName: "lookupReferenceOptions" }] : /* istanbul ignore next */ []);
565
+ loading = signal(true, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
566
+ canRead = signal(false, ...ngDevMode ? [{ debugName: "canRead" }] : /* istanbul ignore next */ []);
567
+ canCreate = signal(false, ...ngDevMode ? [{ debugName: "canCreate" }] : /* istanbul ignore next */ []);
568
+ permissions = signal(null, ...ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []);
569
+ resultCount = signal(null, ...ngDevMode ? [{ debugName: "resultCount" }] : /* istanbul ignore next */ []);
570
+ customActions = signal([], ...ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []);
571
+ defaultActions = signal([], ...ngDevMode ? [{ debugName: "defaultActions" }] : /* istanbul ignore next */ []);
572
+ disabledActions = signal([], ...ngDevMode ? [{ debugName: "disabledActions" }] : /* istanbul ignore next */ []);
573
+ offersCreate = computed(() => {
574
+ if (!this.canCreate() || this.recycleBin()) return false;
575
+ const withheld = this.disabledActions().map((name) => name.toLowerCase());
576
+ return !withheld.includes("new") && !withheld.includes("save");
577
+ }, ...ngDevMode ? [{ debugName: "offersCreate" }] : /* istanbul ignore next */ []);
578
+ visibleCustomActions = computed(() => {
579
+ if (this.recycleBin()) return [];
580
+ const withheld = this.disabledActions();
581
+ if (!withheld.length) return this.customActions();
582
+ const lowered = new Set(withheld.map((name) => name.toLowerCase()));
583
+ return this.customActions().filter((action) => !lowered.has(action.name.toLowerCase()));
584
+ }, ...ngDevMode ? [{ debugName: "visibleCustomActions" }] : /* istanbul ignore next */ []);
585
+ fetchFn = signal(null, ...ngDevMode ? [{ debugName: "fetchFn" }] : /* istanbul ignore next */ []);
586
+ errorMessage = signal(null, ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
587
+ selectionMode = computed(() => selectionModeFor(this.visibleCustomActions(), this.selectionModeSetting() ?? this.query()?.selectionMode ?? "auto"), ...ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []);
588
+ toolbarActions = computed(() => {
589
+ const actions = [];
590
+ const withheld = new Set(this.disabledActions().map((name) => name.toLowerCase()));
591
+ for (const definition of this.defaultActions()) {
592
+ const name = definition.name.toLowerCase();
593
+ if (name === "new" && this.offersCreate()) actions.push({
594
+ kind: "new",
595
+ name: definition.name,
596
+ definition,
597
+ priority: 1 + definition.offset
598
+ });
599
+ else if (name === "delete" && this.offersDelete(withheld) && this.selectionMode() !== "none") actions.push({
600
+ kind: "delete",
601
+ name: definition.name,
602
+ definition,
603
+ priority: 5 + definition.offset
604
+ });
605
+ }
606
+ for (const definition of this.visibleCustomActions()) actions.push({
607
+ kind: "custom",
608
+ name: definition.name,
609
+ definition,
610
+ priority: 10 + definition.offset
611
+ });
612
+ return actions;
613
+ }, ...ngDevMode ? [{ debugName: "toolbarActions" }] : /* istanbul ignore next */ []);
614
+ rowActions = computed(() => {
615
+ const withheld = new Set(this.disabledActions().map((name) => name.toLowerCase()));
616
+ const rowTaking = (definition) => !!definition.selectionRule?.trim() && parseSelectionRule(definition.selectionRule)(1);
617
+ const actions = [];
618
+ for (const definition of this.defaultActions()) if (definition.name.toLowerCase() === "delete" && this.offersDelete(withheld) && rowTaking(definition)) actions.push({
619
+ kind: "delete",
620
+ name: definition.name,
621
+ definition,
622
+ priority: 5 + definition.offset
623
+ });
624
+ for (const definition of this.visibleCustomActions()) if (rowTaking(definition)) actions.push({
625
+ kind: "custom",
626
+ name: definition.name,
627
+ definition,
628
+ priority: 10 + definition.offset
629
+ });
630
+ return actions;
631
+ }, ...ngDevMode ? [{ debugName: "rowActions" }] : /* istanbul ignore next */ []);
632
+ offersDelete(withheld) {
633
+ return !this.recycleBin() && !withheld.has("delete");
634
+ }
635
+ isToolbarActionEnabled(action) {
636
+ return this.isActionEnabled(action.definition);
637
+ }
638
+ async runToolbarAction(action) {
639
+ switch (action.kind) {
640
+ case "new":
641
+ this.startNew();
642
+ return;
643
+ case "delete":
644
+ await this.deleteRows(this.selection().map((r) => r.id), action.definition);
645
+ return;
646
+ default: await this.onCustomAction(action.definition);
647
+ }
648
+ }
649
+ clearSelection() {
650
+ this.selection.set([]);
651
+ }
652
+ openRowMenu = signal(null, ...ngDevMode ? [{ debugName: "openRowMenu" }] : /* istanbul ignore next */ []);
653
+ rowMenuPositions = [{
654
+ originX: "end",
655
+ originY: "bottom",
656
+ overlayX: "end",
657
+ overlayY: "top"
658
+ }, {
659
+ originX: "end",
660
+ originY: "top",
661
+ overlayX: "end",
662
+ overlayY: "bottom"
663
+ }];
664
+ toggleRowMenu(row) {
665
+ this.openRowMenu.update((open) => open === row.id ? null : row.id);
666
+ }
667
+ closeRowMenu(rowId) {
668
+ if (rowId !== void 0 && this.openRowMenu() !== rowId) return;
669
+ this.openRowMenu.set(null);
670
+ }
671
+ onRowMenuOutsideClick(event, toggle, rowId) {
672
+ if (event.target instanceof Node && toggle.contains(event.target)) return;
673
+ this.closeRowMenu(rowId);
674
+ }
675
+ async chooseRowAction(action, row) {
676
+ this.closeRowMenu();
677
+ await this.runRowAction(action, row);
678
+ }
679
+ async runRowAction(action, row) {
680
+ if (action.kind === "delete") {
681
+ await this.deleteRows([row.id], action.definition);
682
+ return;
683
+ }
684
+ await this.onCustomAction(action.definition, [row.id]);
685
+ }
686
+ startNew() {
687
+ const type = this.entityType();
688
+ if (!type) return;
689
+ this.createClicked.emit();
690
+ const parentId = this.parentId();
691
+ const parentType = this.parentType();
692
+ const query = this.query();
693
+ const queryParams = parentId && parentType && query ? {
694
+ parentId,
695
+ parentType,
696
+ queryId: query.alias || query.id
697
+ } : void 0;
698
+ this.router.navigate([
699
+ "/po",
700
+ type.alias || type.id,
701
+ "new"
702
+ ], {
703
+ queryParams,
704
+ state: this.rowLinkState()
705
+ });
706
+ }
707
+ async deleteRows(ids, definition) {
708
+ const type = this.entityType();
709
+ if (!type || !ids.length) return;
710
+ const key = definition?.confirmationMessageKey;
711
+ const message = ids.length === 1 ? this.lang.t("common.confirmDelete") : (key ? this.lang.t(key) : "") || this.lang.t("common.confirmDeleteSelected");
712
+ if (!confirm(`${message || "Are you sure?"}${ids.length > 1 ? ` (${ids.length})` : ""}`)) return;
713
+ const parentId = this.parentId();
714
+ const parentType = this.parentType();
715
+ try {
716
+ await this.sparkService.deleteMany(type.id, ids, {
717
+ queryId: this.query()?.id,
718
+ ...parentId && parentType ? {
719
+ parentId,
720
+ parentType
721
+ } : {}
722
+ });
723
+ const removed = new Set(ids);
724
+ this.selection.set(this.selection().filter((r) => !removed.has(r.id)));
725
+ this.reload();
726
+ } catch (e) {
727
+ const err = e;
728
+ this.errorMessage.set(err.error?.result?.errors?.[0]?.message || err.error?.result?.error || err.error?.error || (err.status === 404 ? this.lang.t("common.deleteRefused") : "") || err.message || this.lang.t("common.actionFailed") || "Action failed");
729
+ }
730
+ }
731
+ isVirtualScrolling = computed(() => isVirtualScrollingQuery(this.query()), ...ngDevMode ? [{ debugName: "isVirtualScrolling" }] : /* istanbul ignore next */ []);
732
+ fetchedColumns = signal([], ...ngDevMode ? [{ debugName: "fetchedColumns" }] : /* istanbul ignore next */ []);
733
+ allColumns = computed(() => this.columns() ?? this.fetchedColumns(), ...ngDevMode ? [{ debugName: "allColumns" }] : /* istanbul ignore next */ []);
734
+ visibleColumns = computed(() => this.allColumns().filter((c) => c.isVisible !== false), ...ngDevMode ? [{ debugName: "visibleColumns" }] : /* istanbul ignore next */ []);
735
+ hasExternalData = computed(() => this.data() !== null || this.query()?.isStreamingQuery === true, ...ngDevMode ? [{ debugName: "hasExternalData" }] : /* istanbul ignore next */ []);
736
+ isActionEnabled(action) {
737
+ return parseSelectionRule(action.selectionRule)(this.selection().map((r) => r.id).length);
738
+ }
739
+ constructor() {
740
+ effect(() => {
741
+ const qId = this.queryId();
742
+ const pId = this.parentId();
743
+ const pType = this.parentType();
744
+ if (qId) this.loadData(qId, pId, pType);
745
+ else this.loading.set(false);
746
+ });
747
+ let first = true;
748
+ effect(() => {
749
+ this.reloadToken();
750
+ this.queryRefresh.tokenFor(this.queryId());
751
+ if (first) {
752
+ first = false;
753
+ return;
754
+ }
755
+ untracked(() => this.reload());
756
+ });
757
+ let firstSearch = true;
758
+ effect(() => {
759
+ this.search();
760
+ if (firstSearch) {
761
+ firstSearch = false;
762
+ return;
763
+ }
764
+ untracked(() => {
765
+ this.selection.set([]);
766
+ this.onSearchChanged();
767
+ });
768
+ });
769
+ let firstDeleted = true;
770
+ effect(() => {
771
+ this.effectiveDeleted();
772
+ if (firstDeleted) {
773
+ firstDeleted = false;
774
+ return;
775
+ }
776
+ untracked(() => {
777
+ this.selection.set([]);
778
+ this.onFilterChanged();
779
+ });
780
+ });
781
+ }
782
+ reload() {
783
+ if (this.hasExternalData()) return;
784
+ const q = this.query();
785
+ if (q) this.fetchFn.set(this.makeFetch(q, this.parentId(), this.parentType()));
786
+ }
787
+ onSearchChanged() {
788
+ if (this.hasExternalData()) return;
789
+ const s = this.settings();
790
+ this.settings.set(new DatatableSettings({
791
+ perPage: {
792
+ values: s.perPage.values,
793
+ selected: s.perPage.selected
794
+ },
795
+ page: {
796
+ values: [1],
797
+ selected: 1
798
+ },
799
+ sortColumns: s.sortColumns
800
+ }));
801
+ this.reload();
802
+ }
803
+ setFetchedColumns(columns) {
804
+ const current = this.fetchedColumns();
805
+ if (current.length === columns.length && JSON.stringify(current) === JSON.stringify(columns)) return;
806
+ this.fetchedColumns.set(columns);
807
+ }
808
+ filters = signal([], ...ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []);
809
+ completeDistincts = /* @__PURE__ */ new Map();
810
+ distinctsFn = async (request) => {
811
+ const queryId = this.query()?.id;
812
+ if (!queryId) return null;
813
+ const others = this.filters().filter((f) => f.name !== request.column);
814
+ const querySearch = this.search() || void 0;
815
+ const parentId = this.parentId();
816
+ const parentType = this.parentType();
817
+ const result = await this.sparkService.getDistinctValues(queryId, request.column, {
818
+ search: request.search,
819
+ querySearch,
820
+ columns: others,
821
+ parentId,
822
+ parentType,
823
+ deleted: this.effectiveDeleted(),
824
+ parentDeleted: this.parentDeleted() && parentId && parentType ? "include" : void 0
825
+ });
826
+ if (result && !result.hasMore && !request.search && !querySearch) this.completeDistincts.set(request.column, result.matching.map((v) => v.value));
827
+ else this.completeDistincts.delete(request.column);
828
+ return result;
829
+ };
830
+ onFilterChange(detail) {
831
+ if (detail.mode !== "values") return;
832
+ const values = detail.selected.map((v) => v.value);
833
+ const next = this.filters().filter((f) => f.name !== detail.column);
834
+ if (values.length > 0) {
835
+ const complete = this.completeDistincts.get(detail.column);
836
+ if (values.some((v) => v === null || v === void 0) && !detail.inverse && complete) {
837
+ const unselected = complete.filter((v) => v !== null && v !== void 0 && !values.some((s) => s === v));
838
+ if (unselected.length > 0) next.push({
839
+ name: detail.column,
840
+ excludes: unselected
841
+ });
842
+ } else next.push(detail.inverse ? {
843
+ name: detail.column,
844
+ excludes: values
845
+ } : {
846
+ name: detail.column,
847
+ includes: values
848
+ });
849
+ }
850
+ this.filters.set(next);
851
+ this.onFilterChanged();
852
+ }
853
+ onFilterChanged() {
854
+ if (this.hasExternalData()) return;
855
+ const s = this.settings();
856
+ this.settings.set(new DatatableSettings({
857
+ perPage: {
858
+ values: s.perPage.values,
859
+ selected: s.perPage.selected
860
+ },
861
+ page: {
862
+ values: [1],
863
+ selected: 1
864
+ },
865
+ sortColumns: s.sortColumns
866
+ }));
867
+ this.reload();
868
+ }
869
+ async onCustomAction(action, rowIds) {
870
+ if (action.confirmationMessageKey) {
871
+ const message = this.lang.t(action.confirmationMessageKey) || "Are you sure?";
872
+ if (!confirm(message)) return;
873
+ }
874
+ try {
875
+ const pId = this.parentId();
876
+ const pType = this.parentType();
877
+ await this.sparkService.executeCustomAction(this.entityType().id, action.name, void 0, rowIds ?? this.selection().map((r) => r.id), pId && pType ? {
878
+ id: pId,
879
+ type: pType
880
+ } : void 0, this.query()?.id);
881
+ this.customActionExecuted.emit({ action });
882
+ if (action.refreshOnCompleted) this.reload();
883
+ } catch (e) {
884
+ const err = e;
885
+ this.errorMessage.set(err.error?.error || err.message || this.lang.t("common.actionFailed") || "Action failed");
886
+ }
887
+ }
888
+ reportError(err) {
889
+ this.errorMessage.set(this.describe(err));
890
+ this.error.emit(err);
891
+ }
892
+ describe(err) {
893
+ if (err?.status === 404) return this.lang.t("spark.query.unavailable") || "This list is not available.";
894
+ return err?.error?.error || err?.message || this.lang.t("common.unexpectedError") || "An unexpected error occurred";
895
+ }
896
+ async loadData(queryId, parentId, parentType) {
897
+ this.loading.set(true);
898
+ this.errorMessage.set(null);
899
+ this.fetchFn.set(null);
900
+ this.query.set(null);
901
+ this.entityType.set(null);
902
+ this.canRead.set(false);
903
+ this.canCreate.set(false);
904
+ this.permissions.set(null);
905
+ this.customActions.set([]);
906
+ this.defaultActions.set([]);
907
+ this.resultCount.set(null);
908
+ this.selection.set([]);
909
+ this.filters.set([]);
910
+ try {
911
+ const [resolvedQuery, entityTypes] = await Promise.all([this.sparkService.getQuery(queryId), this.sparkService.getEntityTypes()]);
912
+ this.query.set(resolvedQuery);
913
+ this.allEntityTypes.set(entityTypes);
914
+ const et = this.resolveEntityType(resolvedQuery, entityTypes);
915
+ this.entityType.set(et);
916
+ if (et) {
917
+ const [permissions, actions] = await Promise.all([this.sparkService.getPermissions(et.id), this.sparkService.getCustomActions(et.id)]);
918
+ this.canRead.set(permissions.canRead);
919
+ this.canCreate.set(permissions.canCreate);
920
+ this.permissions.set(permissions);
921
+ this.customActions.set(filterQueryActions(actions));
922
+ this.defaultActions.set(defaultQueryActions(actions));
923
+ } else if (resolvedQuery) console.warn(`[spark] Query '${resolvedQuery.name}' names entity type '${resolvedQuery.entityType ?? resolvedQuery.source}', which resolves to no type in /spark/types; the grid offers no actions and no row links.`);
924
+ this.settings.set(initialGridSettings(resolvedQuery));
925
+ if (this.data() === null && !resolvedQuery?.isStreamingQuery) this.fetchFn.set(this.makeFetch(resolvedQuery, parentId, parentType));
926
+ this.loadLookupReferenceOptions();
927
+ } catch (e) {
928
+ this.fetchFn.set(null);
929
+ this.reportError(e);
930
+ } finally {
931
+ this.loading.set(false);
932
+ }
933
+ }
934
+ resolveEntityType(query, entityTypes) {
935
+ if (!query) return null;
936
+ if (query.entityType) return entityTypes.find((t) => t.name === query.entityType || t.alias === query.entityType?.toLowerCase()) ?? null;
937
+ const sourceName = extractSourceName(query.source);
938
+ const singular = singularize(sourceName);
939
+ return entityTypes.find((t) => t.name === sourceName || t.name === singular || t.name + "s" === sourceName || t.clrType?.endsWith(singular)) ?? null;
940
+ }
941
+ makeFetch(query, parentId, parentType) {
942
+ return (req) => this.sparkService.executeQuery(query.id, {
943
+ sortColumns: req.sortColumns,
944
+ skip: (req.page - 1) * req.perPage,
945
+ take: req.perPage,
946
+ search: this.search() || void 0,
947
+ columns: this.filters(),
948
+ parentId,
949
+ parentType,
950
+ ...this.effectiveDeleted() ? { deleted: this.effectiveDeleted() } : {},
951
+ ...this.parentDeleted() && parentId && parentType ? { parentDeleted: "include" } : {}
952
+ }).then((r) => {
953
+ this.errorMessage.set(null);
954
+ this.resultCount.set(r.totalItems);
955
+ this.setFetchedColumns(r.columns ?? []);
956
+ this.disabledActions.set(r.disabledActions ?? []);
957
+ return {
958
+ data: r.items,
959
+ totalRecords: r.totalItems,
960
+ totalPages: Math.ceil(r.totalItems / req.perPage) || 1,
961
+ perPage: req.perPage,
962
+ page: req.page
963
+ };
964
+ }).catch((e) => {
965
+ this.reportError(e);
966
+ this.resultCount.set(0);
967
+ return {
968
+ data: [],
969
+ totalRecords: 0,
970
+ totalPages: 1,
971
+ perPage: req.perPage,
972
+ page: req.page
973
+ };
974
+ });
975
+ }
976
+ async loadLookupReferenceOptions() {
977
+ this.lookupReferenceOptions.set(await this.gridRenderers.loadLookupOptions(this.allColumns()));
978
+ }
979
+ rendererValueFor(item, column) {
980
+ return cellValue(valueFor(item, column.name));
981
+ }
982
+ getColumnRendererComponent(column) {
983
+ return this.gridRenderers.columnComponentFor(column);
984
+ }
985
+ filterLabelFor(column) {
986
+ return this.gridRenderers.filterLabelFor(column);
987
+ }
988
+ getColumnRendererInputs(component, item, column) {
989
+ return this.gridRenderers.columnInputsFor(component, item, column);
990
+ }
991
+ static ɵfac = i0.ɵɵngDeclareFactory({
992
+ minVersion: "12.0.0",
993
+ version: "22.2.0",
994
+ ngImport: i0,
995
+ type: SparkQueryGridComponent,
996
+ deps: [],
997
+ target: i0.ɵɵFactoryTarget.Component
998
+ });
999
+ static ɵcmp = i0.ɵɵngDeclareComponent({
1000
+ minVersion: "17.0.0",
1001
+ version: "22.2.0",
1002
+ type: SparkQueryGridComponent,
1003
+ isStandalone: true,
1004
+ selector: "spark-query-grid",
1005
+ inputs: {
1006
+ queryId: {
1007
+ classPropertyName: "queryId",
1008
+ publicName: "queryId",
1009
+ isSignal: true,
1010
+ isRequired: true,
1011
+ transformFunction: null
1012
+ },
1013
+ parentId: {
1014
+ classPropertyName: "parentId",
1015
+ publicName: "parentId",
1016
+ isSignal: true,
1017
+ isRequired: false,
1018
+ transformFunction: null
1019
+ },
1020
+ parentType: {
1021
+ classPropertyName: "parentType",
1022
+ publicName: "parentType",
1023
+ isSignal: true,
1024
+ isRequired: false,
1025
+ transformFunction: null
1026
+ },
1027
+ data: {
1028
+ classPropertyName: "data",
1029
+ publicName: "data",
1030
+ isSignal: true,
1031
+ isRequired: false,
1032
+ transformFunction: null
1033
+ },
1034
+ search: {
1035
+ classPropertyName: "search",
1036
+ publicName: "search",
1037
+ isSignal: true,
1038
+ isRequired: false,
1039
+ transformFunction: null
1040
+ },
1041
+ columns: {
1042
+ classPropertyName: "columns",
1043
+ publicName: "columns",
1044
+ isSignal: true,
1045
+ isRequired: false,
1046
+ transformFunction: null
1047
+ },
1048
+ reloadToken: {
1049
+ classPropertyName: "reloadToken",
1050
+ publicName: "reloadToken",
1051
+ isSignal: true,
1052
+ isRequired: false,
1053
+ transformFunction: null
1054
+ },
1055
+ deleted: {
1056
+ classPropertyName: "deleted",
1057
+ publicName: "deleted",
1058
+ isSignal: true,
1059
+ isRequired: false,
1060
+ transformFunction: null
1061
+ },
1062
+ parentDeleted: {
1063
+ classPropertyName: "parentDeleted",
1064
+ publicName: "parentDeleted",
1065
+ isSignal: true,
1066
+ isRequired: false,
1067
+ transformFunction: null
1068
+ },
1069
+ settings: {
1070
+ classPropertyName: "settings",
1071
+ publicName: "settings",
1072
+ isSignal: true,
1073
+ isRequired: false,
1074
+ transformFunction: null
1075
+ },
1076
+ selection: {
1077
+ classPropertyName: "selection",
1078
+ publicName: "selection",
1079
+ isSignal: true,
1080
+ isRequired: false,
1081
+ transformFunction: null
1082
+ },
1083
+ rowRoute: {
1084
+ classPropertyName: "rowRoute",
1085
+ publicName: "rowRoute",
1086
+ isSignal: true,
1087
+ isRequired: false,
1088
+ transformFunction: null
1089
+ },
1090
+ selectionModeSetting: {
1091
+ classPropertyName: "selectionModeSetting",
1092
+ publicName: "selectionModeSetting",
1093
+ isSignal: true,
1094
+ isRequired: false,
1095
+ transformFunction: null
1096
+ }
1097
+ },
1098
+ outputs: {
1099
+ settings: "settingsChange",
1100
+ selection: "selectionChange",
1101
+ error: "error",
1102
+ rowClicked: "rowClicked",
1103
+ customActionExecuted: "customActionExecuted",
1104
+ createClicked: "createClicked"
1105
+ },
1106
+ ngImport: i0,
1107
+ template: "<!--\n Three explicit states, in this order, and the order is load-bearing.\n\n This template used to open with `@if (query(); as q)` wrapping EVERYTHING, and `query()` is only\n set after loadData's awaits resolve. Three separate bugs fell out of that one line: the spinner\n was unreachable on a first load (it rendered inside the gate, while `loading` starts true and\n `query` starts null), a first-load failure rendered ZERO DOM — no card, no message, nothing to\n see or report — and a failed RELOAD left the previous query's rows on screen.\n\n So: `loading` is checked first and outside any dependency on `query`, and the final `@else`\n always renders something. Do not fold these back into one gate, and do not move the spinner\n inside the `query()` branch.\n-->\n@if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n} @else if (query()) {\n <!-- A page-fetch failure while the grid is already resolved. Without this the catch returns an\n empty page and a failed query is indistinguishable from one that legitimately has no rows. -->\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n\n <!--\n One datatable, two row sources. `[data]` and `[fetch]` are mutually exclusive in\n ng-bootstrap — binding both makes `[fetch]` win silently — so exactly one is ever non-null\n here: `fetchFn` is left null whenever `data` is bound. See the class comment for why rows can\n arrive from outside at all (streaming must not put a websocket in every detail page's bundle).\n -->\n <!--\n The selection bar (#460, M15): the \"N selected\" chip, only while rows are selected, and part\n of the grid rather than of a host, so the query card and the query-list page show the same\n thing. There is deliberately no select-all (owner decision, 2026-09-29): with paged, lazy or\n virtual-scrolled rows it could only tick the rows loaded so far. Deselect-all is the\n datatable's own header checkbox, which it shows only while a row is selected; the chip does\n not repeat it. Single selection has no checkbox column, so no header checkbox either, and a\n row click only ever selects: there the chip's ⊗ is the deselect. The bar keeps its height\n while empty, so ticking the first row does not push the table down under the pointer.\n -->\n @if (selectionMode() !== 'none') {\n <div class=\"spark-selection-bar d-flex align-items-center gap-2 mb-2\" style=\"min-height: 1.5rem\">\n @if (selection().length) {\n <bs-badge [type]=\"colors.primary\" class=\"spark-selection-chip\">\n {{ selection().length }} {{ 'common.selected' | t }}\n @if (selectionMode() === 'single') {\n <button type=\"button\" class=\"spark-selection-clear\"\n [attr.aria-label]=\"'common.clearSelection' | t\" (click)=\"clearSelection()\">\n <spark-icon name=\"x-circle\" />\n </button>\n }\n </bs-badge>\n }\n </div>\n }\n\n <bs-datatable\n [selectionMode]=\"selectionMode()\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"data()\"\n [fetch]=\"fetchFn()\"\n [distincts]=\"distinctsFn\"\n (filterChange)=\"onFilterChange($event)\"\n (rowClick)=\"onRowClick($event)\"\n (rowDblClick)=\"onRowDblClick($event)\"\n [(settings)]=\"settings\">\n @for (col of visibleColumns(); track col.name) {\n <!--\n `canSort`/`canFilter` arrive already resolved by the server (query override → attribute →\n true), so this only spells the polarity. `!== false` is needed for BOTH, for opposite\n reasons: absent means capable on our wire, while the datatable's own `filterable` defaults\n to false, unlike `sortable`.\n\n ⚠️ `filterActive` and `filterSummary` are deliberately NOT bound. Any *bsDatatableColumn\n input change recomputes the column set, which retires the live filter view — while the web\n component's mount-once guard leaves the panel's nodes in place and never re-renders them.\n Binding them to state our own panel mutates would freeze the panel the instant a value is\n ticked, until it is closed and reopened. The cost is that a filtered column carries no\n visual marker yet, which is worth far less than a panel that dies on first use. See #431 F7.\n -->\n <div *bsDatatableColumn=\"col.name; sortable: col.canSort !== false; filterable: col.canFilter !== false\">\n {{ (col.label | resolveTranslation) || col.name }}\n <spark-attribute-description [description]=\"col.description\" />\n\n @if (col.canFilter !== false) {\n <ng-container *bsDatatableFilterPanel=\"let values; let ctx = ctx\">\n <spark-column-filter-panel\n [column]=\"col\"\n [values]=\"values()\"\n [ctx]=\"ctx\"\n [filterLabel]=\"filterLabelFor(col)\" />\n </ng-container>\n }\n </div>\n }\n @if (rowActions().length) {\n <!-- The per-row ⋮ menu's column: no header, never sorted or filtered. -->\n <div *bsDatatableColumn=\"'__sparkRowActions'; sortable: false; filterable: false\">\n <span class=\"visually-hidden\">{{ 'common.actions' | t }}</span>\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (col of visibleColumns(); track col.name; let first = $first) {\n <td>\n @if (row) {\n <!-- The first column links to the row's detail page. Two things about this are\n routinely misread:\n\n 1. `cellContent` is projected INSIDE the anchor, so a custom `renderer` does NOT\n suppress this link — a renderer that emits its own <a> produces nested anchors\n (invalid HTML) rather than replacing this one.\n 2. `canRead()` is the whole gate, and it is the rights model: `Query` without\n `Read` lists the rows and withholds the link. Withhold `Read` for a query\n whose rows no detail page can load — a Custom.* query that fabricates rows in\n memory, say.\n 3. `rowRoute` replaces the DESTINATION only, never the gate: returning a route\n from it cannot make a row clickable that `canRead()` withheld, and returning\n null from it suppresses the link for that one row. -->\n @if (first && canRead() && rowRouteFor(row); as route) {\n <a [routerLink]=\"route\" [queryParams]=\"rowQueryParams()\" [state]=\"rowLinkState()\"\n (click)=\"rowClicked.emit(row)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n }\n } @else {\n &nbsp;\n }\n </td>\n }\n @if (rowActions().length) {\n <!-- The row menu: every action whose rule accepts one row, run on THIS row only. The\n checkbox selection is left alone (runRowAction never reads or writes it). -->\n <td class=\"spark-row-actions text-end\" (click)=\"$event.stopPropagation()\" (dblclick)=\"$event.stopPropagation()\">\n @if (row) {\n <button type=\"button\" cdkOverlayOrigin #rowMenuOrigin=\"cdkOverlayOrigin\" #rowMenuToggle\n class=\"btn btn-sm btn-link spark-row-menu-toggle\"\n aria-haspopup=\"menu\" [attr.aria-expanded]=\"openRowMenu() === row.id\"\n [attr.aria-label]=\"'common.actions' | t\" (click)=\"toggleRowMenu(row)\">\n <spark-icon name=\"three-dots-vertical\" />\n </button>\n <ng-template cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"rowMenuOrigin\"\n [cdkConnectedOverlayOpen]=\"openRowMenu() === row.id\"\n [cdkConnectedOverlayPositions]=\"rowMenuPositions\"\n (overlayOutsideClick)=\"onRowMenuOutsideClick($event, rowMenuToggle, row.id)\"\n (overlayKeydown)=\"$event.key === 'Escape' && closeRowMenu(row.id)\"\n (detach)=\"closeRowMenu(row.id)\">\n <bs-dropdown-menu class=\"spark-row-menu\">\n @for (action of rowActions(); track action.name) {\n <li bsDropdownItem class=\"spark-row-action\" [attr.data-action]=\"action.name\"\n (click)=\"chooseRowAction(action, row)\">\n {{ action.definition.displayName | resolveTranslation }}\n </li>\n }\n </bs-dropdown-menu>\n </ng-template>\n }\n </td>\n }\n </ng-container>\n </bs-datatable>\n} @else {\n <!-- The query could not be resolved. Rendering something keeps the host's layout intact and,\n crucially, makes the failure visible: this branch used to render nothing at all. -->\n <bs-alert [type]=\"colors.danger\" class=\"mb-0\">\n {{ errorMessage() || ('spark.query.unavailable' | t) }}\n </bs-alert>\n}\n\n<ng-template #cellContent let-item let-col=\"col\">\n <!--\n Presentation lives in <spark-grid-cell>, shared with the AsDetail table on the PO detail page.\n Value resolution stays here: a query row is a PersistentObject with an attribute list, and the\n AsDetail table's rows are plain dictionaries, so only the rendering is common.\n\n `link` is deliberately not passed. The first-column link is the grid's own rule and wraps this\n template from outside; setting both would nest anchors.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(col | queryCellValue:item:lookupReferenceOptions())\"\n [rendererValue]=\"rendererValueFor(item, col)\"\n [item]=\"item\"\n [chips]=\"(col | queryReferenceChips:item)\" />\n</ng-template>\n",
1108
+ styles: [":host ::ng-deep tbody tr[data-selected=true] a:not(.btn),:host ::ng-deep tbody tr[data-selected=true] .spark-row-menu-toggle{color:var(--mp-datatable-row-selected-color)}:host ::ng-deep tbody tr[data-selected=true] .spark-row-menu-toggle{--bs-btn-color: var(--mp-datatable-row-selected-color);--bs-btn-hover-color: var(--mp-datatable-row-selected-color);--bs-btn-active-color: var(--mp-datatable-row-selected-color)}\n"],
1109
+ dependencies: [
1110
+ {
1111
+ kind: "ngmodule",
1112
+ type: CommonModule
1113
+ },
1114
+ {
1115
+ kind: "directive",
1116
+ type: i1$1.NgTemplateOutlet,
1117
+ selector: "[ngTemplateOutlet]",
1118
+ inputs: [
1119
+ "ngTemplateOutletContext",
1120
+ "ngTemplateOutlet",
1121
+ "ngTemplateOutletInjector"
1122
+ ]
1123
+ },
1124
+ {
1125
+ kind: "ngmodule",
1126
+ type: RouterModule
1127
+ },
1128
+ {
1129
+ kind: "directive",
1130
+ type: i2.RouterLink,
1131
+ selector: "[routerLink]",
1132
+ inputs: [
1133
+ "target",
1134
+ "queryParams",
1135
+ "fragment",
1136
+ "queryParamsHandling",
1137
+ "state",
1138
+ "info",
1139
+ "relativeTo",
1140
+ "preserveFragment",
1141
+ "skipLocationChange",
1142
+ "replaceUrl",
1143
+ "browserUrl",
1144
+ "routerLink"
1145
+ ]
1146
+ },
1147
+ {
1148
+ kind: "component",
1149
+ type: BsAlertComponent,
1150
+ selector: "bs-alert",
1151
+ inputs: [
1152
+ "type",
1153
+ "announce",
1154
+ "isVisible"
1155
+ ],
1156
+ outputs: ["isVisibleChange", "afterOpenedOrClosed"]
1157
+ },
1158
+ {
1159
+ kind: "component",
1160
+ type: BsDatatableComponent,
1161
+ selector: "bs-datatable",
1162
+ inputs: [
1163
+ "columns",
1164
+ "data",
1165
+ "fetch",
1166
+ "distincts",
1167
+ "labels",
1168
+ "settings",
1169
+ "selectionMode",
1170
+ "selectable",
1171
+ "selection",
1172
+ "rowKey",
1173
+ "resizableColumns",
1174
+ "pagination",
1175
+ "virtualScroll",
1176
+ "itemSize",
1177
+ "virtualBuffer",
1178
+ "isResponsive",
1179
+ "compareWith",
1180
+ "tree",
1181
+ "idKey",
1182
+ "childCountKey",
1183
+ "treeIndent",
1184
+ "expandedIds",
1185
+ "selectionStrategy"
1186
+ ],
1187
+ outputs: [
1188
+ "settingsChange",
1189
+ "selectionChange",
1190
+ "rowClick",
1191
+ "rowDblClick",
1192
+ "rowContextMenu",
1193
+ "expandedIdsChange",
1194
+ "rowExpand",
1195
+ "rowCollapse",
1196
+ "filterChange"
1197
+ ]
1198
+ },
1199
+ {
1200
+ kind: "directive",
1201
+ type: BsDatatableColumnDirective,
1202
+ selector: "[bsDatatableColumn]",
1203
+ inputs: [
1204
+ "bsDatatableColumn",
1205
+ "bsDatatableColumnSortable",
1206
+ "bsDatatableColumnFilterable",
1207
+ "bsDatatableColumnFilterActive",
1208
+ "bsDatatableColumnFilterSummary",
1209
+ "bsDatatableColumnFilterSelection",
1210
+ "bsDatatableColumnFilterMode",
1211
+ "bsDatatableColumnFilterInputType",
1212
+ "bsDatatableColumnFilterOperators"
1213
+ ]
1214
+ },
1215
+ {
1216
+ kind: "directive",
1217
+ type: BsDatatableFilterPanelDirective,
1218
+ selector: "[bsDatatableFilterPanel]"
1219
+ },
1220
+ {
1221
+ kind: "directive",
1222
+ type: BsRowTemplateDirective,
1223
+ selector: "[bsRowTemplate]"
1224
+ },
1225
+ {
1226
+ kind: "component",
1227
+ type: BsSpinnerComponent,
1228
+ selector: "bs-spinner",
1229
+ inputs: ["type", "color"]
1230
+ },
1231
+ {
1232
+ kind: "component",
1233
+ type: SparkGridCellComponent,
1234
+ selector: "spark-grid-cell",
1235
+ inputs: [
1236
+ "column",
1237
+ "display",
1238
+ "rendererValue",
1239
+ "item",
1240
+ "chips",
1241
+ "link"
1242
+ ]
1243
+ },
1244
+ {
1245
+ kind: "component",
1246
+ type: SparkAttributeDescriptionComponent,
1247
+ selector: "spark-attribute-description",
1248
+ inputs: ["description", "position"]
1249
+ },
1250
+ {
1251
+ kind: "component",
1252
+ type: SparkColumnFilterPanelComponent,
1253
+ selector: "spark-column-filter-panel",
1254
+ inputs: [
1255
+ "column",
1256
+ "values",
1257
+ "ctx",
1258
+ "filterLabel"
1259
+ ]
1260
+ },
1261
+ {
1262
+ kind: "component",
1263
+ type: BsBadgeComponent,
1264
+ selector: "bs-badge",
1265
+ inputs: [
1266
+ "type",
1267
+ "unit",
1268
+ "decorative"
1269
+ ]
1270
+ },
1271
+ {
1272
+ kind: "directive",
1273
+ type: CdkOverlayOrigin,
1274
+ selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]",
1275
+ exportAs: ["cdkOverlayOrigin"]
1276
+ },
1277
+ {
1278
+ kind: "directive",
1279
+ type: CdkConnectedOverlay,
1280
+ selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]",
1281
+ inputs: [
1282
+ "cdkConnectedOverlayOrigin",
1283
+ "cdkConnectedOverlayPositions",
1284
+ "cdkConnectedOverlayPositionStrategy",
1285
+ "cdkConnectedOverlayOffsetX",
1286
+ "cdkConnectedOverlayOffsetY",
1287
+ "cdkConnectedOverlayWidth",
1288
+ "cdkConnectedOverlayHeight",
1289
+ "cdkConnectedOverlayMinWidth",
1290
+ "cdkConnectedOverlayMinHeight",
1291
+ "cdkConnectedOverlayBackdropClass",
1292
+ "cdkConnectedOverlayPanelClass",
1293
+ "cdkConnectedOverlayViewportMargin",
1294
+ "cdkConnectedOverlayScrollStrategy",
1295
+ "cdkConnectedOverlayOpen",
1296
+ "cdkConnectedOverlayDisableClose",
1297
+ "cdkConnectedOverlayTransformOriginOn",
1298
+ "cdkConnectedOverlayHasBackdrop",
1299
+ "cdkConnectedOverlayLockPosition",
1300
+ "cdkConnectedOverlayFlexibleDimensions",
1301
+ "cdkConnectedOverlayGrowAfterOpen",
1302
+ "cdkConnectedOverlayPush",
1303
+ "cdkConnectedOverlayDisposeOnNavigation",
1304
+ "cdkConnectedOverlayUsePopover",
1305
+ "cdkConnectedOverlayMatchWidth",
1306
+ "cdkConnectedOverlay"
1307
+ ],
1308
+ outputs: [
1309
+ "backdropClick",
1310
+ "positionChange",
1311
+ "attach",
1312
+ "detach",
1313
+ "overlayKeydown",
1314
+ "overlayOutsideClick"
1315
+ ],
1316
+ exportAs: ["cdkConnectedOverlay"]
1317
+ },
1318
+ {
1319
+ kind: "component",
1320
+ type: BsDropdownMenuComponent,
1321
+ selector: "bs-dropdown-menu",
1322
+ inputs: ["menuMode", "maxHeight"],
1323
+ outputs: ["select"]
1324
+ },
1325
+ {
1326
+ kind: "directive",
1327
+ type: BsDropdownItemDirective,
1328
+ selector: "[bsDropdownItem]",
1329
+ inputs: [
1330
+ "active",
1331
+ "disabled",
1332
+ "value"
1333
+ ]
1334
+ },
1335
+ {
1336
+ kind: "component",
1337
+ type: SparkIconComponent,
1338
+ selector: "spark-icon",
1339
+ inputs: ["name"]
1340
+ },
1341
+ {
1342
+ kind: "pipe",
1343
+ type: ResolveTranslationPipe,
1344
+ name: "resolveTranslation"
1345
+ },
1346
+ {
1347
+ kind: "pipe",
1348
+ type: QueryCellValuePipe,
1349
+ name: "queryCellValue"
1350
+ },
1351
+ {
1352
+ kind: "pipe",
1353
+ type: QueryReferenceChipsPipe,
1354
+ name: "queryReferenceChips"
1355
+ },
1356
+ {
1357
+ kind: "pipe",
1358
+ type: TranslateKeyPipe,
1359
+ name: "t"
1360
+ }
1361
+ ],
1362
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
1363
+ });
1364
+ };
1365
+ i0.ɵɵngDeclareClassMetadata({
1366
+ minVersion: "12.0.0",
1367
+ version: "22.2.0",
1368
+ ngImport: i0,
1369
+ type: SparkQueryGridComponent,
1370
+ decorators: [{
1371
+ type: Component,
1372
+ args: [{
1373
+ selector: "spark-query-grid",
1374
+ imports: [
1375
+ CommonModule,
1376
+ RouterModule,
1377
+ BsAlertComponent,
1378
+ BsDatatableComponent,
1379
+ BsDatatableColumnDirective,
1380
+ BsDatatableFilterPanelDirective,
1381
+ BsRowTemplateDirective,
1382
+ BsSpinnerComponent,
1383
+ SparkGridCellComponent,
1384
+ ResolveTranslationPipe,
1385
+ QueryCellValuePipe,
1386
+ QueryReferenceChipsPipe,
1387
+ TranslateKeyPipe,
1388
+ SparkAttributeDescriptionComponent,
1389
+ SparkColumnFilterPanelComponent,
1390
+ BsBadgeComponent,
1391
+ CdkOverlayOrigin,
1392
+ CdkConnectedOverlay,
1393
+ BsDropdownMenuComponent,
1394
+ BsDropdownItemDirective,
1395
+ SparkIconComponent
1396
+ ],
1397
+ changeDetection: ChangeDetectionStrategy.OnPush,
1398
+ template: "<!--\n Three explicit states, in this order, and the order is load-bearing.\n\n This template used to open with `@if (query(); as q)` wrapping EVERYTHING, and `query()` is only\n set after loadData's awaits resolve. Three separate bugs fell out of that one line: the spinner\n was unreachable on a first load (it rendered inside the gate, while `loading` starts true and\n `query` starts null), a first-load failure rendered ZERO DOM — no card, no message, nothing to\n see or report — and a failed RELOAD left the previous query's rows on screen.\n\n So: `loading` is checked first and outside any dependency on `query`, and the final `@else`\n always renders something. Do not fold these back into one gate, and do not move the spinner\n inside the `query()` branch.\n-->\n@if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n} @else if (query()) {\n <!-- A page-fetch failure while the grid is already resolved. Without this the catch returns an\n empty page and a failed query is indistinguishable from one that legitimately has no rows. -->\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n\n <!--\n One datatable, two row sources. `[data]` and `[fetch]` are mutually exclusive in\n ng-bootstrap — binding both makes `[fetch]` win silently — so exactly one is ever non-null\n here: `fetchFn` is left null whenever `data` is bound. See the class comment for why rows can\n arrive from outside at all (streaming must not put a websocket in every detail page's bundle).\n -->\n <!--\n The selection bar (#460, M15): the \"N selected\" chip, only while rows are selected, and part\n of the grid rather than of a host, so the query card and the query-list page show the same\n thing. There is deliberately no select-all (owner decision, 2026-09-29): with paged, lazy or\n virtual-scrolled rows it could only tick the rows loaded so far. Deselect-all is the\n datatable's own header checkbox, which it shows only while a row is selected; the chip does\n not repeat it. Single selection has no checkbox column, so no header checkbox either, and a\n row click only ever selects: there the chip's ⊗ is the deselect. The bar keeps its height\n while empty, so ticking the first row does not push the table down under the pointer.\n -->\n @if (selectionMode() !== 'none') {\n <div class=\"spark-selection-bar d-flex align-items-center gap-2 mb-2\" style=\"min-height: 1.5rem\">\n @if (selection().length) {\n <bs-badge [type]=\"colors.primary\" class=\"spark-selection-chip\">\n {{ selection().length }} {{ 'common.selected' | t }}\n @if (selectionMode() === 'single') {\n <button type=\"button\" class=\"spark-selection-clear\"\n [attr.aria-label]=\"'common.clearSelection' | t\" (click)=\"clearSelection()\">\n <spark-icon name=\"x-circle\" />\n </button>\n }\n </bs-badge>\n }\n </div>\n }\n\n <bs-datatable\n [selectionMode]=\"selectionMode()\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"data()\"\n [fetch]=\"fetchFn()\"\n [distincts]=\"distinctsFn\"\n (filterChange)=\"onFilterChange($event)\"\n (rowClick)=\"onRowClick($event)\"\n (rowDblClick)=\"onRowDblClick($event)\"\n [(settings)]=\"settings\">\n @for (col of visibleColumns(); track col.name) {\n <!--\n `canSort`/`canFilter` arrive already resolved by the server (query override → attribute →\n true), so this only spells the polarity. `!== false` is needed for BOTH, for opposite\n reasons: absent means capable on our wire, while the datatable's own `filterable` defaults\n to false, unlike `sortable`.\n\n ⚠️ `filterActive` and `filterSummary` are deliberately NOT bound. Any *bsDatatableColumn\n input change recomputes the column set, which retires the live filter view — while the web\n component's mount-once guard leaves the panel's nodes in place and never re-renders them.\n Binding them to state our own panel mutates would freeze the panel the instant a value is\n ticked, until it is closed and reopened. The cost is that a filtered column carries no\n visual marker yet, which is worth far less than a panel that dies on first use. See #431 F7.\n -->\n <div *bsDatatableColumn=\"col.name; sortable: col.canSort !== false; filterable: col.canFilter !== false\">\n {{ (col.label | resolveTranslation) || col.name }}\n <spark-attribute-description [description]=\"col.description\" />\n\n @if (col.canFilter !== false) {\n <ng-container *bsDatatableFilterPanel=\"let values; let ctx = ctx\">\n <spark-column-filter-panel\n [column]=\"col\"\n [values]=\"values()\"\n [ctx]=\"ctx\"\n [filterLabel]=\"filterLabelFor(col)\" />\n </ng-container>\n }\n </div>\n }\n @if (rowActions().length) {\n <!-- The per-row ⋮ menu's column: no header, never sorted or filtered. -->\n <div *bsDatatableColumn=\"'__sparkRowActions'; sortable: false; filterable: false\">\n <span class=\"visually-hidden\">{{ 'common.actions' | t }}</span>\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (col of visibleColumns(); track col.name; let first = $first) {\n <td>\n @if (row) {\n <!-- The first column links to the row's detail page. Two things about this are\n routinely misread:\n\n 1. `cellContent` is projected INSIDE the anchor, so a custom `renderer` does NOT\n suppress this link — a renderer that emits its own <a> produces nested anchors\n (invalid HTML) rather than replacing this one.\n 2. `canRead()` is the whole gate, and it is the rights model: `Query` without\n `Read` lists the rows and withholds the link. Withhold `Read` for a query\n whose rows no detail page can load — a Custom.* query that fabricates rows in\n memory, say.\n 3. `rowRoute` replaces the DESTINATION only, never the gate: returning a route\n from it cannot make a row clickable that `canRead()` withheld, and returning\n null from it suppresses the link for that one row. -->\n @if (first && canRead() && rowRouteFor(row); as route) {\n <a [routerLink]=\"route\" [queryParams]=\"rowQueryParams()\" [state]=\"rowLinkState()\"\n (click)=\"rowClicked.emit(row)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n }\n } @else {\n &nbsp;\n }\n </td>\n }\n @if (rowActions().length) {\n <!-- The row menu: every action whose rule accepts one row, run on THIS row only. The\n checkbox selection is left alone (runRowAction never reads or writes it). -->\n <td class=\"spark-row-actions text-end\" (click)=\"$event.stopPropagation()\" (dblclick)=\"$event.stopPropagation()\">\n @if (row) {\n <button type=\"button\" cdkOverlayOrigin #rowMenuOrigin=\"cdkOverlayOrigin\" #rowMenuToggle\n class=\"btn btn-sm btn-link spark-row-menu-toggle\"\n aria-haspopup=\"menu\" [attr.aria-expanded]=\"openRowMenu() === row.id\"\n [attr.aria-label]=\"'common.actions' | t\" (click)=\"toggleRowMenu(row)\">\n <spark-icon name=\"three-dots-vertical\" />\n </button>\n <ng-template cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"rowMenuOrigin\"\n [cdkConnectedOverlayOpen]=\"openRowMenu() === row.id\"\n [cdkConnectedOverlayPositions]=\"rowMenuPositions\"\n (overlayOutsideClick)=\"onRowMenuOutsideClick($event, rowMenuToggle, row.id)\"\n (overlayKeydown)=\"$event.key === 'Escape' && closeRowMenu(row.id)\"\n (detach)=\"closeRowMenu(row.id)\">\n <bs-dropdown-menu class=\"spark-row-menu\">\n @for (action of rowActions(); track action.name) {\n <li bsDropdownItem class=\"spark-row-action\" [attr.data-action]=\"action.name\"\n (click)=\"chooseRowAction(action, row)\">\n {{ action.definition.displayName | resolveTranslation }}\n </li>\n }\n </bs-dropdown-menu>\n </ng-template>\n }\n </td>\n }\n </ng-container>\n </bs-datatable>\n} @else {\n <!-- The query could not be resolved. Rendering something keeps the host's layout intact and,\n crucially, makes the failure visible: this branch used to render nothing at all. -->\n <bs-alert [type]=\"colors.danger\" class=\"mb-0\">\n {{ errorMessage() || ('spark.query.unavailable' | t) }}\n </bs-alert>\n}\n\n<ng-template #cellContent let-item let-col=\"col\">\n <!--\n Presentation lives in <spark-grid-cell>, shared with the AsDetail table on the PO detail page.\n Value resolution stays here: a query row is a PersistentObject with an attribute list, and the\n AsDetail table's rows are plain dictionaries, so only the rendering is common.\n\n `link` is deliberately not passed. The first-column link is the grid's own rule and wraps this\n template from outside; setting both would nest anchors.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(col | queryCellValue:item:lookupReferenceOptions())\"\n [rendererValue]=\"rendererValueFor(item, col)\"\n [item]=\"item\"\n [chips]=\"(col | queryReferenceChips:item)\" />\n</ng-template>\n",
1399
+ styles: [":host ::ng-deep tbody tr[data-selected=true] a:not(.btn),:host ::ng-deep tbody tr[data-selected=true] .spark-row-menu-toggle{color:var(--mp-datatable-row-selected-color)}:host ::ng-deep tbody tr[data-selected=true] .spark-row-menu-toggle{--bs-btn-color: var(--mp-datatable-row-selected-color);--bs-btn-hover-color: var(--mp-datatable-row-selected-color);--bs-btn-active-color: var(--mp-datatable-row-selected-color)}\n"]
1400
+ }]
1401
+ }],
1402
+ ctorParameters: () => [],
1403
+ propDecorators: {
1404
+ queryId: [{
1405
+ type: i0.Input,
1406
+ args: [{
1407
+ isSignal: true,
1408
+ alias: "queryId",
1409
+ required: true
1410
+ }]
1411
+ }],
1412
+ parentId: [{
1413
+ type: i0.Input,
1414
+ args: [{
1415
+ isSignal: true,
1416
+ alias: "parentId",
1417
+ required: false
1418
+ }]
1419
+ }],
1420
+ parentType: [{
1421
+ type: i0.Input,
1422
+ args: [{
1423
+ isSignal: true,
1424
+ alias: "parentType",
1425
+ required: false
1426
+ }]
1427
+ }],
1428
+ data: [{
1429
+ type: i0.Input,
1430
+ args: [{
1431
+ isSignal: true,
1432
+ alias: "data",
1433
+ required: false
1434
+ }]
1435
+ }],
1436
+ search: [{
1437
+ type: i0.Input,
1438
+ args: [{
1439
+ isSignal: true,
1440
+ alias: "search",
1441
+ required: false
1442
+ }]
1443
+ }],
1444
+ columns: [{
1445
+ type: i0.Input,
1446
+ args: [{
1447
+ isSignal: true,
1448
+ alias: "columns",
1449
+ required: false
1450
+ }]
1451
+ }],
1452
+ reloadToken: [{
1453
+ type: i0.Input,
1454
+ args: [{
1455
+ isSignal: true,
1456
+ alias: "reloadToken",
1457
+ required: false
1458
+ }]
1459
+ }],
1460
+ deleted: [{
1461
+ type: i0.Input,
1462
+ args: [{
1463
+ isSignal: true,
1464
+ alias: "deleted",
1465
+ required: false
1466
+ }]
1467
+ }],
1468
+ parentDeleted: [{
1469
+ type: i0.Input,
1470
+ args: [{
1471
+ isSignal: true,
1472
+ alias: "parentDeleted",
1473
+ required: false
1474
+ }]
1475
+ }],
1476
+ settings: [{
1477
+ type: i0.Input,
1478
+ args: [{
1479
+ isSignal: true,
1480
+ alias: "settings",
1481
+ required: false
1482
+ }]
1483
+ }, {
1484
+ type: i0.Output,
1485
+ args: ["settingsChange"]
1486
+ }],
1487
+ selection: [{
1488
+ type: i0.Input,
1489
+ args: [{
1490
+ isSignal: true,
1491
+ alias: "selection",
1492
+ required: false
1493
+ }]
1494
+ }, {
1495
+ type: i0.Output,
1496
+ args: ["selectionChange"]
1497
+ }],
1498
+ rowRoute: [{
1499
+ type: i0.Input,
1500
+ args: [{
1501
+ isSignal: true,
1502
+ alias: "rowRoute",
1503
+ required: false
1504
+ }]
1505
+ }],
1506
+ selectionModeSetting: [{
1507
+ type: i0.Input,
1508
+ args: [{
1509
+ isSignal: true,
1510
+ alias: "selectionModeSetting",
1511
+ required: false
1512
+ }]
1513
+ }],
1514
+ error: [{
1515
+ type: i0.Output,
1516
+ args: ["error"]
1517
+ }],
1518
+ rowClicked: [{
1519
+ type: i0.Output,
1520
+ args: ["rowClicked"]
1521
+ }],
1522
+ customActionExecuted: [{
1523
+ type: i0.Output,
1524
+ args: ["customActionExecuted"]
1525
+ }],
1526
+ createClicked: [{
1527
+ type: i0.Output,
1528
+ args: ["createClicked"]
1529
+ }]
1530
+ }
1531
+ });
996
1532
  function extractSourceName(source) {
997
- const dotIndex = source.indexOf('.');
998
- return dotIndex >= 0 ? source.substring(dotIndex + 1) : source;
1533
+ const dotIndex = source.indexOf(".");
1534
+ return dotIndex >= 0 ? source.substring(dotIndex + 1) : source;
999
1535
  }
1000
- /**
1001
- * Enough English to map a query source onto a type name — `Cars` to `Car`, `People` to `Person`.
1002
- *
1003
- * Deliberately a small table plus three suffix rules rather than a library: it runs against type
1004
- * names the application itself chose, and a wrong guess costs nothing, since the caller falls
1005
- * through to matching the plural and the CLR type name.
1006
- */
1007
1536
  function singularize(plural) {
1008
- const irregulars = {
1009
- 'People': 'Person',
1010
- 'Children': 'Child',
1011
- 'Men': 'Man',
1012
- 'Women': 'Woman',
1013
- };
1014
- if (irregulars[plural])
1015
- return irregulars[plural];
1016
- if (plural.endsWith('ies'))
1017
- return plural.slice(0, -3) + 'y';
1018
- if (plural.endsWith('es'))
1019
- return plural.slice(0, -2);
1020
- if (plural.endsWith('s'))
1021
- return plural.slice(0, -1);
1022
- return plural;
1537
+ const irregulars = {
1538
+ "People": "Person",
1539
+ "Children": "Child",
1540
+ "Men": "Man",
1541
+ "Women": "Woman"
1542
+ };
1543
+ if (irregulars[plural]) return irregulars[plural];
1544
+ if (plural.endsWith("ies")) return plural.slice(0, -3) + "y";
1545
+ if (plural.endsWith("es")) return plural.slice(0, -2);
1546
+ if (plural.endsWith("s")) return plural.slice(0, -1);
1547
+ return plural;
1023
1548
  }
1024
-
1025
- /**
1026
- * A `<bs-card>` around a {@link SparkQueryGridComponent}: icon, caption, actions, grid.
1027
- *
1028
- * ## Chrome is overridden, not replaced
1029
- *
1030
- * Every header region has a default, and a host replaces only the ones it supplies. That is what
1031
- * lets this component serve the auto-rendered case: a sub-query is rendered once per entry in
1032
- * `EntityTypeDefinition.Queries` with nobody projecting into it, and it must look right with no
1033
- * host cooperation at all. A slot mechanism that only ever *replaced* chrome would leave that
1034
- * call site blank — which is why an earlier design had the query carry its own chrome from the
1035
- * server. Overriding a default needs no host; replacing one does.
1036
- *
1037
- * ## Two ways in, one set of templates
1038
- *
1039
- * Hand-written markup uses the structural directives and is found with `contentChildren`.
1040
- * The auto-rendered path cannot: `spark-po-detail` is created by the router, so in a default app
1041
- * there is no tag to project into. It therefore forwards `TemplateRef`s as inputs instead — a
1042
- * directive cannot cross a component boundary but its template can, and that forwarding is
1043
- * already the idiom in `spark-po-detail` (`extraActionsTemplate`, `extraContentTemplate`).
1044
- *
1045
- * Content wins over a forwarded input: the closer declaration is the more specific one.
1046
- */
1047
- class SparkQueryCardComponent {
1048
- lang = inject(SparkLanguageService);
1049
- queryId = input.required(/* @ts-ignore */
1050
- ...(ngDevMode ? [{ debugName: "queryId" }] : /* istanbul ignore next */ []));
1051
- parentId = input('', /* @ts-ignore */
1052
- ...(ngDevMode ? [{ debugName: "parentId" }] : /* istanbul ignore next */ []));
1053
- parentType = input('', /* @ts-ignore */
1054
- ...(ngDevMode ? [{ debugName: "parentType" }] : /* istanbul ignore next */ []));
1055
- reloadToken = input(null, /* @ts-ignore */
1056
- ...(ngDevMode ? [{ debugName: "reloadToken" }] : /* istanbul ignore next */ []));
1057
- data = input(null, /* @ts-ignore */
1058
- ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
1059
- search = input('', /* @ts-ignore */
1060
- ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
1061
- /**
1062
- * Slots forwarded from a host that cannot project content — see the class comment. A card
1063
- * reached through hand-written markup should use the structural directives instead.
1064
- */
1065
- iconTemplate = input(null, /* @ts-ignore */
1066
- ...(ngDevMode ? [{ debugName: "iconTemplate" }] : /* istanbul ignore next */ []));
1067
- captionTemplate = input(null, /* @ts-ignore */
1068
- ...(ngDevMode ? [{ debugName: "captionTemplate" }] : /* istanbul ignore next */ []));
1069
- actionsTemplate = input(null, /* @ts-ignore */
1070
- ...(ngDevMode ? [{ debugName: "actionsTemplate" }] : /* istanbul ignore next */ []));
1071
- error = output();
1072
- rowClicked = output();
1073
- customActionExecuted = output();
1074
- /**
1075
- * Forwarded to the grid. Without this the escape hatch was unreachable from a card — i.e. from
1076
- * every auto-rendered sub-query and program-unit query page, which is most grids.
1077
- */
1078
- rowRoute = input(null, /* @ts-ignore */
1079
- ...(ngDevMode ? [{ debugName: "rowRoute" }] : /* istanbul ignore next */ []));
1080
- colors = Color;
1081
- iconSlots = contentChildren(SparkQueryIconDirective, /* @ts-ignore */
1082
- ...(ngDevMode ? [{ debugName: "iconSlots" }] : /* istanbul ignore next */ []));
1083
- captionSlots = contentChildren(SparkQueryCaptionDirective, /* @ts-ignore */
1084
- ...(ngDevMode ? [{ debugName: "captionSlots" }] : /* istanbul ignore next */ []));
1085
- actionSlots = contentChildren(SparkQueryActionsDirective, /* @ts-ignore */
1086
- ...(ngDevMode ? [{ debugName: "actionSlots" }] : /* istanbul ignore next */ []));
1087
- /**
1088
- * The grid, read only to surface its state — the query, the actions, the selection — to this
1089
- * component's own header.
1090
- *
1091
- * Optional rather than `required` on purpose. The header renders ABOVE the grid in this
1092
- * template, so on the first change-detection pass the view query has not resolved yet and
1093
- * `viewChild.required()` would throw where a plain one returns `undefined`. Everything below
1094
- * therefore reads it defensively; the signal updates once the view initialises and the header
1095
- * re-renders with the real query.
1096
- *
1097
- * A host reads grid state through a template reference variable instead, because a host may put
1098
- * the grid behind an `@if`, where a view query genuinely is intermittently undefined.
1099
- */
1100
- grid = viewChild(SparkQueryGridComponent, /* @ts-ignore */
1101
- ...(ngDevMode ? [{ debugName: "grid" }] : /* istanbul ignore next */ []));
1102
- query = computed(() => this.grid()?.query() ?? null, /* @ts-ignore */
1103
- ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
1104
- iconTpl = computed(() => slotFor(this.iconSlots(), this.query())?.templateRef ?? this.iconTemplate(), /* @ts-ignore */
1105
- ...(ngDevMode ? [{ debugName: "iconTpl" }] : /* istanbul ignore next */ []));
1106
- captionTpl = computed(() => slotFor(this.captionSlots(), this.query())?.templateRef ?? this.captionTemplate(), /* @ts-ignore */
1107
- ...(ngDevMode ? [{ debugName: "captionTpl" }] : /* istanbul ignore next */ []));
1108
- actionsTpl = computed(() => slotFor(this.actionSlots(), this.query())?.templateRef ?? this.actionsTemplate(), /* @ts-ignore */
1109
- ...(ngDevMode ? [{ debugName: "actionsTpl" }] : /* istanbul ignore next */ []));
1110
- /** The caption the card renders unless a slot replaces it. */
1111
- caption = computed(() => {
1112
- const q = this.query();
1113
- return (q?.description ? this.lang.resolve(q.description) : '') || q?.name || '';
1114
- }, /* @ts-ignore */
1115
- ...(ngDevMode ? [{ debugName: "caption" }] : /* istanbul ignore next */ []));
1116
- customActions = computed(() => this.grid()?.visibleCustomActions() ?? [], /* @ts-ignore */
1117
- ...(ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []));
1118
- selection = computed(() => this.grid()?.selection() ?? [], /* @ts-ignore */
1119
- ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
1120
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1121
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkQueryCardComponent, isStandalone: true, selector: "spark-query-card", inputs: { queryId: { classPropertyName: "queryId", publicName: "queryId", isSignal: true, isRequired: true, transformFunction: null }, parentId: { classPropertyName: "parentId", publicName: "parentId", isSignal: true, isRequired: false, transformFunction: null }, parentType: { classPropertyName: "parentType", publicName: "parentType", isSignal: true, isRequired: false, transformFunction: null }, reloadToken: { classPropertyName: "reloadToken", publicName: "reloadToken", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, search: { classPropertyName: "search", publicName: "search", isSignal: true, isRequired: false, transformFunction: null }, iconTemplate: { classPropertyName: "iconTemplate", publicName: "iconTemplate", isSignal: true, isRequired: false, transformFunction: null }, captionTemplate: { classPropertyName: "captionTemplate", publicName: "captionTemplate", isSignal: true, isRequired: false, transformFunction: null }, actionsTemplate: { classPropertyName: "actionsTemplate", publicName: "actionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, rowRoute: { classPropertyName: "rowRoute", publicName: "rowRoute", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { error: "error", rowClicked: "rowClicked", customActionExecuted: "customActionExecuted" }, queries: [{ propertyName: "iconSlots", predicate: SparkQueryIconDirective, isSignal: true }, { propertyName: "captionSlots", predicate: SparkQueryCaptionDirective, isSignal: true }, { propertyName: "actionSlots", predicate: SparkQueryActionsDirective, isSignal: true }], viewQueries: [{ propertyName: "grid", first: true, predicate: SparkQueryGridComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n The card is always rendered. A host that wants no card uses <spark-query-grid> directly \u2014 which\n is what the old `showCard=\"false\"` meant, expressed as a component choice rather than a flag.\n-->\n<bs-card class=\"d-block my-3\">\n <bs-card-header>\n <div class=\"d-flex align-items-center gap-2\">\n @if (iconTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: query() }\"></ng-container>\n }\n\n @if (actionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: customActions(), selection: selection(), query: query() }\"></ng-container>\n } @else if (customActions().length) {\n <!-- The default: the actions the server declares for this type. The nav renders only when\n there are any, so a query without actions is pixel-identical to a plain caption.\n\n Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rule. -->\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @for (action of customActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" class=\"btn btn-sm btn-outline-primary\"\n [disabled]=\"!grid()?.isActionEnabled(action)\" (click)=\"grid()?.onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n </bs-priority-nav>\n }\n\n <!--\n Caption last, actions first. The auto margin is conditional rather than fixed, and that is\n the point: with actions present `ms-auto` pushes the caption to the trailing edge, and with\n none it falls back to `me-auto` so a plain card looks exactly as it always did. A fixed\n `ms-auto` would have right-aligned the caption of every action-less query card in every app\n \u2014 a change nobody asked for, to fix a layout only some cards have.\n -->\n <span [class.ms-auto]=\"customActions().length\" [class.me-auto]=\"!customActions().length\">\n @if (captionTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: caption(), query: query() }\"></ng-container>\n } @else {\n {{ caption() }}\n }\n </span>\n </div>\n </bs-card-header>\n\n <div class=\"p-3\">\n <spark-query-grid\n [queryId]=\"queryId()\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [reloadToken]=\"reloadToken()\"\n [data]=\"data()\"\n [search]=\"search()\"\n [rowRoute]=\"rowRoute()\"\n (error)=\"error.emit($event)\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n </div>\n</bs-card>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: BsCardComponent, selector: "bs-card", inputs: ["color", "outline"] }, { kind: "component", type: BsCardHeaderComponent, selector: "bs-card-header", inputs: ["color", "navStyle"] }, { kind: "component", type: BsPriorityNavComponent, selector: "bs-priority-nav", inputs: ["moreLabel", "moreLabelTemplate", "collapseAt", "overflowFrom", "hideEmptyMore", "ariaLabel"], outputs: ["overflowChange"] }, { kind: "directive", type: BsPriorityNavItemDirective, selector: "[bsPriorityNavItem]", inputs: ["bsPriorityNavItem", "bsPriorityNavItemHideBelow"] }, { kind: "component", type: SparkQueryGridComponent, selector: "spark-query-grid", inputs: ["queryId", "parentId", "parentType", "data", "search", "columns", "reloadToken", "settings", "selection", "rowRoute"], outputs: ["settingsChange", "selectionChange", "error", "rowClicked", "customActionExecuted"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1549
+ function sparkActionClass(definition, size) {
1550
+ const variant = definition.variant?.toLowerCase();
1551
+ const sizeClass = size ? ` btn-${size}` : "";
1552
+ switch (variant) {
1553
+ case "danger":
1554
+ case "warning":
1555
+ case "primary":
1556
+ case "secondary":
1557
+ case "success": return `btn btn-${variant}${sizeClass}`;
1558
+ default: return `btn btn-outline-primary${sizeClass}`;
1559
+ }
1122
1560
  }
1123
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryCardComponent, decorators: [{
1124
- type: Component,
1125
- args: [{ selector: 'spark-query-card', imports: [CommonModule, BsCardComponent, BsCardHeaderComponent, BsPriorityNavComponent, BsPriorityNavItemDirective, SparkQueryGridComponent, ResolveTranslationPipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n The card is always rendered. A host that wants no card uses <spark-query-grid> directly \u2014 which\n is what the old `showCard=\"false\"` meant, expressed as a component choice rather than a flag.\n-->\n<bs-card class=\"d-block my-3\">\n <bs-card-header>\n <div class=\"d-flex align-items-center gap-2\">\n @if (iconTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: query() }\"></ng-container>\n }\n\n @if (actionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: customActions(), selection: selection(), query: query() }\"></ng-container>\n } @else if (customActions().length) {\n <!-- The default: the actions the server declares for this type. The nav renders only when\n there are any, so a query without actions is pixel-identical to a plain caption.\n\n Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rule. -->\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @for (action of customActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" class=\"btn btn-sm btn-outline-primary\"\n [disabled]=\"!grid()?.isActionEnabled(action)\" (click)=\"grid()?.onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n </bs-priority-nav>\n }\n\n <!--\n Caption last, actions first. The auto margin is conditional rather than fixed, and that is\n the point: with actions present `ms-auto` pushes the caption to the trailing edge, and with\n none it falls back to `me-auto` so a plain card looks exactly as it always did. A fixed\n `ms-auto` would have right-aligned the caption of every action-less query card in every app\n \u2014 a change nobody asked for, to fix a layout only some cards have.\n -->\n <span [class.ms-auto]=\"customActions().length\" [class.me-auto]=\"!customActions().length\">\n @if (captionTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: caption(), query: query() }\"></ng-container>\n } @else {\n {{ caption() }}\n }\n </span>\n </div>\n </bs-card-header>\n\n <div class=\"p-3\">\n <spark-query-grid\n [queryId]=\"queryId()\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [reloadToken]=\"reloadToken()\"\n [data]=\"data()\"\n [search]=\"search()\"\n [rowRoute]=\"rowRoute()\"\n (error)=\"error.emit($event)\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n </div>\n</bs-card>\n" }]
1126
- }], propDecorators: { queryId: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryId", required: true }] }], parentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentId", required: false }] }], parentType: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentType", required: false }] }], reloadToken: [{ type: i0.Input, args: [{ isSignal: true, alias: "reloadToken", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], search: [{ type: i0.Input, args: [{ isSignal: true, alias: "search", required: false }] }], iconTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconTemplate", required: false }] }], captionTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionTemplate", required: false }] }], actionsTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionsTemplate", required: false }] }], error: [{ type: i0.Output, args: ["error"] }], rowClicked: [{ type: i0.Output, args: ["rowClicked"] }], customActionExecuted: [{ type: i0.Output, args: ["customActionExecuted"] }], rowRoute: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowRoute", required: false }] }], iconSlots: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SparkQueryIconDirective), { isSignal: true }] }], captionSlots: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SparkQueryCaptionDirective), { isSignal: true }] }], actionSlots: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SparkQueryActionsDirective), { isSignal: true }] }], grid: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SparkQueryGridComponent), { isSignal: true }] }] } });
1127
-
1128
- /**
1129
- * Generated bundle index. Do not edit.
1130
- */
1561
+ const SPARK_SEARCH_DEBOUNCE_MS = 300;
1562
+ var SparkSearchBoxComponent = class SparkSearchBoxComponent {
1563
+ term = model("", ...ngDevMode ? [{ debugName: "term" }] : /* istanbul ignore next */ []);
1564
+ size = input("md", ...ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []);
1565
+ debounce = input(300, ...ngDevMode ? [{ debugName: "debounce" }] : /* istanbul ignore next */ []);
1566
+ text = signal("", ...ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []);
1567
+ timer = null;
1568
+ constructor() {
1569
+ effect(() => {
1570
+ const term = this.term();
1571
+ untracked(() => {
1572
+ if (term !== this.text()) {
1573
+ this.cancelPending();
1574
+ this.text.set(term);
1575
+ }
1576
+ });
1577
+ });
1578
+ inject(DestroyRef).onDestroy(() => this.cancelPending());
1579
+ }
1580
+ onInput(value) {
1581
+ this.text.set(value ?? "");
1582
+ this.cancelPending();
1583
+ const delay = this.debounce();
1584
+ if (delay <= 0 || !value) {
1585
+ this.term.set(value ?? "");
1586
+ return;
1587
+ }
1588
+ this.timer = setTimeout(() => {
1589
+ this.timer = null;
1590
+ this.term.set(this.text());
1591
+ }, delay);
1592
+ }
1593
+ clear() {
1594
+ this.cancelPending();
1595
+ this.text.set("");
1596
+ this.term.set("");
1597
+ }
1598
+ cancelPending() {
1599
+ if (this.timer !== null) {
1600
+ clearTimeout(this.timer);
1601
+ this.timer = null;
1602
+ }
1603
+ }
1604
+ static ɵfac = i0.ɵɵngDeclareFactory({
1605
+ minVersion: "12.0.0",
1606
+ version: "22.2.0",
1607
+ ngImport: i0,
1608
+ type: SparkSearchBoxComponent,
1609
+ deps: [],
1610
+ target: i0.ɵɵFactoryTarget.Component
1611
+ });
1612
+ static ɵcmp = i0.ɵɵngDeclareComponent({
1613
+ minVersion: "17.0.0",
1614
+ version: "22.2.0",
1615
+ type: SparkSearchBoxComponent,
1616
+ isStandalone: true,
1617
+ selector: "spark-search-box",
1618
+ inputs: {
1619
+ term: {
1620
+ classPropertyName: "term",
1621
+ publicName: "term",
1622
+ isSignal: true,
1623
+ isRequired: false,
1624
+ transformFunction: null
1625
+ },
1626
+ size: {
1627
+ classPropertyName: "size",
1628
+ publicName: "size",
1629
+ isSignal: true,
1630
+ isRequired: false,
1631
+ transformFunction: null
1632
+ },
1633
+ debounce: {
1634
+ classPropertyName: "debounce",
1635
+ publicName: "debounce",
1636
+ isSignal: true,
1637
+ isRequired: false,
1638
+ transformFunction: null
1639
+ }
1640
+ },
1641
+ outputs: { term: "termChange" },
1642
+ ngImport: i0,
1643
+ template: `
1644
+ <bs-form>
1645
+ <bs-input-group [size]="size()">
1646
+ <span class="addon">
1647
+ <spark-icon name="search" />
1648
+ </span>
1649
+ <input
1650
+ type="text"
1651
+ class="spark-search-input"
1652
+ [attr.aria-label]="'common.search' | t"
1653
+ [placeholder]="'common.search' | t"
1654
+ [ngModel]="text()"
1655
+ (ngModelChange)="onInput($event)"
1656
+ (keydown.escape)="clear()">
1657
+ @if (text()) {
1658
+ <button
1659
+ type="button"
1660
+ class="btn btn-outline-secondary spark-search-clear"
1661
+ [attr.aria-label]="'common.clearSearch' | t"
1662
+ (click)="clear()">
1663
+ <spark-icon name="x-lg" />
1664
+ </button>
1665
+ }
1666
+ </bs-input-group>
1667
+ </bs-form>
1668
+ `,
1669
+ isInline: true,
1670
+ styles: [":host{display:block}:host ::ng-deep form{margin:0}\n"],
1671
+ dependencies: [
1672
+ {
1673
+ kind: "ngmodule",
1674
+ type: FormsModule
1675
+ },
1676
+ {
1677
+ kind: "directive",
1678
+ type: i1.DefaultValueAccessor,
1679
+ selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"
1680
+ },
1681
+ {
1682
+ kind: "directive",
1683
+ type: i1.NgControlStatus,
1684
+ selector: "[formControlName],[ngModel],[formControl]"
1685
+ },
1686
+ {
1687
+ kind: "directive",
1688
+ type: i1.NgModel,
1689
+ selector: "[ngModel]:not([formControlName]):not([formControl])",
1690
+ inputs: [
1691
+ "name",
1692
+ "disabled",
1693
+ "ngModel",
1694
+ "ngModelOptions"
1695
+ ],
1696
+ outputs: ["ngModelChange"],
1697
+ exportAs: ["ngModel"]
1698
+ },
1699
+ {
1700
+ kind: "component",
1701
+ type: BsFormComponent,
1702
+ selector: "bs-form",
1703
+ inputs: ["action", "method"],
1704
+ outputs: ["submitted"]
1705
+ },
1706
+ {
1707
+ kind: "directive",
1708
+ type: BsFormControlDirective,
1709
+ selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)"
1710
+ },
1711
+ {
1712
+ kind: "component",
1713
+ type: BsInputGroupComponent,
1714
+ selector: "bs-input-group",
1715
+ inputs: ["size"]
1716
+ },
1717
+ {
1718
+ kind: "component",
1719
+ type: SparkIconComponent,
1720
+ selector: "spark-icon",
1721
+ inputs: ["name"]
1722
+ },
1723
+ {
1724
+ kind: "pipe",
1725
+ type: TranslateKeyPipe,
1726
+ name: "t"
1727
+ }
1728
+ ],
1729
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
1730
+ });
1731
+ };
1732
+ i0.ɵɵngDeclareClassMetadata({
1733
+ minVersion: "12.0.0",
1734
+ version: "22.2.0",
1735
+ ngImport: i0,
1736
+ type: SparkSearchBoxComponent,
1737
+ decorators: [{
1738
+ type: Component,
1739
+ args: [{
1740
+ selector: "spark-search-box",
1741
+ imports: [
1742
+ FormsModule,
1743
+ BsFormComponent,
1744
+ BsFormControlDirective,
1745
+ BsInputGroupComponent,
1746
+ SparkIconComponent,
1747
+ TranslateKeyPipe
1748
+ ],
1749
+ template: `
1750
+ <bs-form>
1751
+ <bs-input-group [size]="size()">
1752
+ <span class="addon">
1753
+ <spark-icon name="search" />
1754
+ </span>
1755
+ <input
1756
+ type="text"
1757
+ class="spark-search-input"
1758
+ [attr.aria-label]="'common.search' | t"
1759
+ [placeholder]="'common.search' | t"
1760
+ [ngModel]="text()"
1761
+ (ngModelChange)="onInput($event)"
1762
+ (keydown.escape)="clear()">
1763
+ @if (text()) {
1764
+ <button
1765
+ type="button"
1766
+ class="btn btn-outline-secondary spark-search-clear"
1767
+ [attr.aria-label]="'common.clearSearch' | t"
1768
+ (click)="clear()">
1769
+ <spark-icon name="x-lg" />
1770
+ </button>
1771
+ }
1772
+ </bs-input-group>
1773
+ </bs-form>
1774
+ `,
1775
+ changeDetection: ChangeDetectionStrategy.OnPush,
1776
+ styles: [":host{display:block}:host ::ng-deep form{margin:0}\n"]
1777
+ }]
1778
+ }],
1779
+ ctorParameters: () => [],
1780
+ propDecorators: {
1781
+ term: [{
1782
+ type: i0.Input,
1783
+ args: [{
1784
+ isSignal: true,
1785
+ alias: "term",
1786
+ required: false
1787
+ }]
1788
+ }, {
1789
+ type: i0.Output,
1790
+ args: ["termChange"]
1791
+ }],
1792
+ size: [{
1793
+ type: i0.Input,
1794
+ args: [{
1795
+ isSignal: true,
1796
+ alias: "size",
1797
+ required: false
1798
+ }]
1799
+ }],
1800
+ debounce: [{
1801
+ type: i0.Input,
1802
+ args: [{
1803
+ isSignal: true,
1804
+ alias: "debounce",
1805
+ required: false
1806
+ }]
1807
+ }]
1808
+ }
1809
+ });
1810
+ var SparkQueryCardComponent = class SparkQueryCardComponent {
1811
+ lang = inject(SparkLanguageService);
1812
+ queryId = input.required(...ngDevMode ? [{ debugName: "queryId" }] : /* istanbul ignore next */ []);
1813
+ parentId = input("", ...ngDevMode ? [{ debugName: "parentId" }] : /* istanbul ignore next */ []);
1814
+ parentType = input("", ...ngDevMode ? [{ debugName: "parentType" }] : /* istanbul ignore next */ []);
1815
+ reloadToken = input(null, ...ngDevMode ? [{ debugName: "reloadToken" }] : /* istanbul ignore next */ []);
1816
+ data = input(null, ...ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []);
1817
+ search = input("", ...ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []);
1818
+ searchable = input(true, ...ngDevMode ? [{ debugName: "searchable" }] : /* istanbul ignore next */ []);
1819
+ searchTerm = linkedSignal(() => this.search(), ...ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []);
1820
+ showSearch = computed(() => this.searchable() && this.data() === null, ...ngDevMode ? [{ debugName: "showSearch" }] : /* istanbul ignore next */ []);
1821
+ selectionMode = input(null, ...ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []);
1822
+ deleted = input(null, ...ngDevMode ? [{ debugName: "deleted" }] : /* istanbul ignore next */ []);
1823
+ parentDeleted = input(false, ...ngDevMode ? [{ debugName: "parentDeleted" }] : /* istanbul ignore next */ []);
1824
+ iconTemplate = input(null, ...ngDevMode ? [{ debugName: "iconTemplate" }] : /* istanbul ignore next */ []);
1825
+ captionTemplate = input(null, ...ngDevMode ? [{ debugName: "captionTemplate" }] : /* istanbul ignore next */ []);
1826
+ actionsTemplate = input(null, ...ngDevMode ? [{ debugName: "actionsTemplate" }] : /* istanbul ignore next */ []);
1827
+ error = output();
1828
+ rowClicked = output();
1829
+ customActionExecuted = output();
1830
+ rowRoute = input(null, ...ngDevMode ? [{ debugName: "rowRoute" }] : /* istanbul ignore next */ []);
1831
+ colors = Color;
1832
+ iconSlots = contentChildren(SparkQueryIconDirective, ...ngDevMode ? [{ debugName: "iconSlots" }] : /* istanbul ignore next */ []);
1833
+ captionSlots = contentChildren(SparkQueryCaptionDirective, ...ngDevMode ? [{ debugName: "captionSlots" }] : /* istanbul ignore next */ []);
1834
+ actionSlots = contentChildren(SparkQueryActionsDirective, ...ngDevMode ? [{ debugName: "actionSlots" }] : /* istanbul ignore next */ []);
1835
+ grid = viewChild(SparkQueryGridComponent, ...ngDevMode ? [{ debugName: "grid" }] : /* istanbul ignore next */ []);
1836
+ query = computed(() => this.grid()?.query() ?? null, ...ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []);
1837
+ iconTpl = computed(() => slotFor(this.iconSlots(), this.query())?.templateRef ?? this.iconTemplate(), ...ngDevMode ? [{ debugName: "iconTpl" }] : /* istanbul ignore next */ []);
1838
+ captionTpl = computed(() => slotFor(this.captionSlots(), this.query())?.templateRef ?? this.captionTemplate(), ...ngDevMode ? [{ debugName: "captionTpl" }] : /* istanbul ignore next */ []);
1839
+ actionsTpl = computed(() => slotFor(this.actionSlots(), this.query())?.templateRef ?? this.actionsTemplate(), ...ngDevMode ? [{ debugName: "actionsTpl" }] : /* istanbul ignore next */ []);
1840
+ caption = computed(() => {
1841
+ const q = this.query();
1842
+ return (q?.description ? this.lang.resolve(q.description) : "") || q?.name || "";
1843
+ }, ...ngDevMode ? [{ debugName: "caption" }] : /* istanbul ignore next */ []);
1844
+ customActions = computed(() => this.grid()?.visibleCustomActions() ?? [], ...ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []);
1845
+ toolbarActions = computed(() => this.grid()?.toolbarActions() ?? [], ...ngDevMode ? [{ debugName: "toolbarActions" }] : /* istanbul ignore next */ []);
1846
+ actionClass(action) {
1847
+ return `${sparkActionClass(action.definition, "sm")} rounded-0`;
1848
+ }
1849
+ isEnabled(action) {
1850
+ return this.grid()?.isToolbarActionEnabled(action) ?? false;
1851
+ }
1852
+ run(action) {
1853
+ this.grid()?.runToolbarAction(action);
1854
+ }
1855
+ selection = computed(() => this.grid()?.selection() ?? [], ...ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []);
1856
+ static ɵfac = i0.ɵɵngDeclareFactory({
1857
+ minVersion: "12.0.0",
1858
+ version: "22.2.0",
1859
+ ngImport: i0,
1860
+ type: SparkQueryCardComponent,
1861
+ deps: [],
1862
+ target: i0.ɵɵFactoryTarget.Component
1863
+ });
1864
+ static ɵcmp = i0.ɵɵngDeclareComponent({
1865
+ minVersion: "17.0.0",
1866
+ version: "22.2.0",
1867
+ type: SparkQueryCardComponent,
1868
+ isStandalone: true,
1869
+ selector: "spark-query-card",
1870
+ inputs: {
1871
+ queryId: {
1872
+ classPropertyName: "queryId",
1873
+ publicName: "queryId",
1874
+ isSignal: true,
1875
+ isRequired: true,
1876
+ transformFunction: null
1877
+ },
1878
+ parentId: {
1879
+ classPropertyName: "parentId",
1880
+ publicName: "parentId",
1881
+ isSignal: true,
1882
+ isRequired: false,
1883
+ transformFunction: null
1884
+ },
1885
+ parentType: {
1886
+ classPropertyName: "parentType",
1887
+ publicName: "parentType",
1888
+ isSignal: true,
1889
+ isRequired: false,
1890
+ transformFunction: null
1891
+ },
1892
+ reloadToken: {
1893
+ classPropertyName: "reloadToken",
1894
+ publicName: "reloadToken",
1895
+ isSignal: true,
1896
+ isRequired: false,
1897
+ transformFunction: null
1898
+ },
1899
+ data: {
1900
+ classPropertyName: "data",
1901
+ publicName: "data",
1902
+ isSignal: true,
1903
+ isRequired: false,
1904
+ transformFunction: null
1905
+ },
1906
+ search: {
1907
+ classPropertyName: "search",
1908
+ publicName: "search",
1909
+ isSignal: true,
1910
+ isRequired: false,
1911
+ transformFunction: null
1912
+ },
1913
+ searchable: {
1914
+ classPropertyName: "searchable",
1915
+ publicName: "searchable",
1916
+ isSignal: true,
1917
+ isRequired: false,
1918
+ transformFunction: null
1919
+ },
1920
+ selectionMode: {
1921
+ classPropertyName: "selectionMode",
1922
+ publicName: "selectionMode",
1923
+ isSignal: true,
1924
+ isRequired: false,
1925
+ transformFunction: null
1926
+ },
1927
+ deleted: {
1928
+ classPropertyName: "deleted",
1929
+ publicName: "deleted",
1930
+ isSignal: true,
1931
+ isRequired: false,
1932
+ transformFunction: null
1933
+ },
1934
+ parentDeleted: {
1935
+ classPropertyName: "parentDeleted",
1936
+ publicName: "parentDeleted",
1937
+ isSignal: true,
1938
+ isRequired: false,
1939
+ transformFunction: null
1940
+ },
1941
+ iconTemplate: {
1942
+ classPropertyName: "iconTemplate",
1943
+ publicName: "iconTemplate",
1944
+ isSignal: true,
1945
+ isRequired: false,
1946
+ transformFunction: null
1947
+ },
1948
+ captionTemplate: {
1949
+ classPropertyName: "captionTemplate",
1950
+ publicName: "captionTemplate",
1951
+ isSignal: true,
1952
+ isRequired: false,
1953
+ transformFunction: null
1954
+ },
1955
+ actionsTemplate: {
1956
+ classPropertyName: "actionsTemplate",
1957
+ publicName: "actionsTemplate",
1958
+ isSignal: true,
1959
+ isRequired: false,
1960
+ transformFunction: null
1961
+ },
1962
+ rowRoute: {
1963
+ classPropertyName: "rowRoute",
1964
+ publicName: "rowRoute",
1965
+ isSignal: true,
1966
+ isRequired: false,
1967
+ transformFunction: null
1968
+ }
1969
+ },
1970
+ outputs: {
1971
+ error: "error",
1972
+ rowClicked: "rowClicked",
1973
+ customActionExecuted: "customActionExecuted"
1974
+ },
1975
+ queries: [
1976
+ {
1977
+ propertyName: "iconSlots",
1978
+ predicate: SparkQueryIconDirective,
1979
+ isSignal: true
1980
+ },
1981
+ {
1982
+ propertyName: "captionSlots",
1983
+ predicate: SparkQueryCaptionDirective,
1984
+ isSignal: true
1985
+ },
1986
+ {
1987
+ propertyName: "actionSlots",
1988
+ predicate: SparkQueryActionsDirective,
1989
+ isSignal: true
1990
+ }
1991
+ ],
1992
+ viewQueries: [{
1993
+ propertyName: "grid",
1994
+ first: true,
1995
+ predicate: SparkQueryGridComponent,
1996
+ descendants: true,
1997
+ isSignal: true
1998
+ }],
1999
+ ngImport: i0,
2000
+ template: "<!--\n The card is always rendered. A host that wants no card uses <spark-query-grid> directly — which\n is what the old `showCard=\"false\"` meant, expressed as a component choice rather than a flag.\n-->\n<bs-card class=\"d-block my-3\">\n <bs-card-header>\n <div class=\"d-flex align-items-center gap-2\">\n @if (iconTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: query() }\"></ng-container>\n }\n\n <!--\n Caption first, actions on the trailing edge (#460 M15, as query-grid-card-PRD.md always said,\n and as Vidyano's sub-query tabs do). `me-auto` on the caption pushes the actions right; a card\n without actions looks exactly as a plain caption did.\n -->\n <span class=\"me-auto spark-query-card-caption\">\n @if (captionTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: caption(), query: query() }\"></ng-container>\n } @else {\n {{ caption() }}\n }\n </span>\n\n @if (actionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: customActions(), actions: toolbarActions(), selection: selection(), query: query() }\"></ng-container>\n } @else if (toolbarActions().length) {\n <!-- The default: New, Delete and the custom actions the server offers this caller, with the\n overflow in the priority nav under the translated \"More\" (common.more). The nav renders only when there are any.\n\n Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rules. -->\n <bs-priority-nav class=\"spark-query-card-actions\" [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @for (action of toolbarActions(); track action.name) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"actionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isEnabled(action)\" (click)=\"run(action)\">\n @if (action.definition.icon && action.kind !== 'custom') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.displayName | resolveTranslation }}\n </button>\n }\n </bs-priority-nav>\n }\n\n <!-- The query search, on the trailing edge next to the actions, as Vidyano's sub-query tabs\n have it (#460 M15, D17 addendum). The same box as the query-list page; it feeds the\n grid's `search`, which the server applies together with the parent. Absent for bound\n `data`, which the grid never refetches. -->\n @if (showSearch()) {\n <spark-search-box class=\"spark-query-card-search\" size=\"sm\" [(term)]=\"searchTerm\" />\n }\n </div>\n </bs-card-header>\n\n <div class=\"p-3\">\n <spark-query-grid\n [queryId]=\"queryId()\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [reloadToken]=\"reloadToken()\"\n [data]=\"data()\"\n [search]=\"searchTerm()\"\n [rowRoute]=\"rowRoute()\"\n [selectionModeSetting]=\"selectionMode()\"\n [deleted]=\"deleted()\"\n [parentDeleted]=\"parentDeleted()\"\n (error)=\"error.emit($event)\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n </div>\n</bs-card>\n",
2001
+ styles: [".spark-query-card-search{flex:0 1 14rem;min-width:8rem}\n"],
2002
+ dependencies: [
2003
+ {
2004
+ kind: "ngmodule",
2005
+ type: CommonModule
2006
+ },
2007
+ {
2008
+ kind: "directive",
2009
+ type: i1$1.NgTemplateOutlet,
2010
+ selector: "[ngTemplateOutlet]",
2011
+ inputs: [
2012
+ "ngTemplateOutletContext",
2013
+ "ngTemplateOutlet",
2014
+ "ngTemplateOutletInjector"
2015
+ ]
2016
+ },
2017
+ {
2018
+ kind: "component",
2019
+ type: BsCardComponent,
2020
+ selector: "bs-card",
2021
+ inputs: ["color", "outline"]
2022
+ },
2023
+ {
2024
+ kind: "component",
2025
+ type: BsCardHeaderComponent,
2026
+ selector: "bs-card-header",
2027
+ inputs: ["color", "navStyle"]
2028
+ },
2029
+ {
2030
+ kind: "component",
2031
+ type: BsPriorityNavComponent,
2032
+ selector: "bs-priority-nav",
2033
+ inputs: [
2034
+ "moreLabel",
2035
+ "moreLabelTemplate",
2036
+ "collapseAt",
2037
+ "overflowFrom",
2038
+ "hideEmptyMore",
2039
+ "ariaLabel"
2040
+ ],
2041
+ outputs: ["overflowChange"]
2042
+ },
2043
+ {
2044
+ kind: "directive",
2045
+ type: BsPriorityNavItemDirective,
2046
+ selector: "[bsPriorityNavItem]",
2047
+ inputs: ["bsPriorityNavItem", "bsPriorityNavItemHideBelow"]
2048
+ },
2049
+ {
2050
+ kind: "component",
2051
+ type: SparkQueryGridComponent,
2052
+ selector: "spark-query-grid",
2053
+ inputs: [
2054
+ "queryId",
2055
+ "parentId",
2056
+ "parentType",
2057
+ "data",
2058
+ "search",
2059
+ "columns",
2060
+ "reloadToken",
2061
+ "deleted",
2062
+ "parentDeleted",
2063
+ "settings",
2064
+ "selection",
2065
+ "rowRoute",
2066
+ "selectionModeSetting"
2067
+ ],
2068
+ outputs: [
2069
+ "settingsChange",
2070
+ "selectionChange",
2071
+ "error",
2072
+ "rowClicked",
2073
+ "customActionExecuted",
2074
+ "createClicked"
2075
+ ]
2076
+ },
2077
+ {
2078
+ kind: "component",
2079
+ type: SparkSearchBoxComponent,
2080
+ selector: "spark-search-box",
2081
+ inputs: [
2082
+ "term",
2083
+ "size",
2084
+ "debounce"
2085
+ ],
2086
+ outputs: ["termChange"]
2087
+ },
2088
+ {
2089
+ kind: "component",
2090
+ type: SparkIconComponent,
2091
+ selector: "spark-icon",
2092
+ inputs: ["name"]
2093
+ },
2094
+ {
2095
+ kind: "pipe",
2096
+ type: ResolveTranslationPipe,
2097
+ name: "resolveTranslation"
2098
+ }
2099
+ ],
2100
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
2101
+ });
2102
+ };
2103
+ i0.ɵɵngDeclareClassMetadata({
2104
+ minVersion: "12.0.0",
2105
+ version: "22.2.0",
2106
+ ngImport: i0,
2107
+ type: SparkQueryCardComponent,
2108
+ decorators: [{
2109
+ type: Component,
2110
+ args: [{
2111
+ selector: "spark-query-card",
2112
+ imports: [
2113
+ CommonModule,
2114
+ BsCardComponent,
2115
+ BsCardHeaderComponent,
2116
+ BsPriorityNavComponent,
2117
+ BsPriorityNavItemDirective,
2118
+ SparkQueryGridComponent,
2119
+ SparkSearchBoxComponent,
2120
+ ResolveTranslationPipe,
2121
+ SparkIconComponent
2122
+ ],
2123
+ changeDetection: ChangeDetectionStrategy.OnPush,
2124
+ template: "<!--\n The card is always rendered. A host that wants no card uses <spark-query-grid> directly — which\n is what the old `showCard=\"false\"` meant, expressed as a component choice rather than a flag.\n-->\n<bs-card class=\"d-block my-3\">\n <bs-card-header>\n <div class=\"d-flex align-items-center gap-2\">\n @if (iconTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: query() }\"></ng-container>\n }\n\n <!--\n Caption first, actions on the trailing edge (#460 M15, as query-grid-card-PRD.md always said,\n and as Vidyano's sub-query tabs do). `me-auto` on the caption pushes the actions right; a card\n without actions looks exactly as a plain caption did.\n -->\n <span class=\"me-auto spark-query-card-caption\">\n @if (captionTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: caption(), query: query() }\"></ng-container>\n } @else {\n {{ caption() }}\n }\n </span>\n\n @if (actionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: customActions(), actions: toolbarActions(), selection: selection(), query: query() }\"></ng-container>\n } @else if (toolbarActions().length) {\n <!-- The default: New, Delete and the custom actions the server offers this caller, with the\n overflow in the priority nav under the translated \"More\" (common.more). The nav renders only when there are any.\n\n Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rules. -->\n <bs-priority-nav class=\"spark-query-card-actions\" [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @for (action of toolbarActions(); track action.name) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"actionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isEnabled(action)\" (click)=\"run(action)\">\n @if (action.definition.icon && action.kind !== 'custom') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.displayName | resolveTranslation }}\n </button>\n }\n </bs-priority-nav>\n }\n\n <!-- The query search, on the trailing edge next to the actions, as Vidyano's sub-query tabs\n have it (#460 M15, D17 addendum). The same box as the query-list page; it feeds the\n grid's `search`, which the server applies together with the parent. Absent for bound\n `data`, which the grid never refetches. -->\n @if (showSearch()) {\n <spark-search-box class=\"spark-query-card-search\" size=\"sm\" [(term)]=\"searchTerm\" />\n }\n </div>\n </bs-card-header>\n\n <div class=\"p-3\">\n <spark-query-grid\n [queryId]=\"queryId()\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [reloadToken]=\"reloadToken()\"\n [data]=\"data()\"\n [search]=\"searchTerm()\"\n [rowRoute]=\"rowRoute()\"\n [selectionModeSetting]=\"selectionMode()\"\n [deleted]=\"deleted()\"\n [parentDeleted]=\"parentDeleted()\"\n (error)=\"error.emit($event)\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n </div>\n</bs-card>\n",
2125
+ styles: [".spark-query-card-search{flex:0 1 14rem;min-width:8rem}\n"]
2126
+ }]
2127
+ }],
2128
+ propDecorators: {
2129
+ queryId: [{
2130
+ type: i0.Input,
2131
+ args: [{
2132
+ isSignal: true,
2133
+ alias: "queryId",
2134
+ required: true
2135
+ }]
2136
+ }],
2137
+ parentId: [{
2138
+ type: i0.Input,
2139
+ args: [{
2140
+ isSignal: true,
2141
+ alias: "parentId",
2142
+ required: false
2143
+ }]
2144
+ }],
2145
+ parentType: [{
2146
+ type: i0.Input,
2147
+ args: [{
2148
+ isSignal: true,
2149
+ alias: "parentType",
2150
+ required: false
2151
+ }]
2152
+ }],
2153
+ reloadToken: [{
2154
+ type: i0.Input,
2155
+ args: [{
2156
+ isSignal: true,
2157
+ alias: "reloadToken",
2158
+ required: false
2159
+ }]
2160
+ }],
2161
+ data: [{
2162
+ type: i0.Input,
2163
+ args: [{
2164
+ isSignal: true,
2165
+ alias: "data",
2166
+ required: false
2167
+ }]
2168
+ }],
2169
+ search: [{
2170
+ type: i0.Input,
2171
+ args: [{
2172
+ isSignal: true,
2173
+ alias: "search",
2174
+ required: false
2175
+ }]
2176
+ }],
2177
+ searchable: [{
2178
+ type: i0.Input,
2179
+ args: [{
2180
+ isSignal: true,
2181
+ alias: "searchable",
2182
+ required: false
2183
+ }]
2184
+ }],
2185
+ selectionMode: [{
2186
+ type: i0.Input,
2187
+ args: [{
2188
+ isSignal: true,
2189
+ alias: "selectionMode",
2190
+ required: false
2191
+ }]
2192
+ }],
2193
+ deleted: [{
2194
+ type: i0.Input,
2195
+ args: [{
2196
+ isSignal: true,
2197
+ alias: "deleted",
2198
+ required: false
2199
+ }]
2200
+ }],
2201
+ parentDeleted: [{
2202
+ type: i0.Input,
2203
+ args: [{
2204
+ isSignal: true,
2205
+ alias: "parentDeleted",
2206
+ required: false
2207
+ }]
2208
+ }],
2209
+ iconTemplate: [{
2210
+ type: i0.Input,
2211
+ args: [{
2212
+ isSignal: true,
2213
+ alias: "iconTemplate",
2214
+ required: false
2215
+ }]
2216
+ }],
2217
+ captionTemplate: [{
2218
+ type: i0.Input,
2219
+ args: [{
2220
+ isSignal: true,
2221
+ alias: "captionTemplate",
2222
+ required: false
2223
+ }]
2224
+ }],
2225
+ actionsTemplate: [{
2226
+ type: i0.Input,
2227
+ args: [{
2228
+ isSignal: true,
2229
+ alias: "actionsTemplate",
2230
+ required: false
2231
+ }]
2232
+ }],
2233
+ error: [{
2234
+ type: i0.Output,
2235
+ args: ["error"]
2236
+ }],
2237
+ rowClicked: [{
2238
+ type: i0.Output,
2239
+ args: ["rowClicked"]
2240
+ }],
2241
+ customActionExecuted: [{
2242
+ type: i0.Output,
2243
+ args: ["customActionExecuted"]
2244
+ }],
2245
+ rowRoute: [{
2246
+ type: i0.Input,
2247
+ args: [{
2248
+ isSignal: true,
2249
+ alias: "rowRoute",
2250
+ required: false
2251
+ }]
2252
+ }],
2253
+ iconSlots: [{
2254
+ type: i0.ContentChildren,
2255
+ args: [i0.forwardRef(() => SparkQueryIconDirective), { isSignal: true }]
2256
+ }],
2257
+ captionSlots: [{
2258
+ type: i0.ContentChildren,
2259
+ args: [i0.forwardRef(() => SparkQueryCaptionDirective), { isSignal: true }]
2260
+ }],
2261
+ actionSlots: [{
2262
+ type: i0.ContentChildren,
2263
+ args: [i0.forwardRef(() => SparkQueryActionsDirective), { isSignal: true }]
2264
+ }],
2265
+ grid: [{
2266
+ type: i0.ViewChild,
2267
+ args: [i0.forwardRef(() => SparkQueryGridComponent), { isSignal: true }]
2268
+ }]
2269
+ }
2270
+ });
2271
+ export { SPARK_GRID_PAGE_SIZES, SPARK_SEARCH_DEBOUNCE_MS, SparkGridCellComponent, SparkGridRenderers, SparkQueryActionsContext, SparkQueryActionsDirective, SparkQueryCaptionContext, SparkQueryCaptionDirective, SparkQueryCardComponent, SparkQueryGridComponent, SparkQueryIconDirective, SparkQuerySlotContext, SparkSearchBoxComponent, initialGridSettings, isVirtualScrollingQuery, slotFor, sparkActionClass, visibleGridAttributes };
1131
2272
 
1132
- export { SPARK_GRID_PAGE_SIZES, SparkGridCellComponent, SparkGridRenderers, SparkQueryActionsContext, SparkQueryActionsDirective, SparkQueryCaptionContext, SparkQueryCaptionDirective, SparkQueryCardComponent, SparkQueryGridComponent, SparkQueryIconDirective, SparkQuerySlotContext, initialGridSettings, isVirtualScrollingQuery, slotFor, visibleGridAttributes };
1133
- //# sourceMappingURL=mintplayer-ng-spark-grid.mjs.map
2273
+ //# sourceMappingURL=mintplayer-ng-spark-grid.mjs.map