@mintplayer/ng-spark 22.3.0 → 22.5.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 (32) hide show
  1. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +10 -1
  2. package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
  3. package/fesm2022/mintplayer-ng-spark-grid.mjs +772 -23
  4. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  5. package/fesm2022/mintplayer-ng-spark-models.mjs +233 -1
  6. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  7. package/fesm2022/mintplayer-ng-spark-pipes.mjs +31 -5
  8. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  9. package/fesm2022/mintplayer-ng-spark-po-create.mjs +2 -2
  10. package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
  11. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +28 -302
  12. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  13. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +2 -2
  14. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  15. package/fesm2022/mintplayer-ng-spark-po-form.mjs +352 -14
  16. package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
  17. package/fesm2022/mintplayer-ng-spark-query-list.mjs +137 -308
  18. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  19. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +6 -4
  20. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
  21. package/fesm2022/mintplayer-ng-spark-services.mjs +12 -0
  22. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  23. package/fesm2022/mintplayer-ng-spark.mjs +1 -1
  24. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  25. package/package.json +2 -2
  26. package/types/mintplayer-ng-spark-client-operations.d.ts +10 -1
  27. package/types/mintplayer-ng-spark-grid.d.ts +455 -19
  28. package/types/mintplayer-ng-spark-models.d.ts +119 -3
  29. package/types/mintplayer-ng-spark-po-detail.d.ts +18 -139
  30. package/types/mintplayer-ng-spark-po-form.d.ts +132 -4
  31. package/types/mintplayer-ng-spark-query-list.d.ts +53 -66
  32. package/types/mintplayer-ng-spark-services.d.ts +10 -0
@@ -1,9 +1,20 @@
1
- import { DatatableSettings } from '@mintplayer/ng-bootstrap/datatable';
2
- import { hasShowedOnFlag, ShowedOn } from '@mintplayer/ng-spark/models';
1
+ import { DatatableSettings, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective } from '@mintplayer/ng-bootstrap/datatable';
2
+ import { hasShowedOnFlag, ShowedOn, selectionModeFor, parseSelectionRule, filterQueryActions } from '@mintplayer/ng-spark/models';
3
3
  import * as i0 from '@angular/core';
4
- import { inject, Injectable } from '@angular/core';
5
- import { SPARK_ATTRIBUTE_RENDERERS, withDeclaredInputs, rendererValue } from '@mintplayer/ng-spark/renderers';
6
- import { SparkService } from '@mintplayer/ng-spark/services';
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, rendererValue, 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, AttributeValuePipe, ReferenceChipsPipe, TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';
16
+ import { BsCardComponent, BsCardHeaderComponent } from '@mintplayer/ng-bootstrap/card';
17
+ import { BsPriorityNavComponent, BsPriorityNavItemDirective } from '@mintplayer/ng-bootstrap/priority-nav';
7
18
 
8
19
  /** Page sizes offered by every Spark grid. */
9
20
  const SPARK_GRID_PAGE_SIZES = [10, 25, 50];
@@ -44,20 +55,21 @@ function isVirtualScrollingQuery(query) {
44
55
  }
45
56
 
46
57
  /**
47
- * The parts of a Spark grid that both grid components need and that were, until this existed,
48
- * written out twice.
49
- *
50
- * `spark-query-list` and `spark-sub-query` had byte-identical copies of the renderer lookup, the
51
- * renderer input construction and the lookup-reference loading around 120 lines between them.
52
- * That duplication is not a tidiness complaint: it is what produced the drift. The two copies
53
- * disagreed about `[indeterminate]`, about resetting permission state, about whether a fetch
54
- * failure surfaces or is swallowed, and about virtual-scroll sizing — four user-visible bugs, each
55
- * fixed on one side and not the other, because nothing made the two files move together.
56
- *
57
- * Kept deliberately small and stateless. The two components differ in real ways one is
58
- * route-coupled and carries streaming, search and a websocket dependency graph — so merging them
59
- * into a single component would drag all of that into every detail page's bundle. Shared logic
60
- * belongs here; shared *state* does not.
58
+ * Renderer lookup and lookup-reference loading for a Spark grid.
59
+ *
60
+ * This was extracted when `spark-query-list` and `spark-sub-query` were two components holding
61
+ * byte-identical copies of it around 120 lines between them. That duplication was not a
62
+ * tidiness complaint: it produced drift. The two copies disagreed about `[indeterminate]`, about
63
+ * resetting permission state, about whether a fetch failure surfaces or is swallowed, and about
64
+ * virtual-scroll sizing four user-visible bugs, each fixed on one side and not the other.
65
+ *
66
+ * There is now one grid, {@link SparkQueryGridComponent}, so the drift cannot recur. This stays
67
+ * separate because it is stateless service-shaped logic rather than view state, and because it is
68
+ * what a custom grid would need in order to render Spark cells at all.
69
+ *
70
+ * The constraint that shaped the split still holds and is why the grid takes rows as an input
71
+ * rather than owning a socket: streaming's dependency graph must not reach the bundle of every
72
+ * detail page.
61
73
  */
62
74
  class SparkGridRenderers {
63
75
  registry = inject(SPARK_ATTRIBUTE_RENDERERS);
@@ -75,10 +87,21 @@ class SparkGridRenderers {
75
87
  */
76
88
  columnInputsFor(component, item, attr) {
77
89
  const itemAttr = item.attributes.find(a => a.name === attr.name);
90
+ return this.cellInputsFor(component, rendererValue(itemAttr), attr, item);
91
+ }
92
+ /**
93
+ * The same contract for a row that is not a <see cref="PersistentObject"/>.
94
+ *
95
+ * An AsDetail row is a plain dictionary — embedded values with no id and no attribute list — so
96
+ * its value cannot be read the way {@link columnInputsFor} reads one. Only the extraction
97
+ * differs; the renderer contract is identical, and stating it once is what stops the two from
98
+ * drifting the way the cell markup already had.
99
+ */
100
+ cellInputsFor(component, value, column, item) {
78
101
  return withDeclaredInputs(component, {
79
- value: rendererValue(itemAttr),
80
- attribute: attr,
81
- options: attr.rendererOptions,
102
+ value,
103
+ attribute: column,
104
+ options: column.rendererOptions,
82
105
  item,
83
106
  });
84
107
  }
@@ -103,9 +126,735 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImpor
103
126
  args: [{ providedIn: 'root' }]
104
127
  }] });
105
128
 
129
+ /**
130
+ * Header slots for `<spark-query-card>`.
131
+ *
132
+ * Each directive marks a template that REPLACES one region of the card header. Supplying one
133
+ * leaves the other two alone — that is the whole point of having three rather than the single
134
+ * whole-header template they replace, where changing the icon meant re-implementing the caption
135
+ * and the action bar as well.
136
+ *
137
+ * An omitted slot is not an empty slot: the card renders its default. This is what keeps the
138
+ * auto-rendered sub-query working. A sub-query is rendered from `EntityTypeDefinition.Queries`
139
+ * with no host to project into, so a mechanism that only ever REPLACED chrome would leave that
140
+ * call site with nothing — which is why an earlier design had the query declare its own chrome
141
+ * server-side. Overriding a default needs no host; replacing one does.
142
+ *
143
+ * The two ends of that are visible in the defaults themselves. Neither `SparkQuery` nor
144
+ * `EntityType` carries an icon, so the server has nothing to say and the slot is the only
145
+ * source. Actions are the mirror image: the server declares them per type, so the default is
146
+ * the real answer and the slot is a rare override.
147
+ *
148
+ * Naming follows ng-bootstrap's `*bsDatatableColumn` convention — prefix, component, slot —
149
+ * with `spark`, since these are ng-spark directives and `bs` is another package's prefix. These
150
+ * are the first attribute directives in ng-spark; every existing selector is an element.
151
+ *
152
+ * ## Targeting one query
153
+ *
154
+ * Each slot takes an optional query alias or id as its value. A detail page renders one card per
155
+ * entry in `EntityTypeDefinition.Queries`, so a host that supplies a bare slot would decorate
156
+ * every one of them identically — rarely what is wanted. A targeted slot applies to that query;
157
+ * an untargeted one is the fallback for the rest.
158
+ *
159
+ * ```html
160
+ * <span *sparkQueryIcon="'cars'"><spark-icon name="car-front" /></span>
161
+ * <span *sparkQueryIcon><spark-icon name="table" /></span>
162
+ * ```
163
+ *
164
+ * Matching is case-insensitive, against the query's alias and then its id. This is the
165
+ * `bsPriorityNavItem` shape: a value input aliased to the selector, collected with
166
+ * `contentChildren`.
167
+ */
168
+ /** Resolves a slot list to the one that applies to a query: targeted first, then untargeted. */
169
+ function slotFor(slots, query) {
170
+ if (!slots.length)
171
+ return null;
172
+ const matches = (target) => !!target && (target.localeCompare(query?.alias ?? '', undefined, { sensitivity: 'accent' }) === 0 ||
173
+ target.localeCompare(query?.id ?? '', undefined, { sensitivity: 'accent' }) === 0);
174
+ // A targeted slot wins over the catch-all even when it is declared later: "this one query"
175
+ // is the more specific statement, and declaration order in a template is not a priority.
176
+ return slots.find(s => matches(s.forQuery())) ?? slots.find(s => !s.forQuery()) ?? null;
177
+ }
178
+ /**
179
+ * The icon at the header's leading edge. No default: nothing in the model describes one.
180
+ *
181
+ * ```html
182
+ * <spark-query-card [queryId]="'cars'">
183
+ * <spark-icon *sparkQueryIcon name="car-front" />
184
+ * </spark-query-card>
185
+ * ```
186
+ */
187
+ class SparkQueryIconDirective {
188
+ templateRef = inject(TemplateRef);
189
+ /** Query alias or id this slot applies to. Empty targets every query on the page. */
190
+ forQuery = input('', { ...(ngDevMode ? { debugName: "forQuery" } : /* istanbul ignore next */ {}), alias: 'sparkQueryIcon' });
191
+ static ngTemplateContextGuard(_dir, ctx) {
192
+ return true;
193
+ }
194
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
195
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.0", type: SparkQueryIconDirective, isStandalone: true, selector: "[sparkQueryIcon]", inputs: { forQuery: { classPropertyName: "forQuery", publicName: "sparkQueryIcon", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
196
+ }
197
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryIconDirective, decorators: [{
198
+ type: Directive,
199
+ args: [{ selector: '[sparkQueryIcon]' }]
200
+ }], propDecorators: { forQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkQueryIcon", required: false }] }] } });
201
+ /**
202
+ * The header caption. Defaults to the query's translated description, falling back to its name.
203
+ *
204
+ * The context carries that resolved default as `$implicit`, so a host can decorate it — add a
205
+ * count, a badge — without re-resolving the translation itself.
206
+ */
207
+ class SparkQueryCaptionDirective {
208
+ templateRef = inject(TemplateRef);
209
+ forQuery = input('', { ...(ngDevMode ? { debugName: "forQuery" } : /* istanbul ignore next */ {}), alias: 'sparkQueryCaption' });
210
+ static ngTemplateContextGuard(_dir, ctx) {
211
+ return true;
212
+ }
213
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryCaptionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
214
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.0", type: SparkQueryCaptionDirective, isStandalone: true, selector: "[sparkQueryCaption]", inputs: { forQuery: { classPropertyName: "forQuery", publicName: "sparkQueryCaption", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
215
+ }
216
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryCaptionDirective, decorators: [{
217
+ type: Directive,
218
+ args: [{ selector: '[sparkQueryCaption]' }]
219
+ }], propDecorators: { forQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkQueryCaption", required: false }] }] } });
220
+ /**
221
+ * The buttons at the header's trailing edge. Defaults to the server-declared custom actions.
222
+ *
223
+ * The context carries those actions and the current selection, so a host that wants its own
224
+ * buttons ALONGSIDE the server's can render both rather than choosing. Without that, overriding
225
+ * this slot to add one button would silently drop every action the type declares — and the
226
+ * server's actions are the ones carrying `selectionRule` and the permission filter.
227
+ *
228
+ * ```html
229
+ * <ng-container *sparkQueryActions="let actions; selection as rows">
230
+ * <button (click)="export(rows)">Export</button>
231
+ * </ng-container>
232
+ * ```
233
+ */
234
+ class SparkQueryActionsDirective {
235
+ templateRef = inject(TemplateRef);
236
+ forQuery = input('', { ...(ngDevMode ? { debugName: "forQuery" } : /* istanbul ignore next */ {}), alias: 'sparkQueryActions' });
237
+ static ngTemplateContextGuard(_dir, ctx) {
238
+ return true;
239
+ }
240
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryActionsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
241
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.0", type: SparkQueryActionsDirective, isStandalone: true, selector: "[sparkQueryActions]", inputs: { forQuery: { classPropertyName: "forQuery", publicName: "sparkQueryActions", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
242
+ }
243
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryActionsDirective, decorators: [{
244
+ type: Directive,
245
+ args: [{ selector: '[sparkQueryActions]' }]
246
+ }], propDecorators: { forQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkQueryActions", required: false }] }] } });
247
+ /** Context for the icon slot: the query whose card is being rendered. */
248
+ class SparkQuerySlotContext {
249
+ $implicit = null;
250
+ }
251
+ class SparkQueryCaptionContext {
252
+ /** The caption the card would have rendered: description, or the query name. */
253
+ $implicit = '';
254
+ query = null;
255
+ }
256
+ class SparkQueryActionsContext {
257
+ /** The custom actions the card would have rendered, already filtered for this query. */
258
+ $implicit = [];
259
+ /** Rows currently ticked. Empty unless an action is selection-gated. */
260
+ selection = [];
261
+ query = null;
262
+ }
263
+
264
+ /**
265
+ * One cell of a Spark table, wherever that table is.
266
+ *
267
+ * ## Why this exists
268
+ *
269
+ * The cell markup was written out three times: twice in the query grids (since merged into
270
+ * {@link SparkQueryGridComponent}) and once more in the AsDetail table on the PO detail page. The
271
+ * third copy was never counted, and it had already drifted — a `boolean` column rendered the text
272
+ * `"true"` there while the grid rendered a checkbox, a `color` rendered its hex string rather than
273
+ * a swatch, and a custom renderer was dispatched by a second, hand-copied lookup.
274
+ *
275
+ * ## What it does and does not own
276
+ *
277
+ * It owns **presentation**: which control a `dataType` becomes, and dispatching a declared custom
278
+ * renderer. It does not own **value resolution**, and deliberately so — the two callers read from
279
+ * genuinely different row models. A query row is a `PersistentObject` with an attribute list and
280
+ * an id; an AsDetail row is a plain dictionary of embedded values with neither. Trying to unify
281
+ * that would put a second row-access path inside this component and buy nothing.
282
+ *
283
+ * So the caller resolves the value with whichever pipe fits its row model, and passes the result.
284
+ *
285
+ * ## The two links are not the same link, and only one is here
286
+ *
287
+ * `link` is for a cell whose own value points at another entity — an AsDetail reference column.
288
+ * The query grid's first-column link is a different rule (it points at the row's own detail page,
289
+ * gated on `Read`), and it stays in the grid, wrapping this component. Passing both would nest
290
+ * anchors, which is invalid HTML; the grid therefore leaves `link` unset.
291
+ */
292
+ class SparkGridCellComponent {
293
+ renderers = inject(SparkGridRenderers);
294
+ /** The column being rendered. Its `dataType` and `renderer` decide everything below. */
295
+ column = input.required(/* @ts-ignore */
296
+ ...(ngDevMode ? [{ debugName: "column" }] : /* istanbul ignore next */ []));
297
+ /**
298
+ * What the caller's value pipe produced: the display text for most columns, and the raw value
299
+ * for `boolean` and `color`, which need the value rather than a rendering of it.
300
+ */
301
+ display = input(null, /* @ts-ignore */
302
+ ...(ngDevMode ? [{ debugName: "display" }] : /* istanbul ignore next */ []));
303
+ /**
304
+ * The value handed to a declared custom renderer.
305
+ *
306
+ * Separate from {@link display} because they are not the same thing: a renderer receives the
307
+ * underlying value — for AsDetail, the nested object itself — while `display` has already been
308
+ * flattened to something printable.
309
+ */
310
+ rendererValue = input(null, /* @ts-ignore */
311
+ ...(ngDevMode ? [{ debugName: "rendererValue" }] : /* istanbul ignore next */ []));
312
+ /** The row, passed through to a custom renderer as `item`. */
313
+ item = input(null, /* @ts-ignore */
314
+ ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
315
+ /**
316
+ * Pre-resolved chips for a reference-array column, or empty when the column is not one.
317
+ *
318
+ * Resolved by the caller because the label source differs: a query row carries a per-id
319
+ * breadcrumb map, while an AsDetail row's `__sparkBreadcrumbs` is keyed by column name only and
320
+ * so cannot label the members of an array. Rather than render a row of raw ids, an AsDetail
321
+ * caller passes nothing and the cell falls through to text.
322
+ */
323
+ chips = input([], /* @ts-ignore */
324
+ ...(ngDevMode ? [{ debugName: "chips" }] : /* istanbul ignore next */ []));
325
+ /** Route for a cell whose value references another entity. See the class comment. */
326
+ link = input(null, /* @ts-ignore */
327
+ ...(ngDevMode ? [{ debugName: "link" }] : /* istanbul ignore next */ []));
328
+ rendererComponent = computed(() => this.renderers.columnComponentFor(this.column()), /* @ts-ignore */
329
+ ...(ngDevMode ? [{ debugName: "rendererComponent" }] : /* istanbul ignore next */ []));
330
+ rendererInputs = computed(() => {
331
+ const component = this.rendererComponent();
332
+ return component
333
+ ? this.renderers.cellInputsFor(component, this.rendererValue(), this.column(), this.item())
334
+ : {};
335
+ }, /* @ts-ignore */
336
+ ...(ngDevMode ? [{ debugName: "rendererInputs" }] : /* istanbul ignore next */ []));
337
+ /** Null and undefined are distinct from `false` here — see the template. */
338
+ isUnsetBoolean = computed(() => this.display() == null, /* @ts-ignore */
339
+ ...(ngDevMode ? [{ debugName: "isUnsetBoolean" }] : /* istanbul ignore next */ []));
340
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkGridCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
341
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", 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 === 'color') {\n @if (display(); as colorVal) {\n <!-- Inline on purpose, and it is not a style-guide slip. In a query grid this cell is\n rendered INSIDE <mp-datatable>'s shadow root, which does not see document\n stylesheets: neither this component's scoped rules nor Bootstrap utilities apply\n there. Inline styles do. Measured 2026-08-23 -- see the comment in the .scss. -->\n <span class=\"d-inline-block align-middle border rounded\"\n [style.background-color]=\"colorVal\"\n style=\"width: 1.5em; height: 1.5em;\"></span>\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}\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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
342
+ }
343
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkGridCellComponent, decorators: [{
344
+ type: Component,
345
+ 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 === 'color') {\n @if (display(); as colorVal) {\n <!-- Inline on purpose, and it is not a style-guide slip. In a query grid this cell is\n rendered INSIDE <mp-datatable>'s shadow root, which does not see document\n stylesheets: neither this component's scoped rules nor Bootstrap utilities apply\n there. Inline styles do. Measured 2026-08-23 -- see the comment in the .scss. -->\n <span class=\"d-inline-block align-middle border rounded\"\n [style.background-color]=\"colorVal\"\n style=\"width: 1.5em; height: 1.5em;\"></span>\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}\n"] }]
346
+ }], 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 }] }] } });
347
+
348
+ /**
349
+ * The one Spark grid: a `<bs-datatable>` rendering a query or a sub-query.
350
+ *
351
+ * This replaces two components that were the same grid written twice — and, counting
352
+ * `spark-query-list`'s streaming and paged branches, the `<bs-datatable>` element itself written
353
+ * three times. The duplication had already produced four user-visible bugs, each fixed on one
354
+ * side and not the other: `[indeterminate]` on null booleans, permission state surviving a failed
355
+ * reload, a swallowed fetch failure, and virtual-scroll sizing. `SparkGridRenderers` was
356
+ * extracted to stop that and could only take the stateless helpers with it; this takes the rest.
357
+ *
358
+ * ## What it deliberately does NOT own: streaming
359
+ *
360
+ * `spark-grid-renderers.ts` records why — a websocket dependency graph must not reach the bundle
361
+ * of every PO detail page, none of which stream. So rows can come from **either** side:
362
+ *
363
+ * - unbound `data` — the grid fetches for itself, paging and sorting server-side;
364
+ * - bound `data` — the grid renders what it is given and never fetches.
365
+ *
366
+ * `spark-query-list` keeps the socket and feeds its snapshot in. This is the line `bs-datatable`
367
+ * itself draws: `[data]` and `[fetch]` are mutually exclusive, and binding both makes `[fetch]`
368
+ * win silently.
369
+ *
370
+ * ## Reading its state
371
+ *
372
+ * `query`, `entityType`, `customActions`, `selection`, `canRead`, `canCreate` and `resultCount`
373
+ * are readable so a host can render chrome around the grid. Read them through a template
374
+ * reference variable — `<spark-query-grid #grid>` … `grid.query()` — not `viewChild`: hosts wrap
375
+ * grids in `@if`, where a `viewChild` is intermittently undefined.
376
+ */
377
+ class SparkQueryGridComponent {
378
+ sparkService = inject(SparkService);
379
+ gridRenderers = inject(SparkGridRenderers);
380
+ queryRefresh = inject(SparkQueryRefreshService);
381
+ lang = inject(SparkLanguageService);
382
+ /** Query alias or id. */
383
+ queryId = input.required(/* @ts-ignore */
384
+ ...(ngDevMode ? [{ debugName: "queryId" }] : /* istanbul ignore next */ []));
385
+ /**
386
+ * The parent persistent object this query is scoped to, when it has one.
387
+ *
388
+ * Optional, because not every query is a detail of something: a page can host a grid that
389
+ * stands on its own — "my accounts", a dashboard list — and the server already treats an absent
390
+ * parent as "no parent" rather than as an error. Requiring these made that shape impossible to
391
+ * express: the component simply never loaded, with no request, no error and no log.
392
+ *
393
+ * Pass both or neither. One without the other is ignored, matching `SparkService.executeQuery`,
394
+ * which omits either param when it is falsy, and the execute endpoint, which resolves a parent
395
+ * only when both are present.
396
+ */
397
+ parentId = input('', /* @ts-ignore */
398
+ ...(ngDevMode ? [{ debugName: "parentId" }] : /* istanbul ignore next */ []));
399
+ parentType = input('', /* @ts-ignore */
400
+ ...(ngDevMode ? [{ debugName: "parentType" }] : /* istanbul ignore next */ []));
401
+ /**
402
+ * Rows supplied from outside. When bound, the grid renders these and runs no fetch.
403
+ *
404
+ * This is how streaming stays out of this component (see the class comment). `null` — the
405
+ * default — means "fetch for yourself"; an empty array means "you have been given no rows",
406
+ * which is a different statement and renders an empty grid rather than triggering a load.
407
+ */
408
+ data = input(null, /* @ts-ignore */
409
+ ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
410
+ /**
411
+ * Server-side search term, passed through to the query.
412
+ *
413
+ * An input rather than a box, so the host owns the control and the grid owns the request. A
414
+ * host with client-side rows filters them itself before binding `data`.
415
+ */
416
+ search = input('', /* @ts-ignore */
417
+ ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
418
+ /**
419
+ * Change this to re-run the query. Any value works; only its identity matters.
420
+ *
421
+ * A declarative token rather than only a `reload()` method, because calling a method means
422
+ * holding a component handle, and hosts wrap this grid in `@if`, where a `viewChild` is
423
+ * intermittently undefined.
424
+ *
425
+ * This drives the CHEAP refresh (see {@link reload}). It deliberately does not feed the main
426
+ * effect: re-running `loadData` would re-resolve the query, the entity types, the permissions
427
+ * and the lookups, and reset the user's page and sort on every button press.
428
+ */
429
+ reloadToken = input(null, /* @ts-ignore */
430
+ ...(ngDevMode ? [{ debugName: "reloadToken" }] : /* istanbul ignore next */ []));
431
+ /**
432
+ * Paging and sorting. Two-way, so a host driving `data` can sort those rows by whatever the
433
+ * user actually clicked — the datatable writes the new sort back through here.
434
+ */
435
+ settings = model(new DatatableSettings({
436
+ perPage: { values: SPARK_GRID_PAGE_SIZES, selected: SPARK_GRID_PAGE_SIZES[0] },
437
+ page: { values: [1], selected: 1 },
438
+ sortColumns: [],
439
+ }), /* @ts-ignore */
440
+ ...(ngDevMode ? [{ debugName: "settings" }] : /* istanbul ignore next */ []));
441
+ /** Rows the user has ticked. Two-way so chrome outside the grid can read and clear them. */
442
+ selection = model([], /* @ts-ignore */
443
+ ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
444
+ /** Emitted whenever a load or a page fetch fails, for a host in bespoke chrome. */
445
+ error = output();
446
+ rowClicked = output();
447
+ customActionExecuted = output();
448
+ colors = Color;
449
+ query = signal(null, /* @ts-ignore */
450
+ ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
451
+ entityType = signal(null, /* @ts-ignore */
452
+ ...(ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []));
453
+ allEntityTypes = signal([], /* @ts-ignore */
454
+ ...(ngDevMode ? [{ debugName: "allEntityTypes" }] : /* istanbul ignore next */ []));
455
+ lookupReferenceOptions = signal({}, /* @ts-ignore */
456
+ ...(ngDevMode ? [{ debugName: "lookupReferenceOptions" }] : /* istanbul ignore next */ []));
457
+ loading = signal(true, /* @ts-ignore */
458
+ ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
459
+ canRead = signal(false, /* @ts-ignore */
460
+ ...(ngDevMode ? [{ debugName: "canRead" }] : /* istanbul ignore next */ []));
461
+ canCreate = signal(false, /* @ts-ignore */
462
+ ...(ngDevMode ? [{ debugName: "canCreate" }] : /* istanbul ignore next */ []));
463
+ resultCount = signal(null, /* @ts-ignore */
464
+ ...(ngDevMode ? [{ debugName: "resultCount" }] : /* istanbul ignore next */ []));
465
+ customActions = signal([], /* @ts-ignore */
466
+ ...(ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []));
467
+ fetchFn = signal(null, /* @ts-ignore */
468
+ ...(ngDevMode ? [{ debugName: "fetchFn" }] : /* istanbul ignore next */ []));
469
+ /**
470
+ * Why the component renders its own failure instead of only reporting one.
471
+ *
472
+ * `SparkService` is a bare `firstValueFrom` passthrough with no interceptor, so every failure
473
+ * surfaces here and nowhere else. A host embedding this grid cannot surface what it never sees,
474
+ * and the default has to be visible with no host cooperation — hence a rendered alert, not just
475
+ * an output.
476
+ */
477
+ errorMessage = signal(null, /* @ts-ignore */
478
+ ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
479
+ /** 'none' unless an action is selection-gated, so unaffected grids gain no checkbox column. */
480
+ selectionMode = computed(() => selectionModeFor(this.customActions()), /* @ts-ignore */
481
+ ...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
482
+ isVirtualScrolling = computed(() => isVirtualScrollingQuery(this.query()), /* @ts-ignore */
483
+ ...(ngDevMode ? [{ debugName: "isVirtualScrolling" }] : /* istanbul ignore next */ []));
484
+ visibleAttributes = computed(() => visibleGridAttributes(this.entityType()), /* @ts-ignore */
485
+ ...(ngDevMode ? [{ debugName: "visibleAttributes" }] : /* istanbul ignore next */ []));
486
+ /**
487
+ * True when rows do not come from this component's own fetch.
488
+ *
489
+ * Either because the host bound `data`, or because the query streams — a streaming query's rows
490
+ * arrive over a socket and `/execute` is not a way to get them. The grid recognises the second
491
+ * case itself rather than waiting to be told, because it resolves the query before the host can
492
+ * see it: left to `data` alone, every streaming grid would fire one pointless fetch on mount,
493
+ * before the host had anything to bind.
494
+ */
495
+ hasExternalData = computed(() => this.data() !== null || this.query()?.isStreamingQuery === true, /* @ts-ignore */
496
+ ...(ngDevMode ? [{ debugName: "hasExternalData" }] : /* istanbul ignore next */ []));
497
+ /** Whether an action's selection rule is satisfied right now. The server checks it again. */
498
+ isActionEnabled(action) {
499
+ return parseSelectionRule(action.selectionRule)(this.selection().length);
500
+ }
501
+ constructor() {
502
+ effect(() => {
503
+ const qId = this.queryId();
504
+ const pId = this.parentId();
505
+ const pType = this.parentType();
506
+ // Only the query id is required. Requiring a parent here is what made a standalone grid
507
+ // silently render nothing.
508
+ if (qId) {
509
+ this.loadData(qId, pId, pType);
510
+ }
511
+ else {
512
+ // No query id at all. `loading` starts true, so without this the component would spin
513
+ // forever instead of saying anything.
514
+ this.loading.set(false);
515
+ }
516
+ });
517
+ // Separate effect, so the token drives the cheap refresh and never the full metadata reload.
518
+ // `first` skips the initial run: the effect above has already fetched, and reacting to the
519
+ // token's starting value would double-fetch on mount.
520
+ let first = true;
521
+ effect(() => {
522
+ // Both the host's token and the server's refreshQuery drive the same cheap refresh.
523
+ this.reloadToken();
524
+ this.queryRefresh.tokenFor(this.queryId());
525
+ if (first) {
526
+ first = false;
527
+ return;
528
+ }
529
+ untracked(() => this.reload());
530
+ });
531
+ // A new search term must refetch even when page, perPage and sort are unchanged — the
532
+ // datatable dedupes reloads by exactly that triple, and a new fetch identity is what resets
533
+ // its dedupe key. Skipping the first run keeps mount to a single request.
534
+ let firstSearch = true;
535
+ effect(() => {
536
+ this.search();
537
+ if (firstSearch) {
538
+ firstSearch = false;
539
+ return;
540
+ }
541
+ untracked(() => this.onSearchChanged());
542
+ });
543
+ }
544
+ /**
545
+ * Re-run the query, keeping the current page, sort and scroll position.
546
+ *
547
+ * Data-level on purpose: it re-seeds the fetch closure and nothing else. For a definition
548
+ * change — new columns, a renamed query — the inputs themselves must change; that is the
549
+ * expensive path. Inert when rows come from the host: there is nothing here to re-run.
550
+ */
551
+ reload() {
552
+ if (this.hasExternalData())
553
+ return;
554
+ const q = this.query();
555
+ if (q)
556
+ this.fetchFn.set(this.makeFetch(q, this.parentId(), this.parentType()));
557
+ }
558
+ onSearchChanged() {
559
+ if (this.hasExternalData())
560
+ return;
561
+ const s = this.settings();
562
+ // Back to page 1: the old page number means nothing against a different result set.
563
+ this.settings.set(new DatatableSettings({
564
+ perPage: { values: s.perPage.values, selected: s.perPage.selected },
565
+ page: { values: [1], selected: 1 },
566
+ sortColumns: s.sortColumns,
567
+ }));
568
+ this.reload();
569
+ }
570
+ async onCustomAction(action) {
571
+ if (action.confirmationMessageKey) {
572
+ const message = this.lang.t(action.confirmationMessageKey) || 'Are you sure?';
573
+ if (!confirm(message))
574
+ return;
575
+ }
576
+ try {
577
+ await this.sparkService.executeCustomAction(this.entityType().id, action.name, undefined, this.selection());
578
+ this.customActionExecuted.emit({ action });
579
+ if (action.refreshOnCompleted)
580
+ this.reload();
581
+ }
582
+ catch (e) {
583
+ const err = e;
584
+ this.errorMessage.set(err.error?.error || err.message || this.lang.t('common.actionFailed') || 'Action failed');
585
+ }
586
+ }
587
+ reportError(err) {
588
+ this.errorMessage.set(this.describe(err));
589
+ this.error.emit(err);
590
+ }
591
+ /**
592
+ * A 404 is deliberately generic.
593
+ *
594
+ * `Endpoints/Queries/Get.cs` answers 404 with byte-identical bodies for "no such query" and
595
+ * "you may not see it", so existence is not disclosed (security audit M-3). The component
596
+ * therefore genuinely cannot tell them apart, and both "Not found" and "Access denied" would be
597
+ * a guess — one of them leaking, the other misleading.
598
+ */
599
+ describe(err) {
600
+ if (err?.status === 404) {
601
+ return this.lang.t('spark.query.unavailable') || 'This list is not available.';
602
+ }
603
+ return err?.error?.error || err?.message
604
+ || this.lang.t('common.unexpectedError') || 'An unexpected error occurred';
605
+ }
606
+ async loadData(queryId, parentId, parentType) {
607
+ this.loading.set(true);
608
+ this.errorMessage.set(null);
609
+ this.fetchFn.set(null);
610
+ // Reset everything derived from the previous query, not just the fetch. Leaving
611
+ // `entityType`/`canRead` behind let a failed reload build a row link out of the PREVIOUS type
612
+ // and the previous permission.
613
+ this.query.set(null);
614
+ this.entityType.set(null);
615
+ this.canRead.set(false);
616
+ this.canCreate.set(false);
617
+ this.customActions.set([]);
618
+ this.resultCount.set(null);
619
+ // Ids from the previous query are meaningless against the next one, and would be POSTed as
620
+ // though they belonged to it.
621
+ this.selection.set([]);
622
+ try {
623
+ const [resolvedQuery, entityTypes] = await Promise.all([
624
+ this.sparkService.getQuery(queryId),
625
+ this.sparkService.getEntityTypes(),
626
+ ]);
627
+ this.query.set(resolvedQuery);
628
+ this.allEntityTypes.set(entityTypes);
629
+ const et = this.resolveEntityType(resolvedQuery, entityTypes);
630
+ this.entityType.set(et);
631
+ if (et) {
632
+ const [permissions, actions] = await Promise.all([
633
+ this.sparkService.getPermissions(et.id),
634
+ this.sparkService.getCustomActions(et.id),
635
+ ]);
636
+ this.canRead.set(permissions.canRead);
637
+ this.canCreate.set(permissions.canCreate);
638
+ // 'query', not 'list'. The server model has always documented "detail" | "query" |
639
+ // "both"; a filter testing for a value nothing emits renders the action NOWHERE.
640
+ this.customActions.set(filterQueryActions(actions));
641
+ }
642
+ this.settings.set(initialGridSettings(resolvedQuery));
643
+ // The datatable drives paging/sorting via [(settings)] and calls fetchFn per page. Virtual
644
+ // scrolling is just the [virtualScroll] template flag.
645
+ // `resolvedQuery`, not `hasExternalData()`: the query signal is set above, but reading the
646
+ // computed here would depend on signal-read ordering inside an async method. Ask the value
647
+ // directly.
648
+ if (this.data() === null && !resolvedQuery?.isStreamingQuery) {
649
+ this.fetchFn.set(this.makeFetch(resolvedQuery, parentId, parentType));
650
+ }
651
+ this.loadLookupReferenceOptions();
652
+ }
653
+ catch (e) {
654
+ this.fetchFn.set(null);
655
+ this.reportError(e);
656
+ }
657
+ finally {
658
+ this.loading.set(false);
659
+ }
660
+ }
661
+ /**
662
+ * The query's entity type.
663
+ *
664
+ * The source-name fallback is not optional garnish: a `Database.*` query need not declare
665
+ * `entityType`, and the sub-query grid — which matched on the declared name alone — rendered
666
+ * those as an empty card with no columns, no rows and no error, while the identical query
667
+ * rendered correctly as a page. Both grids are this one now, so there is one answer.
668
+ */
669
+ resolveEntityType(query, entityTypes) {
670
+ if (!query)
671
+ return null;
672
+ if (query.entityType) {
673
+ return entityTypes.find(t => t.name === query.entityType || t.alias === query.entityType?.toLowerCase()) ?? null;
674
+ }
675
+ const sourceName = extractSourceName(query.source);
676
+ const singular = singularize(sourceName);
677
+ return entityTypes.find(t => t.name === sourceName || t.name === singular || t.clrType.endsWith(singular)) ?? null;
678
+ }
679
+ makeFetch(query, parentId, parentType) {
680
+ return (req) => this.sparkService.executeQuery(query.id, {
681
+ sortColumns: req.sortColumns,
682
+ skip: (req.page - 1) * req.perPage,
683
+ take: req.perPage,
684
+ search: this.search() || undefined,
685
+ parentId, parentType,
686
+ }).then(r => {
687
+ this.errorMessage.set(null);
688
+ this.resultCount.set(r.totalRecords);
689
+ return {
690
+ data: r.data,
691
+ totalRecords: r.totalRecords,
692
+ totalPages: Math.ceil(r.totalRecords / req.perPage) || 1,
693
+ perPage: req.perPage,
694
+ page: req.page,
695
+ };
696
+ }).catch((e) => {
697
+ // Report before returning the empty page, or a failed fetch is indistinguishable from a
698
+ // query that legitimately has no rows.
699
+ this.reportError(e);
700
+ this.resultCount.set(0);
701
+ return { data: [], totalRecords: 0, totalPages: 1, perPage: req.perPage, page: req.page };
702
+ });
703
+ }
704
+ async loadLookupReferenceOptions() {
705
+ this.lookupReferenceOptions.set(await this.gridRenderers.loadLookupOptions(this.visibleAttributes()));
706
+ }
707
+ /**
708
+ * The underlying value a custom renderer receives, which is not the printable one.
709
+ *
710
+ * `rendererValue` unwraps an AsDetail attribute to the nested object (or objects) rather than
711
+ * flattening it to text — a renderer for a nested type needs the object, not a summary of it.
712
+ */
713
+ rendererValueFor(item, attr) {
714
+ return rendererValue(item.attributes.find(a => a.name === attr.name));
715
+ }
716
+ getColumnRendererComponent(attr) {
717
+ return this.gridRenderers.columnComponentFor(attr);
718
+ }
719
+ getColumnRendererInputs(component, item, attr) {
720
+ return this.gridRenderers.columnInputsFor(component, item, attr);
721
+ }
722
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
723
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", 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 }, 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 } }, 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 [(settings)]=\"settings\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (attr of visibleAttributes(); track attr.id; 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 @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, row.id]\"\n (click)=\"rowClicked.emit(row)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></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-attr=\"attr\">\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]=\"attr\"\n [display]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes())\"\n [rendererValue]=\"rendererValueFor(item, attr)\"\n [item]=\"item\"\n [chips]=\"(attr.name | referenceChips: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", "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"] }, { kind: "directive", type: BsDatatableColumnDirective, selector: "[bsDatatableColumn]", inputs: ["bsDatatableColumn", "bsDatatableColumnSortable"] }, { 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: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: AttributeValuePipe, name: "attributeValue" }, { kind: "pipe", type: ReferenceChipsPipe, name: "referenceChips" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
724
+ }
725
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryGridComponent, decorators: [{
726
+ type: Component,
727
+ args: [{ selector: 'spark-query-grid', imports: [CommonModule, RouterModule, BsAlertComponent, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective, BsSpinnerComponent, SparkGridCellComponent, ResolveTranslationPipe, AttributeValuePipe, ReferenceChipsPipe, TranslateKeyPipe], 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 [(settings)]=\"settings\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (attr of visibleAttributes(); track attr.id; 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 @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, row.id]\"\n (click)=\"rowClicked.emit(row)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></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-attr=\"attr\">\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]=\"attr\"\n [display]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes())\"\n [rendererValue]=\"rendererValueFor(item, attr)\"\n [item]=\"item\"\n [chips]=\"(attr.name | referenceChips:item)\" />\n</ng-template>\n" }]
728
+ }], 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 }] }], 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"] }], error: [{ type: i0.Output, args: ["error"] }], rowClicked: [{ type: i0.Output, args: ["rowClicked"] }], customActionExecuted: [{ type: i0.Output, args: ["customActionExecuted"] }] } });
729
+ function extractSourceName(source) {
730
+ const dotIndex = source.indexOf('.');
731
+ return dotIndex >= 0 ? source.substring(dotIndex + 1) : source;
732
+ }
733
+ /**
734
+ * Enough English to map a query source onto a type name — `Cars` to `Car`, `People` to `Person`.
735
+ *
736
+ * Deliberately a small table plus three suffix rules rather than a library: it runs against type
737
+ * names the application itself chose, and a wrong guess costs nothing, since the caller falls
738
+ * through to matching the plural and the CLR type name.
739
+ */
740
+ function singularize(plural) {
741
+ const irregulars = {
742
+ 'People': 'Person',
743
+ 'Children': 'Child',
744
+ 'Men': 'Man',
745
+ 'Women': 'Woman',
746
+ };
747
+ if (irregulars[plural])
748
+ return irregulars[plural];
749
+ if (plural.endsWith('ies'))
750
+ return plural.slice(0, -3) + 'y';
751
+ if (plural.endsWith('es'))
752
+ return plural.slice(0, -2);
753
+ if (plural.endsWith('s'))
754
+ return plural.slice(0, -1);
755
+ return plural;
756
+ }
757
+
758
+ /**
759
+ * A `<bs-card>` around a {@link SparkQueryGridComponent}: icon, caption, actions, grid.
760
+ *
761
+ * ## Chrome is overridden, not replaced
762
+ *
763
+ * Every header region has a default, and a host replaces only the ones it supplies. That is what
764
+ * lets this component serve the auto-rendered case: a sub-query is rendered once per entry in
765
+ * `EntityTypeDefinition.Queries` with nobody projecting into it, and it must look right with no
766
+ * host cooperation at all. A slot mechanism that only ever *replaced* chrome would leave that
767
+ * call site blank — which is why an earlier design had the query carry its own chrome from the
768
+ * server. Overriding a default needs no host; replacing one does.
769
+ *
770
+ * ## Two ways in, one set of templates
771
+ *
772
+ * Hand-written markup uses the structural directives and is found with `contentChildren`.
773
+ * The auto-rendered path cannot: `spark-po-detail` is created by the router, so in a default app
774
+ * there is no tag to project into. It therefore forwards `TemplateRef`s as inputs instead — a
775
+ * directive cannot cross a component boundary but its template can, and that forwarding is
776
+ * already the idiom in `spark-po-detail` (`extraActionsTemplate`, `extraContentTemplate`).
777
+ *
778
+ * Content wins over a forwarded input: the closer declaration is the more specific one.
779
+ */
780
+ class SparkQueryCardComponent {
781
+ lang = inject(SparkLanguageService);
782
+ queryId = input.required(/* @ts-ignore */
783
+ ...(ngDevMode ? [{ debugName: "queryId" }] : /* istanbul ignore next */ []));
784
+ parentId = input('', /* @ts-ignore */
785
+ ...(ngDevMode ? [{ debugName: "parentId" }] : /* istanbul ignore next */ []));
786
+ parentType = input('', /* @ts-ignore */
787
+ ...(ngDevMode ? [{ debugName: "parentType" }] : /* istanbul ignore next */ []));
788
+ reloadToken = input(null, /* @ts-ignore */
789
+ ...(ngDevMode ? [{ debugName: "reloadToken" }] : /* istanbul ignore next */ []));
790
+ data = input(null, /* @ts-ignore */
791
+ ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
792
+ search = input('', /* @ts-ignore */
793
+ ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
794
+ /**
795
+ * Slots forwarded from a host that cannot project content — see the class comment. A card
796
+ * reached through hand-written markup should use the structural directives instead.
797
+ */
798
+ iconTemplate = input(null, /* @ts-ignore */
799
+ ...(ngDevMode ? [{ debugName: "iconTemplate" }] : /* istanbul ignore next */ []));
800
+ captionTemplate = input(null, /* @ts-ignore */
801
+ ...(ngDevMode ? [{ debugName: "captionTemplate" }] : /* istanbul ignore next */ []));
802
+ actionsTemplate = input(null, /* @ts-ignore */
803
+ ...(ngDevMode ? [{ debugName: "actionsTemplate" }] : /* istanbul ignore next */ []));
804
+ error = output();
805
+ rowClicked = output();
806
+ customActionExecuted = output();
807
+ colors = Color;
808
+ iconSlots = contentChildren(SparkQueryIconDirective, /* @ts-ignore */
809
+ ...(ngDevMode ? [{ debugName: "iconSlots" }] : /* istanbul ignore next */ []));
810
+ captionSlots = contentChildren(SparkQueryCaptionDirective, /* @ts-ignore */
811
+ ...(ngDevMode ? [{ debugName: "captionSlots" }] : /* istanbul ignore next */ []));
812
+ actionSlots = contentChildren(SparkQueryActionsDirective, /* @ts-ignore */
813
+ ...(ngDevMode ? [{ debugName: "actionSlots" }] : /* istanbul ignore next */ []));
814
+ /**
815
+ * The grid, read only to surface its state — the query, the actions, the selection — to this
816
+ * component's own header.
817
+ *
818
+ * Optional rather than `required` on purpose. The header renders ABOVE the grid in this
819
+ * template, so on the first change-detection pass the view query has not resolved yet and
820
+ * `viewChild.required()` would throw where a plain one returns `undefined`. Everything below
821
+ * therefore reads it defensively; the signal updates once the view initialises and the header
822
+ * re-renders with the real query.
823
+ *
824
+ * A host reads grid state through a template reference variable instead, because a host may put
825
+ * the grid behind an `@if`, where a view query genuinely is intermittently undefined.
826
+ */
827
+ grid = viewChild(SparkQueryGridComponent, /* @ts-ignore */
828
+ ...(ngDevMode ? [{ debugName: "grid" }] : /* istanbul ignore next */ []));
829
+ query = computed(() => this.grid()?.query() ?? null, /* @ts-ignore */
830
+ ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
831
+ iconTpl = computed(() => slotFor(this.iconSlots(), this.query())?.templateRef ?? this.iconTemplate(), /* @ts-ignore */
832
+ ...(ngDevMode ? [{ debugName: "iconTpl" }] : /* istanbul ignore next */ []));
833
+ captionTpl = computed(() => slotFor(this.captionSlots(), this.query())?.templateRef ?? this.captionTemplate(), /* @ts-ignore */
834
+ ...(ngDevMode ? [{ debugName: "captionTpl" }] : /* istanbul ignore next */ []));
835
+ actionsTpl = computed(() => slotFor(this.actionSlots(), this.query())?.templateRef ?? this.actionsTemplate(), /* @ts-ignore */
836
+ ...(ngDevMode ? [{ debugName: "actionsTpl" }] : /* istanbul ignore next */ []));
837
+ /** The caption the card renders unless a slot replaces it. */
838
+ caption = computed(() => {
839
+ const q = this.query();
840
+ return (q?.description ? this.lang.resolve(q.description) : '') || q?.name || '';
841
+ }, /* @ts-ignore */
842
+ ...(ngDevMode ? [{ debugName: "caption" }] : /* istanbul ignore next */ []));
843
+ customActions = computed(() => this.grid()?.customActions() ?? [], /* @ts-ignore */
844
+ ...(ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []));
845
+ selection = computed(() => this.grid()?.selection() ?? [], /* @ts-ignore */
846
+ ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
847
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
848
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", 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 } }, 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 <span class=\"me-auto\">\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(), 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 </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 (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", "reloadToken", "settings", "selection"], outputs: ["settingsChange", "selectionChange", "error", "rowClicked", "customActionExecuted"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
849
+ }
850
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryCardComponent, decorators: [{
851
+ type: Component,
852
+ 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 <span class=\"me-auto\">\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(), 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 </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 (error)=\"error.emit($event)\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n </div>\n</bs-card>\n" }]
853
+ }], 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"] }], 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 }] }] } });
854
+
106
855
  /**
107
856
  * Generated bundle index. Do not edit.
108
857
  */
109
858
 
110
- export { SPARK_GRID_PAGE_SIZES, SparkGridRenderers, initialGridSettings, isVirtualScrollingQuery, visibleGridAttributes };
859
+ export { SPARK_GRID_PAGE_SIZES, SparkGridCellComponent, SparkGridRenderers, SparkQueryActionsContext, SparkQueryActionsDirective, SparkQueryCaptionContext, SparkQueryCaptionDirective, SparkQueryCardComponent, SparkQueryGridComponent, SparkQueryIconDirective, SparkQuerySlotContext, initialGridSettings, isVirtualScrollingQuery, slotFor, visibleGridAttributes };
111
860
  //# sourceMappingURL=mintplayer-ng-spark-grid.mjs.map