@mintplayer/ng-spark 22.3.0 → 22.4.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.
@@ -1,7 +1,12 @@
1
- import { DatatableSettings } from '@mintplayer/ng-bootstrap/datatable';
2
- import { SparkQuery, EntityType, EntityAttributeDefinition, PersistentObject, LookupReference } from '@mintplayer/ng-spark/models';
3
- import * as i0 from '@angular/core';
4
- import { Type } from '@angular/core';
1
+ import { DatatableSettings, BsDatatableFetch } from '@mintplayer/ng-bootstrap/datatable';
2
+ import * as _mintplayer_ng_spark_models from '@mintplayer/ng-spark/models';
3
+ import { SparkQuery, EntityType, EntityAttributeDefinition, PersistentObject, LookupReference, CustomActionDefinition } from '@mintplayer/ng-spark/models';
4
+ import * as _angular_core from '@angular/core';
5
+ import { Type, TemplateRef } from '@angular/core';
6
+ import { HttpErrorResponse } from '@angular/common/http';
7
+ import { Color } from '@mintplayer/ng-bootstrap';
8
+ import { SparkLanguageService } from '@mintplayer/ng-spark/services';
9
+ import { ReferenceChip } from '@mintplayer/ng-spark/pipes';
5
10
 
6
11
  /** Page sizes offered by every Spark grid. */
7
12
  declare const SPARK_GRID_PAGE_SIZES: number[];
@@ -24,20 +29,21 @@ declare function initialGridSettings(query: SparkQuery | null): DatatableSetting
24
29
  declare function isVirtualScrollingQuery(query: SparkQuery | null): boolean;
25
30
 
26
31
  /**
27
- * The parts of a Spark grid that both grid components need and that were, until this existed,
28
- * written out twice.
32
+ * Renderer lookup and lookup-reference loading for a Spark grid.
29
33
  *
30
- * `spark-query-list` and `spark-sub-query` had byte-identical copies of the renderer lookup, the
31
- * renderer input construction and the lookup-reference loading — around 120 lines between them.
32
- * That duplication is not a tidiness complaint: it is what produced the drift. The two copies
33
- * disagreed about `[indeterminate]`, about resetting permission state, about whether a fetch
34
- * failure surfaces or is swallowed, and about virtual-scroll sizing — four user-visible bugs, each
35
- * fixed on one side and not the other, because nothing made the two files move together.
34
+ * This was extracted when `spark-query-list` and `spark-sub-query` were two components holding
35
+ * byte-identical copies of it — around 120 lines between them. That duplication was not a
36
+ * tidiness complaint: it produced drift. The two copies disagreed about `[indeterminate]`, about
37
+ * resetting permission state, about whether a fetch failure surfaces or is swallowed, and about
38
+ * virtual-scroll sizing — four user-visible bugs, each fixed on one side and not the other.
36
39
  *
37
- * Kept deliberately small and stateless. The two components differ in real ways one is
38
- * route-coupled and carries streaming, search and a websocket dependency graph so merging them
39
- * into a single component would drag all of that into every detail page's bundle. Shared logic
40
- * belongs here; shared *state* does not.
40
+ * There is now one grid, {@link SparkQueryGridComponent}, so the drift cannot recur. This stays
41
+ * separate because it is stateless service-shaped logic rather than view state, and because it is
42
+ * what a custom grid would need in order to render Spark cells at all.
43
+ *
44
+ * The constraint that shaped the split still holds and is why the grid takes rows as an input
45
+ * rather than owning a socket: streaming's dependency graph must not reach the bundle of every
46
+ * detail page.
41
47
  */
42
48
  declare class SparkGridRenderers {
43
49
  private readonly registry;
@@ -50,14 +56,444 @@ declare class SparkGridRenderers {
50
56
  * member of the renderer contract be optional.
51
57
  */
52
58
  columnInputsFor(component: Type<any>, item: PersistentObject, attr: EntityAttributeDefinition): Record<string, any>;
59
+ /**
60
+ * The same contract for a row that is not a <see cref="PersistentObject"/>.
61
+ *
62
+ * An AsDetail row is a plain dictionary — embedded values with no id and no attribute list — so
63
+ * its value cannot be read the way {@link columnInputsFor} reads one. Only the extraction
64
+ * differs; the renderer contract is identical, and stating it once is what stops the two from
65
+ * drifting the way the cell markup already had.
66
+ */
67
+ cellInputsFor(component: Type<any>, value: unknown, column: EntityAttributeDefinition, item: unknown): Record<string, any>;
53
68
  /**
54
69
  * Loads every lookup reference the visible attributes need, in one pass.
55
70
  *
56
71
  * Returns an empty map rather than throwing when there are none, so callers never branch on it.
57
72
  */
58
73
  loadLookupOptions(attributes: EntityAttributeDefinition[]): Promise<Record<string, LookupReference>>;
59
- static ɵfac: i0.ɵɵFactoryDeclaration<SparkGridRenderers, never>;
60
- static ɵprov: i0.ɵɵInjectableDeclaration<SparkGridRenderers>;
74
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkGridRenderers, never>;
75
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<SparkGridRenderers>;
76
+ }
77
+
78
+ /**
79
+ * Header slots for `<spark-query-card>`.
80
+ *
81
+ * Each directive marks a template that REPLACES one region of the card header. Supplying one
82
+ * leaves the other two alone — that is the whole point of having three rather than the single
83
+ * whole-header template they replace, where changing the icon meant re-implementing the caption
84
+ * and the action bar as well.
85
+ *
86
+ * An omitted slot is not an empty slot: the card renders its default. This is what keeps the
87
+ * auto-rendered sub-query working. A sub-query is rendered from `EntityTypeDefinition.Queries`
88
+ * with no host to project into, so a mechanism that only ever REPLACED chrome would leave that
89
+ * call site with nothing — which is why an earlier design had the query declare its own chrome
90
+ * server-side. Overriding a default needs no host; replacing one does.
91
+ *
92
+ * The two ends of that are visible in the defaults themselves. Neither `SparkQuery` nor
93
+ * `EntityType` carries an icon, so the server has nothing to say and the slot is the only
94
+ * source. Actions are the mirror image: the server declares them per type, so the default is
95
+ * the real answer and the slot is a rare override.
96
+ *
97
+ * Naming follows ng-bootstrap's `*bsDatatableColumn` convention — prefix, component, slot —
98
+ * with `spark`, since these are ng-spark directives and `bs` is another package's prefix. These
99
+ * are the first attribute directives in ng-spark; every existing selector is an element.
100
+ *
101
+ * ## Targeting one query
102
+ *
103
+ * Each slot takes an optional query alias or id as its value. A detail page renders one card per
104
+ * entry in `EntityTypeDefinition.Queries`, so a host that supplies a bare slot would decorate
105
+ * every one of them identically — rarely what is wanted. A targeted slot applies to that query;
106
+ * an untargeted one is the fallback for the rest.
107
+ *
108
+ * ```html
109
+ * <span *sparkQueryIcon="'cars'"><spark-icon name="car-front" /></span>
110
+ * <span *sparkQueryIcon><spark-icon name="table" /></span>
111
+ * ```
112
+ *
113
+ * Matching is case-insensitive, against the query's alias and then its id. This is the
114
+ * `bsPriorityNavItem` shape: a value input aliased to the selector, collected with
115
+ * `contentChildren`.
116
+ */
117
+ /** Resolves a slot list to the one that applies to a query: targeted first, then untargeted. */
118
+ declare function slotFor<T extends {
119
+ forQuery: () => string;
120
+ }>(slots: readonly T[], query: SparkQuery | null): T | null;
121
+ /**
122
+ * The icon at the header's leading edge. No default: nothing in the model describes one.
123
+ *
124
+ * ```html
125
+ * <spark-query-card [queryId]="'cars'">
126
+ * <spark-icon *sparkQueryIcon name="car-front" />
127
+ * </spark-query-card>
128
+ * ```
129
+ */
130
+ declare class SparkQueryIconDirective {
131
+ readonly templateRef: TemplateRef<SparkQuerySlotContext>;
132
+ /** Query alias or id this slot applies to. Empty targets every query on the page. */
133
+ readonly forQuery: _angular_core.InputSignal<string>;
134
+ static ngTemplateContextGuard(_dir: SparkQueryIconDirective, ctx: unknown): ctx is SparkQuerySlotContext;
135
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkQueryIconDirective, never>;
136
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SparkQueryIconDirective, "[sparkQueryIcon]", never, { "forQuery": { "alias": "sparkQueryIcon"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
137
+ }
138
+ /**
139
+ * The header caption. Defaults to the query's translated description, falling back to its name.
140
+ *
141
+ * The context carries that resolved default as `$implicit`, so a host can decorate it — add a
142
+ * count, a badge — without re-resolving the translation itself.
143
+ */
144
+ declare class SparkQueryCaptionDirective {
145
+ readonly templateRef: TemplateRef<SparkQueryCaptionContext>;
146
+ readonly forQuery: _angular_core.InputSignal<string>;
147
+ static ngTemplateContextGuard(_dir: SparkQueryCaptionDirective, ctx: unknown): ctx is SparkQueryCaptionContext;
148
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkQueryCaptionDirective, never>;
149
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SparkQueryCaptionDirective, "[sparkQueryCaption]", never, { "forQuery": { "alias": "sparkQueryCaption"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
150
+ }
151
+ /**
152
+ * The buttons at the header's trailing edge. Defaults to the server-declared custom actions.
153
+ *
154
+ * The context carries those actions and the current selection, so a host that wants its own
155
+ * buttons ALONGSIDE the server's can render both rather than choosing. Without that, overriding
156
+ * this slot to add one button would silently drop every action the type declares — and the
157
+ * server's actions are the ones carrying `selectionRule` and the permission filter.
158
+ *
159
+ * ```html
160
+ * <ng-container *sparkQueryActions="let actions; selection as rows">
161
+ * <button (click)="export(rows)">Export</button>
162
+ * </ng-container>
163
+ * ```
164
+ */
165
+ declare class SparkQueryActionsDirective {
166
+ readonly templateRef: TemplateRef<SparkQueryActionsContext>;
167
+ readonly forQuery: _angular_core.InputSignal<string>;
168
+ static ngTemplateContextGuard(_dir: SparkQueryActionsDirective, ctx: unknown): ctx is SparkQueryActionsContext;
169
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkQueryActionsDirective, never>;
170
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SparkQueryActionsDirective, "[sparkQueryActions]", never, { "forQuery": { "alias": "sparkQueryActions"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
171
+ }
172
+ /** Context for the icon slot: the query whose card is being rendered. */
173
+ declare class SparkQuerySlotContext {
174
+ $implicit: SparkQuery | null;
175
+ }
176
+ declare class SparkQueryCaptionContext {
177
+ /** The caption the card would have rendered: description, or the query name. */
178
+ $implicit: string;
179
+ query: SparkQuery | null;
180
+ }
181
+ declare class SparkQueryActionsContext {
182
+ /** The custom actions the card would have rendered, already filtered for this query. */
183
+ $implicit: CustomActionDefinition[];
184
+ /** Rows currently ticked. Empty unless an action is selection-gated. */
185
+ selection: PersistentObject[];
186
+ query: SparkQuery | null;
187
+ }
188
+
189
+ /**
190
+ * The one Spark grid: a `<bs-datatable>` rendering a query or a sub-query.
191
+ *
192
+ * This replaces two components that were the same grid written twice — and, counting
193
+ * `spark-query-list`'s streaming and paged branches, the `<bs-datatable>` element itself written
194
+ * three times. The duplication had already produced four user-visible bugs, each fixed on one
195
+ * side and not the other: `[indeterminate]` on null booleans, permission state surviving a failed
196
+ * reload, a swallowed fetch failure, and virtual-scroll sizing. `SparkGridRenderers` was
197
+ * extracted to stop that and could only take the stateless helpers with it; this takes the rest.
198
+ *
199
+ * ## What it deliberately does NOT own: streaming
200
+ *
201
+ * `spark-grid-renderers.ts` records why — a websocket dependency graph must not reach the bundle
202
+ * of every PO detail page, none of which stream. So rows can come from **either** side:
203
+ *
204
+ * - unbound `data` — the grid fetches for itself, paging and sorting server-side;
205
+ * - bound `data` — the grid renders what it is given and never fetches.
206
+ *
207
+ * `spark-query-list` keeps the socket and feeds its snapshot in. This is the line `bs-datatable`
208
+ * itself draws: `[data]` and `[fetch]` are mutually exclusive, and binding both makes `[fetch]`
209
+ * win silently.
210
+ *
211
+ * ## Reading its state
212
+ *
213
+ * `query`, `entityType`, `customActions`, `selection`, `canRead`, `canCreate` and `resultCount`
214
+ * are readable so a host can render chrome around the grid. Read them through a template
215
+ * reference variable — `<spark-query-grid #grid>` … `grid.query()` — not `viewChild`: hosts wrap
216
+ * grids in `@if`, where a `viewChild` is intermittently undefined.
217
+ */
218
+ declare class SparkQueryGridComponent {
219
+ private readonly sparkService;
220
+ private readonly gridRenderers;
221
+ private readonly queryRefresh;
222
+ readonly lang: SparkLanguageService;
223
+ /** Query alias or id. */
224
+ queryId: _angular_core.InputSignal<string>;
225
+ /**
226
+ * The parent persistent object this query is scoped to, when it has one.
227
+ *
228
+ * Optional, because not every query is a detail of something: a page can host a grid that
229
+ * stands on its own — "my accounts", a dashboard list — and the server already treats an absent
230
+ * parent as "no parent" rather than as an error. Requiring these made that shape impossible to
231
+ * express: the component simply never loaded, with no request, no error and no log.
232
+ *
233
+ * Pass both or neither. One without the other is ignored, matching `SparkService.executeQuery`,
234
+ * which omits either param when it is falsy, and the execute endpoint, which resolves a parent
235
+ * only when both are present.
236
+ */
237
+ parentId: _angular_core.InputSignal<string>;
238
+ parentType: _angular_core.InputSignal<string>;
239
+ /**
240
+ * Rows supplied from outside. When bound, the grid renders these and runs no fetch.
241
+ *
242
+ * This is how streaming stays out of this component (see the class comment). `null` — the
243
+ * default — means "fetch for yourself"; an empty array means "you have been given no rows",
244
+ * which is a different statement and renders an empty grid rather than triggering a load.
245
+ */
246
+ data: _angular_core.InputSignal<PersistentObject[] | null>;
247
+ /**
248
+ * Server-side search term, passed through to the query.
249
+ *
250
+ * An input rather than a box, so the host owns the control and the grid owns the request. A
251
+ * host with client-side rows filters them itself before binding `data`.
252
+ */
253
+ search: _angular_core.InputSignal<string>;
254
+ /**
255
+ * Change this to re-run the query. Any value works; only its identity matters.
256
+ *
257
+ * A declarative token rather than only a `reload()` method, because calling a method means
258
+ * holding a component handle, and hosts wrap this grid in `@if`, where a `viewChild` is
259
+ * intermittently undefined.
260
+ *
261
+ * This drives the CHEAP refresh (see {@link reload}). It deliberately does not feed the main
262
+ * effect: re-running `loadData` would re-resolve the query, the entity types, the permissions
263
+ * and the lookups, and reset the user's page and sort on every button press.
264
+ */
265
+ reloadToken: _angular_core.InputSignal<unknown>;
266
+ /**
267
+ * Paging and sorting. Two-way, so a host driving `data` can sort those rows by whatever the
268
+ * user actually clicked — the datatable writes the new sort back through here.
269
+ */
270
+ settings: _angular_core.ModelSignal<DatatableSettings>;
271
+ /** Rows the user has ticked. Two-way so chrome outside the grid can read and clear them. */
272
+ selection: _angular_core.ModelSignal<PersistentObject[]>;
273
+ /** Emitted whenever a load or a page fetch fails, for a host in bespoke chrome. */
274
+ error: _angular_core.OutputEmitterRef<HttpErrorResponse>;
275
+ rowClicked: _angular_core.OutputEmitterRef<PersistentObject>;
276
+ customActionExecuted: _angular_core.OutputEmitterRef<{
277
+ action: CustomActionDefinition;
278
+ }>;
279
+ colors: typeof Color;
280
+ query: _angular_core.WritableSignal<SparkQuery | null>;
281
+ entityType: _angular_core.WritableSignal<EntityType | null>;
282
+ allEntityTypes: _angular_core.WritableSignal<EntityType[]>;
283
+ lookupReferenceOptions: _angular_core.WritableSignal<Record<string, LookupReference>>;
284
+ loading: _angular_core.WritableSignal<boolean>;
285
+ canRead: _angular_core.WritableSignal<boolean>;
286
+ canCreate: _angular_core.WritableSignal<boolean>;
287
+ resultCount: _angular_core.WritableSignal<number | null>;
288
+ customActions: _angular_core.WritableSignal<CustomActionDefinition[]>;
289
+ fetchFn: _angular_core.WritableSignal<BsDatatableFetch<PersistentObject> | null>;
290
+ /**
291
+ * Why the component renders its own failure instead of only reporting one.
292
+ *
293
+ * `SparkService` is a bare `firstValueFrom` passthrough with no interceptor, so every failure
294
+ * surfaces here and nowhere else. A host embedding this grid cannot surface what it never sees,
295
+ * and the default has to be visible with no host cooperation — hence a rendered alert, not just
296
+ * an output.
297
+ */
298
+ errorMessage: _angular_core.WritableSignal<string | null>;
299
+ /** 'none' unless an action is selection-gated, so unaffected grids gain no checkbox column. */
300
+ selectionMode: _angular_core.Signal<_mintplayer_ng_spark_models.SparkSelectionMode>;
301
+ isVirtualScrolling: _angular_core.Signal<boolean>;
302
+ visibleAttributes: _angular_core.Signal<EntityAttributeDefinition[]>;
303
+ /**
304
+ * True when rows do not come from this component's own fetch.
305
+ *
306
+ * Either because the host bound `data`, or because the query streams — a streaming query's rows
307
+ * arrive over a socket and `/execute` is not a way to get them. The grid recognises the second
308
+ * case itself rather than waiting to be told, because it resolves the query before the host can
309
+ * see it: left to `data` alone, every streaming grid would fire one pointless fetch on mount,
310
+ * before the host had anything to bind.
311
+ */
312
+ hasExternalData: _angular_core.Signal<boolean>;
313
+ /** Whether an action's selection rule is satisfied right now. The server checks it again. */
314
+ isActionEnabled(action: CustomActionDefinition): boolean;
315
+ constructor();
316
+ /**
317
+ * Re-run the query, keeping the current page, sort and scroll position.
318
+ *
319
+ * Data-level on purpose: it re-seeds the fetch closure and nothing else. For a definition
320
+ * change — new columns, a renamed query — the inputs themselves must change; that is the
321
+ * expensive path. Inert when rows come from the host: there is nothing here to re-run.
322
+ */
323
+ reload(): void;
324
+ private onSearchChanged;
325
+ onCustomAction(action: CustomActionDefinition): Promise<void>;
326
+ private reportError;
327
+ /**
328
+ * A 404 is deliberately generic.
329
+ *
330
+ * `Endpoints/Queries/Get.cs` answers 404 with byte-identical bodies for "no such query" and
331
+ * "you may not see it", so existence is not disclosed (security audit M-3). The component
332
+ * therefore genuinely cannot tell them apart, and both "Not found" and "Access denied" would be
333
+ * a guess — one of them leaking, the other misleading.
334
+ */
335
+ private describe;
336
+ private loadData;
337
+ /**
338
+ * The query's entity type.
339
+ *
340
+ * The source-name fallback is not optional garnish: a `Database.*` query need not declare
341
+ * `entityType`, and the sub-query grid — which matched on the declared name alone — rendered
342
+ * those as an empty card with no columns, no rows and no error, while the identical query
343
+ * rendered correctly as a page. Both grids are this one now, so there is one answer.
344
+ */
345
+ private resolveEntityType;
346
+ private makeFetch;
347
+ private loadLookupReferenceOptions;
348
+ /**
349
+ * The underlying value a custom renderer receives, which is not the printable one.
350
+ *
351
+ * `rendererValue` unwraps an AsDetail attribute to the nested object (or objects) rather than
352
+ * flattening it to text — a renderer for a nested type needs the object, not a summary of it.
353
+ */
354
+ rendererValueFor(item: PersistentObject, attr: EntityAttributeDefinition): unknown;
355
+ getColumnRendererComponent(attr: EntityAttributeDefinition): Type<any> | null;
356
+ getColumnRendererInputs(component: Type<any>, item: PersistentObject, attr: EntityAttributeDefinition): Record<string, any>;
357
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkQueryGridComponent, never>;
358
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SparkQueryGridComponent, "spark-query-grid", never, { "queryId": { "alias": "queryId"; "required": true; "isSignal": true; }; "parentId": { "alias": "parentId"; "required": false; "isSignal": true; }; "parentType": { "alias": "parentType"; "required": false; "isSignal": true; }; "data": { "alias": "data"; "required": false; "isSignal": true; }; "search": { "alias": "search"; "required": false; "isSignal": true; }; "reloadToken": { "alias": "reloadToken"; "required": false; "isSignal": true; }; "settings": { "alias": "settings"; "required": false; "isSignal": true; }; "selection": { "alias": "selection"; "required": false; "isSignal": true; }; }, { "settings": "settingsChange"; "selection": "selectionChange"; "error": "error"; "rowClicked": "rowClicked"; "customActionExecuted": "customActionExecuted"; }, never, never, true, never>;
359
+ }
360
+
361
+ /**
362
+ * A `<bs-card>` around a {@link SparkQueryGridComponent}: icon, caption, actions, grid.
363
+ *
364
+ * ## Chrome is overridden, not replaced
365
+ *
366
+ * Every header region has a default, and a host replaces only the ones it supplies. That is what
367
+ * lets this component serve the auto-rendered case: a sub-query is rendered once per entry in
368
+ * `EntityTypeDefinition.Queries` with nobody projecting into it, and it must look right with no
369
+ * host cooperation at all. A slot mechanism that only ever *replaced* chrome would leave that
370
+ * call site blank — which is why an earlier design had the query carry its own chrome from the
371
+ * server. Overriding a default needs no host; replacing one does.
372
+ *
373
+ * ## Two ways in, one set of templates
374
+ *
375
+ * Hand-written markup uses the structural directives and is found with `contentChildren`.
376
+ * The auto-rendered path cannot: `spark-po-detail` is created by the router, so in a default app
377
+ * there is no tag to project into. It therefore forwards `TemplateRef`s as inputs instead — a
378
+ * directive cannot cross a component boundary but its template can, and that forwarding is
379
+ * already the idiom in `spark-po-detail` (`extraActionsTemplate`, `extraContentTemplate`).
380
+ *
381
+ * Content wins over a forwarded input: the closer declaration is the more specific one.
382
+ */
383
+ declare class SparkQueryCardComponent {
384
+ readonly lang: SparkLanguageService;
385
+ queryId: _angular_core.InputSignal<string>;
386
+ parentId: _angular_core.InputSignal<string>;
387
+ parentType: _angular_core.InputSignal<string>;
388
+ reloadToken: _angular_core.InputSignal<unknown>;
389
+ data: _angular_core.InputSignal<PersistentObject[] | null>;
390
+ search: _angular_core.InputSignal<string>;
391
+ /**
392
+ * Slots forwarded from a host that cannot project content — see the class comment. A card
393
+ * reached through hand-written markup should use the structural directives instead.
394
+ */
395
+ iconTemplate: _angular_core.InputSignal<TemplateRef<any> | null>;
396
+ captionTemplate: _angular_core.InputSignal<TemplateRef<any> | null>;
397
+ actionsTemplate: _angular_core.InputSignal<TemplateRef<any> | null>;
398
+ error: _angular_core.OutputEmitterRef<HttpErrorResponse>;
399
+ rowClicked: _angular_core.OutputEmitterRef<PersistentObject>;
400
+ customActionExecuted: _angular_core.OutputEmitterRef<{
401
+ action: CustomActionDefinition;
402
+ }>;
403
+ colors: typeof Color;
404
+ private readonly iconSlots;
405
+ private readonly captionSlots;
406
+ private readonly actionSlots;
407
+ /**
408
+ * The grid, read only to surface its state — the query, the actions, the selection — to this
409
+ * component's own header.
410
+ *
411
+ * Optional rather than `required` on purpose. The header renders ABOVE the grid in this
412
+ * template, so on the first change-detection pass the view query has not resolved yet and
413
+ * `viewChild.required()` would throw where a plain one returns `undefined`. Everything below
414
+ * therefore reads it defensively; the signal updates once the view initialises and the header
415
+ * re-renders with the real query.
416
+ *
417
+ * A host reads grid state through a template reference variable instead, because a host may put
418
+ * the grid behind an `@if`, where a view query genuinely is intermittently undefined.
419
+ */
420
+ protected readonly grid: _angular_core.Signal<SparkQueryGridComponent | undefined>;
421
+ protected readonly query: _angular_core.Signal<_mintplayer_ng_spark_models.SparkQuery | null>;
422
+ protected readonly iconTpl: _angular_core.Signal<TemplateRef<any> | null>;
423
+ protected readonly captionTpl: _angular_core.Signal<TemplateRef<any> | null>;
424
+ protected readonly actionsTpl: _angular_core.Signal<TemplateRef<any> | null>;
425
+ /** The caption the card renders unless a slot replaces it. */
426
+ protected readonly caption: _angular_core.Signal<string>;
427
+ protected readonly customActions: _angular_core.Signal<CustomActionDefinition[]>;
428
+ protected readonly selection: _angular_core.Signal<PersistentObject[]>;
429
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkQueryCardComponent, never>;
430
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SparkQueryCardComponent, "spark-query-card", never, { "queryId": { "alias": "queryId"; "required": true; "isSignal": true; }; "parentId": { "alias": "parentId"; "required": false; "isSignal": true; }; "parentType": { "alias": "parentType"; "required": false; "isSignal": true; }; "reloadToken": { "alias": "reloadToken"; "required": false; "isSignal": true; }; "data": { "alias": "data"; "required": false; "isSignal": true; }; "search": { "alias": "search"; "required": false; "isSignal": true; }; "iconTemplate": { "alias": "iconTemplate"; "required": false; "isSignal": true; }; "captionTemplate": { "alias": "captionTemplate"; "required": false; "isSignal": true; }; "actionsTemplate": { "alias": "actionsTemplate"; "required": false; "isSignal": true; }; }, { "error": "error"; "rowClicked": "rowClicked"; "customActionExecuted": "customActionExecuted"; }, ["iconSlots", "captionSlots", "actionSlots"], never, true, never>;
431
+ }
432
+
433
+ /**
434
+ * One cell of a Spark table, wherever that table is.
435
+ *
436
+ * ## Why this exists
437
+ *
438
+ * The cell markup was written out three times: twice in the query grids (since merged into
439
+ * {@link SparkQueryGridComponent}) and once more in the AsDetail table on the PO detail page. The
440
+ * third copy was never counted, and it had already drifted — a `boolean` column rendered the text
441
+ * `"true"` there while the grid rendered a checkbox, a `color` rendered its hex string rather than
442
+ * a swatch, and a custom renderer was dispatched by a second, hand-copied lookup.
443
+ *
444
+ * ## What it does and does not own
445
+ *
446
+ * It owns **presentation**: which control a `dataType` becomes, and dispatching a declared custom
447
+ * renderer. It does not own **value resolution**, and deliberately so — the two callers read from
448
+ * genuinely different row models. A query row is a `PersistentObject` with an attribute list and
449
+ * an id; an AsDetail row is a plain dictionary of embedded values with neither. Trying to unify
450
+ * that would put a second row-access path inside this component and buy nothing.
451
+ *
452
+ * So the caller resolves the value with whichever pipe fits its row model, and passes the result.
453
+ *
454
+ * ## The two links are not the same link, and only one is here
455
+ *
456
+ * `link` is for a cell whose own value points at another entity — an AsDetail reference column.
457
+ * The query grid's first-column link is a different rule (it points at the row's own detail page,
458
+ * gated on `Read`), and it stays in the grid, wrapping this component. Passing both would nest
459
+ * anchors, which is invalid HTML; the grid therefore leaves `link` unset.
460
+ */
461
+ declare class SparkGridCellComponent {
462
+ private readonly renderers;
463
+ /** The column being rendered. Its `dataType` and `renderer` decide everything below. */
464
+ column: _angular_core.InputSignal<EntityAttributeDefinition>;
465
+ /**
466
+ * What the caller's value pipe produced: the display text for most columns, and the raw value
467
+ * for `boolean` and `color`, which need the value rather than a rendering of it.
468
+ */
469
+ display: _angular_core.InputSignal<unknown>;
470
+ /**
471
+ * The value handed to a declared custom renderer.
472
+ *
473
+ * Separate from {@link display} because they are not the same thing: a renderer receives the
474
+ * underlying value — for AsDetail, the nested object itself — while `display` has already been
475
+ * flattened to something printable.
476
+ */
477
+ rendererValue: _angular_core.InputSignal<unknown>;
478
+ /** The row, passed through to a custom renderer as `item`. */
479
+ item: _angular_core.InputSignal<unknown>;
480
+ /**
481
+ * Pre-resolved chips for a reference-array column, or empty when the column is not one.
482
+ *
483
+ * Resolved by the caller because the label source differs: a query row carries a per-id
484
+ * breadcrumb map, while an AsDetail row's `__sparkBreadcrumbs` is keyed by column name only and
485
+ * so cannot label the members of an array. Rather than render a row of raw ids, an AsDetail
486
+ * caller passes nothing and the cell falls through to text.
487
+ */
488
+ chips: _angular_core.InputSignal<ReferenceChip[]>;
489
+ /** Route for a cell whose value references another entity. See the class comment. */
490
+ link: _angular_core.InputSignal<unknown[] | null>;
491
+ protected readonly rendererComponent: _angular_core.Signal<_angular_core.Type<any> | null>;
492
+ protected readonly rendererInputs: _angular_core.Signal<Record<string, any>>;
493
+ /** Null and undefined are distinct from `false` here — see the template. */
494
+ protected readonly isUnsetBoolean: _angular_core.Signal<boolean>;
495
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkGridCellComponent, never>;
496
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SparkGridCellComponent, "spark-grid-cell", never, { "column": { "alias": "column"; "required": true; "isSignal": true; }; "display": { "alias": "display"; "required": false; "isSignal": true; }; "rendererValue": { "alias": "rendererValue"; "required": false; "isSignal": true; }; "item": { "alias": "item"; "required": false; "isSignal": true; }; "chips": { "alias": "chips"; "required": false; "isSignal": true; }; "link": { "alias": "link"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
61
497
  }
62
498
 
63
- export { SPARK_GRID_PAGE_SIZES, SparkGridRenderers, initialGridSettings, isVirtualScrollingQuery, visibleGridAttributes };
499
+ export { SPARK_GRID_PAGE_SIZES, SparkGridCellComponent, SparkGridRenderers, SparkQueryActionsContext, SparkQueryActionsDirective, SparkQueryCaptionContext, SparkQueryCaptionDirective, SparkQueryCardComponent, SparkQueryGridComponent, SparkQueryIconDirective, SparkQuerySlotContext, initialGridSettings, isVirtualScrollingQuery, slotFor, visibleGridAttributes };
@@ -335,7 +335,9 @@ type StreamingMessage = StreamingSnapshotMessage | StreamingPatchMessage | Strea
335
335
 
336
336
  /**
337
337
  * Resolves an `EntityType` by its CLR type name (e.g. `"HR.Entities.Address"`).
338
- * Callers typically close over `sparkService.getEntityTypes()`'s cached list.
338
+ * Callers typically close over a list they already hold. `getEntityTypes()` is NOT cached --
339
+ * it issues a request per call -- so resolve against a list you fetched once rather than
340
+ * calling it inside the resolver.
339
341
  */
340
342
  type EntityTypeResolver = (clrTypeName: string) => EntityType | undefined;
341
343
  /**
@@ -348,6 +350,28 @@ type EntityTypeResolver = (clrTypeName: string) => EntityType | undefined;
348
350
  * back to the flat dict the form components already handle.
349
351
  */
350
352
  declare function nestedPoToDict(po: PersistentObject | null | undefined): Record<string, any>;
353
+ /**
354
+ * Reserved key under which a flattened nested object keeps the breadcrumb the SERVER resolved for
355
+ * that object itself (as opposed to {@link AS_DETAIL_BREADCRUMBS_KEY}, which keys the breadcrumbs
356
+ * of its reference attributes by attribute name).
357
+ *
358
+ * This exists because a breadcrumb template can name a property the model does not carry. HR's
359
+ * `Address` declares `[Breadcrumb, IgnoreProperty] string Crumb`, and `Address.json` renders it as
360
+ * `"{Crumb}"` — the server resolves that by reflecting over the CLR property, which no client can
361
+ * do, because `[IgnoreProperty]` is exactly the instruction to keep it out of the model. Flattening
362
+ * used to discard the resolved string, leaving the form to substitute `{Crumb}` against a dict that
363
+ * can never contain it.
364
+ */
365
+ declare const AS_DETAIL_SELF_BREADCRUMB_KEY = "__sparkBreadcrumb";
366
+ /**
367
+ * The breadcrumb the server resolved for a flattened object, or null when it resolved to nothing.
368
+ *
369
+ * `EntityMapper` never emits an empty breadcrumb: when the template renders blank it substitutes
370
+ * the CLR type name, so an unset `Address` arrives as the literal string `"Address"`
371
+ * (EntityMapper.cs:209-211). That is a placeholder, not data, and rendering it would be worse than
372
+ * rendering nothing — it reads as a real value. `typeName` lets a caller filter it back out.
373
+ */
374
+ declare function selfBreadcrumb(row: Record<string, any> | null | undefined, typeName?: string): string | null;
351
375
  /**
352
376
  * Reserved key under which {@link nestedPoToDisplayRow} stashes the server-resolved breadcrumb of
353
377
  * each reference attribute (keyed by attribute name). Lets an AsDetail reference cell render the
@@ -422,5 +446,5 @@ type SparkSelectionMode = 'none' | 'single' | 'multiple';
422
446
  */
423
447
  declare function selectionModeFor(actions: CustomActionDefinition[]): SparkSelectionMode;
424
448
 
425
- export { AS_DETAIL_BREADCRUMBS_KEY, ELookupDisplayType, EReferenceDisplayType, ShowedOn, currentLanguage, dictToNestedPo, filterQueryActions, hasShowedOnFlag, nestedPoToDict, nestedPoToDisplayRow, parseSelectionRule, resolveTranslation, selectionModeFor };
449
+ export { AS_DETAIL_BREADCRUMBS_KEY, AS_DETAIL_SELF_BREADCRUMB_KEY, ELookupDisplayType, EReferenceDisplayType, ShowedOn, currentLanguage, dictToNestedPo, filterQueryActions, hasShowedOnFlag, nestedPoToDict, nestedPoToDisplayRow, parseSelectionRule, resolveTranslation, selectionModeFor, selfBreadcrumb };
426
450
  export type { AttributeGroup, AttributeTab, CustomActionDefinition, EntityAttributeDefinition, EntityPermissions, EntityType, EntityTypeResolver, LookupReference, LookupReferenceListItem, LookupReferenceValue, PersistentObject, PersistentObjectAttribute, PersistentObjectPermissions, ProgramUnit, ProgramUnitGroup, ProgramUnitsConfiguration, QueryResult, RetryActionPayload, RetryActionResult, SparkQuery, SparkQueryRenderMode, SparkQuerySortColumn, SparkSelectionMode, StreamingErrorMessage, StreamingMessage, StreamingPatchItem, StreamingPatchMessage, StreamingSnapshotMessage, TranslatedString, ValidationError, ValidationErrorResponse, ValidationRule };