@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
@@ -2,10 +2,7 @@ import * as _angular_core from '@angular/core';
2
2
  import { TemplateRef, Type } from '@angular/core';
3
3
  import { Color } from '@mintplayer/ng-bootstrap';
4
4
  import { SparkLanguageService } from '@mintplayer/ng-spark/services';
5
- import * as _mintplayer_ng_spark_models from '@mintplayer/ng-spark/models';
6
- import { PersistentObject, EntityType, CustomActionDefinition, LookupReference, EntityAttributeDefinition, AttributeTab, AttributeGroup, SparkQuery } from '@mintplayer/ng-spark/models';
7
- import { HttpErrorResponse } from '@angular/common/http';
8
- import { DatatableSettings, BsDatatableFetch } from '@mintplayer/ng-bootstrap/datatable';
5
+ import { PersistentObject, EntityType, CustomActionDefinition, LookupReference, EntityAttributeDefinition, AttributeTab, AttributeGroup } from '@mintplayer/ng-spark/models';
9
6
 
10
7
  declare class SparkPoDetailComponent {
11
8
  private readonly route;
@@ -20,6 +17,21 @@ declare class SparkPoDetailComponent {
20
17
  $implicit: PersistentObject;
21
18
  entityType: EntityType;
22
19
  }> | null>;
20
+ /**
21
+ * Header slots forwarded to every sub-query card on this page.
22
+ *
23
+ * A structural directive cannot cross a component boundary, and this component is created by
24
+ * the router — in a default app there is no `<spark-po-detail>` tag to project content into.
25
+ * So an app that wants slotted chrome on its sub-queries substitutes its own route component
26
+ * via `SparkRouteConfig.poDetail` and forwards the templates through here. Forwarding a
27
+ * `TemplateRef` as an input is already this component's idiom; see the two above.
28
+ *
29
+ * Each slot may target one query by alias, so a single forwarded set serves a page rendering
30
+ * several sub-queries.
31
+ */
32
+ queryIconTemplate: _angular_core.InputSignal<TemplateRef<any> | null>;
33
+ queryCaptionTemplate: _angular_core.InputSignal<TemplateRef<any> | null>;
34
+ queryActionsTemplate: _angular_core.InputSignal<TemplateRef<any> | null>;
23
35
  edited: _angular_core.OutputEmitterRef<void>;
24
36
  deleted: _angular_core.OutputEmitterRef<void>;
25
37
  customActionExecuted: _angular_core.OutputEmitterRef<{
@@ -59,140 +71,7 @@ declare class SparkPoDetailComponent {
59
71
  onDelete(): Promise<void>;
60
72
  onBack(): void;
61
73
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkPoDetailComponent, never>;
62
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<SparkPoDetailComponent, "spark-po-detail", never, { "showCustomActions": { "alias": "showCustomActions"; "required": false; "isSignal": true; }; "extraActionsTemplate": { "alias": "extraActionsTemplate"; "required": false; "isSignal": true; }; "extraContentTemplate": { "alias": "extraContentTemplate"; "required": false; "isSignal": true; }; }, { "edited": "edited"; "deleted": "deleted"; "customActionExecuted": "customActionExecuted"; }, never, never, true, never>;
63
- }
64
-
65
- declare class SparkSubQueryComponent {
66
- private readonly sparkService;
67
- private readonly gridRenderers;
68
- readonly lang: SparkLanguageService;
69
- queryId: _angular_core.InputSignal<string>;
70
- /**
71
- * The parent persistent object this query is scoped to, when it has one.
72
- *
73
- * Optional, because not every query is a detail of something: a page can host
74
- * a grid that stands on its own — "my accounts", a dashboard list — and the
75
- * server already treats an absent parent as "no parent" rather than as an
76
- * error. Leaving these required made that shape impossible to express: the
77
- * component simply never loaded, with no request, no error and no log.
78
- *
79
- * Pass both or neither. One without the other is ignored, matching
80
- * `SparkService.executeQuery`, which omits either param when it is falsy, and
81
- * the execute endpoint, which resolves a parent only when both are present.
82
- */
83
- parentId: _angular_core.InputSignal<string>;
84
- parentType: _angular_core.InputSignal<string>;
85
- /**
86
- * Change this to re-run the query. Any value works; only its identity matters.
87
- *
88
- * A declarative token rather than only a `reload()` method, because calling a
89
- * method means holding a component handle, and hosts wrap this grid in `@if`,
90
- * where a `viewChild` is intermittently undefined. Nothing else in ng-spark
91
- * uses `viewChild` either — the house idiom is to re-seed a signal.
92
- *
93
- * This drives the CHEAP refresh (see {@link reload}). It deliberately does not
94
- * feed the main effect: re-running `loadData` would re-resolve the query, the
95
- * entity types, the permissions and the lookups, and reset the user's page and
96
- * sort on every button press.
97
- */
98
- reloadToken: _angular_core.InputSignal<unknown>;
99
- /**
100
- * Render without the surrounding card, for a host that owns its own chrome — a tab
101
- * body, a modal, a dashboard tile.
102
- *
103
- * This is the escape hatch for a genuinely chromeless embed. It only serves a host
104
- * that instantiated this component by hand — where the component is auto-rendered
105
- * from `EntityTypeDefinition.Queries` there is no host to pass it.
106
- */
107
- showCard: _angular_core.InputSignal<boolean>;
108
- /**
109
- * Replace the header for one hand-instantiated usage.
110
- *
111
- * A `TemplateRef` rather than `<ng-content>` deliberately: it matches
112
- * `spark-po-detail`'s `extraActionsTemplate`/`extraContentTemplate`, and unlike
113
- * projection it can be forwarded by a host that is itself several layers up.
114
- *
115
- * Precedence is headerTemplate -> caption + query actions.
116
- */
117
- headerTemplate: _angular_core.InputSignal<TemplateRef<{
118
- $implicit: SparkQuery;
119
- }> | null>;
120
- colors: typeof Color;
121
- query: _angular_core.WritableSignal<SparkQuery | null>;
122
- entityType: _angular_core.WritableSignal<EntityType | null>;
123
- allEntityTypes: _angular_core.WritableSignal<EntityType[]>;
124
- /**
125
- * Why the component renders its own failure instead of only reporting one.
126
- *
127
- * `SparkService` is a bare `firstValueFrom` passthrough with no interceptor, so
128
- * every failure surfaces here and nowhere else. A host embedding this grid cannot
129
- * surface what it never sees, and the default has to be visible with no host
130
- * cooperation — hence a rendered alert, not just an output.
131
- *
132
- * A 404 is deliberately vague. `Endpoints/Queries/Get.cs` answers 404 for BOTH
133
- * "no such query" and "you may not see it", with byte-identical bodies, so that
134
- * existence is not disclosed (security audit M-3). This component therefore
135
- * genuinely cannot tell the two apart, and any message claiming otherwise would
136
- * either leak or mislead.
137
- */
138
- errorMessage: _angular_core.WritableSignal<string | null>;
139
- /**
140
- * Actions the query declares, rendered in this component's own header.
141
- *
142
- * This is what makes a query's chrome work with no host: a sub-query is rendered
143
- * automatically from `EntityTypeDefinition.Queries`, so there is nobody to project
144
- * a toolbar in. The query says what belongs in its header, and it follows the query
145
- * wherever it is rendered.
146
- */
147
- customActions: _angular_core.WritableSignal<CustomActionDefinition[]>;
148
- /** Emitted whenever a load or a page fetch fails, for a host in bespoke chrome. */
149
- error: _angular_core.OutputEmitterRef<HttpErrorResponse>;
150
- lookupReferenceOptions: _angular_core.WritableSignal<Record<string, LookupReference>>;
151
- loading: _angular_core.WritableSignal<boolean>;
152
- canRead: _angular_core.WritableSignal<boolean>;
153
- settings: _angular_core.WritableSignal<DatatableSettings>;
154
- fetchFn: _angular_core.WritableSignal<BsDatatableFetch<PersistentObject> | null>;
155
- /**
156
- * Rows the user has ticked. Lives here rather than in the datatable so the action bar can
157
- * read it, and MUST be cleared whenever the source changes — otherwise route A's selection
158
- * is POSTed as ids of route B's type.
159
- */
160
- selection: _angular_core.WritableSignal<PersistentObject[]>;
161
- private readonly queryRefresh;
162
- /** 'none' unless an action is selection-gated, so unaffected grids gain no checkbox column. */
163
- selectionMode: _angular_core.Signal<_mintplayer_ng_spark_models.SparkSelectionMode>;
164
- /** Whether an action's selection rule is satisfied right now. The server checks it again. */
165
- isActionEnabled(action: CustomActionDefinition): boolean;
166
- isVirtualScrolling: _angular_core.Signal<boolean>;
167
- visibleAttributes: _angular_core.Signal<EntityAttributeDefinition[]>;
168
- constructor();
169
- /**
170
- * Re-run the query, keeping the current page, sort and scroll position.
171
- *
172
- * Data-level on purpose: it re-seeds the fetch closure and nothing else, mirroring
173
- * `SparkQueryListComponent.reload()`. Use it after something mutates server-side
174
- * state the query reads from. For a definition change — new columns, a renamed
175
- * query — the inputs themselves must change; that is the expensive path.
176
- */
177
- onCustomAction(action: CustomActionDefinition): Promise<void>;
178
- private reportError;
179
- /**
180
- * A 404 is deliberately generic.
181
- *
182
- * `Endpoints/Queries/Get.cs` answers 404 with byte-identical bodies for "no such
183
- * query" and "you may not see it", so existence is not disclosed (audit M-3). The
184
- * component therefore cannot tell them apart, and both "Not found" and "Access
185
- * denied" would be a guess — one of them leaking, the other misleading.
186
- */
187
- private describe;
188
- reload(): void;
189
- private loadData;
190
- private makeFetch;
191
- private loadLookupReferenceOptions;
192
- getColumnRendererComponent(attr: EntityAttributeDefinition): Type<any> | null;
193
- getColumnRendererInputs(component: Type<any>, item: PersistentObject, attr: EntityAttributeDefinition): Record<string, any>;
194
- static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkSubQueryComponent, never>;
195
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<SparkSubQueryComponent, "spark-sub-query", 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; }; "showCard": { "alias": "showCard"; "required": false; "isSignal": true; }; "headerTemplate": { "alias": "headerTemplate"; "required": false; "isSignal": true; }; }, { "error": "error"; }, never, never, true, never>;
74
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SparkPoDetailComponent, "spark-po-detail", never, { "showCustomActions": { "alias": "showCustomActions"; "required": false; "isSignal": true; }; "extraActionsTemplate": { "alias": "extraActionsTemplate"; "required": false; "isSignal": true; }; "extraContentTemplate": { "alias": "extraContentTemplate"; "required": false; "isSignal": true; }; "queryIconTemplate": { "alias": "queryIconTemplate"; "required": false; "isSignal": true; }; "queryCaptionTemplate": { "alias": "queryCaptionTemplate"; "required": false; "isSignal": true; }; "queryActionsTemplate": { "alias": "queryActionsTemplate"; "required": false; "isSignal": true; }; }, { "edited": "edited"; "deleted": "deleted"; "customActionExecuted": "customActionExecuted"; }, never, never, true, never>;
196
75
  }
197
76
 
198
- export { SparkPoDetailComponent, SparkSubQueryComponent };
77
+ export { SparkPoDetailComponent };
@@ -4,10 +4,58 @@ import { CdkDragDrop } from '@angular/cdk/drag-drop';
4
4
  import { Color } from '@mintplayer/ng-bootstrap';
5
5
  import { InMemoryTreeSelectProvider, TreeNode } from '@mintplayer/ng-bootstrap/tree-select';
6
6
  import * as _mintplayer_ng_spark_models from '@mintplayer/ng-spark/models';
7
- import { EntityType, ValidationError, PersistentObject, LookupReference, EntityAttributeDefinition, EntityPermissions, ELookupDisplayType, EReferenceDisplayType, AttributeTab, AttributeGroup, LookupReferenceValue } from '@mintplayer/ng-spark/models';
7
+ import { PersistentObject, EntityType, ValidationError, RefreshOverlay, LookupReference, EntityAttributeDefinition, EntityPermissions, ELookupDisplayType, EReferenceDisplayType, AttributeTab, AttributeGroup, LookupReferenceValue, RuleFailure } from '@mintplayer/ng-spark/models';
8
8
  import { DatatableSettings } from '@mintplayer/ng-bootstrap/datatable';
9
9
  import { PaginationResponse } from '@mintplayer/pagination';
10
10
 
11
+ /** What the coordinator needs from its host, so it can be tested without mounting a form. */
12
+ interface RefreshCoordinatorHost {
13
+ /** POSTs the object and resolves with the reshaped one. */
14
+ send(triggeredBy: string): Promise<PersistentObject>;
15
+ /** Values as they are right now — read at dispatch time to snapshot what is being sent. */
16
+ currentValues(): Record<string, any>;
17
+ /** Applies a settled response. Not called for a superseded one. */
18
+ apply(response: PersistentObject, sent: Record<string, any>): void;
19
+ /** Surfaced so the host can show a busy affordance. Never used to disable fields. */
20
+ setBusy(busy: boolean): void;
21
+ }
22
+ /**
23
+ * Serializes refreshes for **one** form instance and drops superseded ones.
24
+ *
25
+ * Per-instance rather than a service, deliberately. The retry-action modal renders its own
26
+ * `spark-po-form`, and a refresh can carry a retry operation — so a refresh can open a modal
27
+ * containing a form whose own attributes may trigger refreshes. A shared coordinator would let the
28
+ * nested form resolve or supersede the outer form's pending request. The same applies to the
29
+ * recursive `spark-po-form` used for modal AsDetail editing.
30
+ *
31
+ * Cancellation is not available: the service layer is promise-based (`firstValueFrom`), so a stale
32
+ * response *will* arrive. It is discarded by sequence number rather than prevented.
33
+ */
34
+ declare class RefreshCoordinator {
35
+ private readonly host;
36
+ private queue;
37
+ private sequence;
38
+ private settled;
39
+ private pending;
40
+ constructor(host: RefreshCoordinatorHost);
41
+ /** Whether a refresh is in flight. */
42
+ get isRefreshing(): boolean;
43
+ /**
44
+ * Marks `attributeName` as needing a refresh without sending one — for free-text editors, which
45
+ * would otherwise issue a request per keystroke. Flushed by {@link blur} or {@link flush}.
46
+ */
47
+ markPending(attributeName: string): void;
48
+ /** Sends a pending refresh for `attributeName`, if one was marked. */
49
+ blur(attributeName: string): Promise<void>;
50
+ /**
51
+ * Sends every refresh still marked pending. Called before save, so a value typed and never blurred
52
+ * — the user tabbing straight to the save button — is still reflected before the object goes.
53
+ */
54
+ flush(): Promise<void>;
55
+ /** Sends a refresh immediately — discrete editors, where every change is a committed one. */
56
+ trigger(attributeName: string): Promise<void>;
57
+ }
58
+
11
59
  declare class SparkPoFormComponent {
12
60
  private readonly sparkService;
13
61
  private readonly translations;
@@ -21,6 +69,23 @@ declare class SparkPoFormComponent {
21
69
  parentType: _angular_core.InputSignal<string | undefined>;
22
70
  save: _angular_core.OutputEmitterRef<void>;
23
71
  cancel: _angular_core.OutputEmitterRef<void>;
72
+ /**
73
+ * The type id to refresh against. Absent means refresh is unavailable — the form still renders and
74
+ * edits normally, so a host that has not opted in loses nothing.
75
+ */
76
+ objectTypeId: _angular_core.InputSignal<string | undefined>;
77
+ /** The id of the object being edited; absent for a create. */
78
+ objectId: _angular_core.InputSignal<string | undefined>;
79
+ /**
80
+ * What the last refresh changed about each attribute's presentation, keyed by attribute name.
81
+ *
82
+ * Deliberately NOT folded back into `entityType`. All option loading hangs off one effect keyed on
83
+ * `entityType` identity and `SparkService` caches nothing, so re-setting it would re-issue every
84
+ * reference query and lookup fetch on every refresh; mutating it in place would not re-render at
85
+ * all.
86
+ */
87
+ refreshOverlay: _angular_core.WritableSignal<RefreshOverlay>;
88
+ isRefreshing: _angular_core.WritableSignal<boolean>;
24
89
  colors: typeof Color;
25
90
  referenceOptions: _angular_core.WritableSignal<Record<string, PersistentObject[]>>;
26
91
  referenceProviders: _angular_core.WritableSignal<Record<string, InMemoryTreeSelectProvider>>;
@@ -35,6 +100,15 @@ declare class SparkPoFormComponent {
35
100
  asDetailReferenceOptions: _angular_core.WritableSignal<Record<string, Record<string, PersistentObject[]>>>;
36
101
  ELookupDisplayType: typeof ELookupDisplayType;
37
102
  EReferenceDisplayType: typeof EReferenceDisplayType;
103
+ /**
104
+ * Every attribute this form could ever need option data for — including ones the model hides,
105
+ * because a refresh may reveal them.
106
+ *
107
+ * Read by the option-loading effect, and deliberately independent of `refreshOverlay`: the loaders
108
+ * read this synchronously, so an overlay dependency here would make every refresh re-issue every
109
+ * reference query and lookup fetch. That is the whole reason the overlay is a separate signal.
110
+ */
111
+ optionSourceAttributes: _angular_core.Signal<EntityAttributeDefinition[]>;
38
112
  editableAttributes: _angular_core.Signal<EntityAttributeDefinition[]>;
39
113
  private static readonly DEFAULT_TAB;
40
114
  ungroupedAttributes: _angular_core.Signal<EntityAttributeDefinition[]>;
@@ -66,12 +140,66 @@ declare class SparkPoFormComponent {
66
140
  /** Edit-renderer for an inline AsDetail cell (so inline editing honors `col.renderer`, not just display). */
67
141
  getAsDetailCellEditRenderer(col: EntityAttributeDefinition): Type<any> | null;
68
142
  getAsDetailCellEditRendererInputs(component: Type<any>, row: Record<string, any>, col: EntityAttributeDefinition): Record<string, any>;
143
+ /**
144
+ * Rules evaluated in the browser, against the *effective* metadata — so a rule a refresh hook
145
+ * imposed is visible before the round-trip rather than only after the server rejects the save.
146
+ */
147
+ clientRuleFailures: _angular_core.Signal<RuleFailure[]>;
69
148
  hasError(attrName: string): boolean;
70
149
  private inlineErrorPath;
71
150
  hasInlineError(attr: EntityAttributeDefinition, rowIndex: number, col: EntityAttributeDefinition): boolean;
72
151
  inlineErrorMessage(attr: EntityAttributeDefinition, rowIndex: number, col: EntityAttributeDefinition): string | null;
73
- onFieldChange(): void;
74
- onSave(): void;
152
+ /**
153
+ * The single funnel every scalar / boolean / inline-cell edit passes through.
154
+ *
155
+ * `attr` is optional only so the AsDetail modal's recursive form, which has no trigger context,
156
+ * can still call it. A caller that knows which attribute changed should always say so — without it
157
+ * no refresh can fire.
158
+ */
159
+ onFieldChange(attr?: EntityAttributeDefinition): void;
160
+ /**
161
+ * A trigger inside an AsDetail row. Addressed by the same `{attr}[{index}].{col}` path the inline
162
+ * validation errors already use, so the server can tell which row asked without a second
163
+ * addressing scheme being invented for it.
164
+ */
165
+ onInlineCellChange(attr: EntityAttributeDefinition, rowIndex: number, col: EntityAttributeDefinition): void;
166
+ /** Which detail row the in-flight refresh belongs to, if any. */
167
+ private pendingNestedTrigger;
168
+ /**
169
+ * Applies a refresh that ran against a detail row: the row's own values, and the column metadata
170
+ * for the grid it lives in.
171
+ *
172
+ * The column metadata comes from `asDetailTypes` — a different signal from `entityType` — which is
173
+ * why a nested response cannot go through the top-level overlay.
174
+ *
175
+ * ⚠️ The row array is mutated in place rather than replaced. Rows are tracked by index, so handing
176
+ * the template a new array destroys and rebuilds every row's DOM and takes focus with it, mid-edit.
177
+ */
178
+ private applyNestedResponse;
179
+ onInlineCellBlur(attr: EntityAttributeDefinition, rowIndex: number, col: EntityAttributeDefinition): void;
180
+ /** Blur handler for free-text editors — sends the refresh their keystrokes only marked pending. */
181
+ onFieldBlur(attr: EntityAttributeDefinition): void;
182
+ private noteChange;
183
+ private canRefresh;
184
+ /**
185
+ * Per-instance, never a service: the retry-action modal renders its own `spark-po-form`, and a
186
+ * refresh may carry a retry operation — so a refresh can open a modal containing a form that
187
+ * refreshes. A shared coordinator would let the nested form supersede this one's request.
188
+ */
189
+ protected readonly refreshCoordinator: RefreshCoordinator;
190
+ private buildRefreshPayload;
191
+ /**
192
+ * Folds replaced option lists into the signals the editors already read, so a refreshed dropdown
193
+ * renders through the same path as a loaded one.
194
+ *
195
+ * `undefined` means the hook did not touch this attribute's options and the loaded set stands; an
196
+ * empty array means it deliberately left none. Collapsing the two would blank every dropdown the
197
+ * hook never mentioned.
198
+ */
199
+ private applyRefreshedOptions;
200
+ /** Sends anything still pending, so a typed-but-never-blurred trigger is reflected before save. */
201
+ flushPendingRefresh(): Promise<void>;
202
+ onSave(): Promise<void>;
75
203
  onCancel(): void;
76
204
  openAsDetailEditor(attr: EntityAttributeDefinition): void;
77
205
  saveAsDetailObject(): void;
@@ -82,7 +210,7 @@ declare class SparkPoFormComponent {
82
210
  removeArrayItem(attr: EntityAttributeDefinition, index: number): void;
83
211
  onAsDetailReorder(attr: EntityAttributeDefinition, event: CdkDragDrop<Record<string, any>[]>): void;
84
212
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkPoFormComponent, never>;
85
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<SparkPoFormComponent, "spark-po-form", never, { "entityType": { "alias": "entityType"; "required": false; "isSignal": true; }; "formData": { "alias": "formData"; "required": false; "isSignal": true; }; "validationErrors": { "alias": "validationErrors"; "required": false; "isSignal": true; }; "showButtons": { "alias": "showButtons"; "required": false; "isSignal": true; }; "isSaving": { "alias": "isSaving"; "required": false; "isSignal": true; }; "parentId": { "alias": "parentId"; "required": false; "isSignal": true; }; "parentType": { "alias": "parentType"; "required": false; "isSignal": true; }; }, { "formData": "formDataChange"; "save": "save"; "cancel": "cancel"; }, never, never, true, never>;
213
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SparkPoFormComponent, "spark-po-form", never, { "entityType": { "alias": "entityType"; "required": false; "isSignal": true; }; "formData": { "alias": "formData"; "required": false; "isSignal": true; }; "validationErrors": { "alias": "validationErrors"; "required": false; "isSignal": true; }; "showButtons": { "alias": "showButtons"; "required": false; "isSignal": true; }; "isSaving": { "alias": "isSaving"; "required": false; "isSignal": true; }; "parentId": { "alias": "parentId"; "required": false; "isSignal": true; }; "parentType": { "alias": "parentType"; "required": false; "isSignal": true; }; "objectTypeId": { "alias": "objectTypeId"; "required": false; "isSignal": true; }; "objectId": { "alias": "objectId"; "required": false; "isSignal": true; }; }, { "formData": "formDataChange"; "save": "save"; "cancel": "cancel"; }, never, never, true, never>;
86
214
  }
87
215
 
88
216
  /**
@@ -1,19 +1,34 @@
1
1
  import * as _mintplayer_ng_spark_models from '@mintplayer/ng-spark/models';
2
- import { PersistentObject, CustomActionDefinition, SparkQuery, EntityType, LookupReference, EntityAttributeDefinition } from '@mintplayer/ng-spark/models';
2
+ import { PersistentObject, CustomActionDefinition } from '@mintplayer/ng-spark/models';
3
3
  import * as _angular_core from '@angular/core';
4
- import { TemplateRef, Type } from '@angular/core';
4
+ import { TemplateRef } from '@angular/core';
5
5
  import { Color } from '@mintplayer/ng-bootstrap';
6
- import { BsDatatableFetch, DatatableSettings } from '@mintplayer/ng-bootstrap/datatable';
7
6
  import { SparkLanguageService } from '@mintplayer/ng-spark/services';
8
7
 
8
+ /**
9
+ * The routed query page: chrome around one {@link SparkQueryGridComponent}.
10
+ *
11
+ * It owns what is genuinely page-shaped and route-shaped, and nothing else:
12
+ *
13
+ * - **Route resolution.** It has no `queryId` input; it reads `paramMap`, and it serves two
14
+ * routes — `query/:queryId`, and `po/:type`, which resolves an entity type to a query. That
15
+ * second one is type-to-query resolution, not query rendering, and is why this component still
16
+ * exists rather than the router pointing at the grid.
17
+ * - **Streaming.** The websocket lives here so it stays out of every PO detail page's bundle;
18
+ * the snapshot is filtered and sorted client-side and handed to the grid as `[data]`.
19
+ * - The action bar, the caption, the LIVE badge, the search box and the New button.
20
+ *
21
+ * The grid itself — columns, cells, paging, the row link, selection, custom-action execution —
22
+ * is the shared component. This page previously wrote out `<bs-datatable>` twice, once per
23
+ * transport, with a shared row template between them; both are gone.
24
+ */
9
25
  declare class SparkQueryListComponent {
10
26
  private readonly route;
11
27
  private readonly router;
12
28
  private readonly sparkService;
13
29
  private readonly streamingService;
14
- protected readonly lang: SparkLanguageService;
15
- private readonly gridRenderers;
16
30
  private readonly destroyRef;
31
+ protected readonly lang: SparkLanguageService;
17
32
  extraActionsTemplate: _angular_core.InputSignal<TemplateRef<void> | null>;
18
33
  showCustomActions: _angular_core.InputSignal<boolean>;
19
34
  rowClicked: _angular_core.OutputEmitterRef<PersistentObject>;
@@ -22,76 +37,48 @@ declare class SparkQueryListComponent {
22
37
  action: CustomActionDefinition;
23
38
  }>;
24
39
  colors: typeof Color;
40
+ /** The query the grid should render, resolved from the route. Null until it is known. */
41
+ queryId: _angular_core.WritableSignal<string | null>;
25
42
  errorMessage: _angular_core.WritableSignal<string | null>;
26
- query: _angular_core.WritableSignal<SparkQuery | null>;
27
- entityType: _angular_core.WritableSignal<EntityType | null>;
28
- allEntityTypes: _angular_core.WritableSignal<EntityType[]>;
29
- lookupReferenceOptions: _angular_core.WritableSignal<Record<string, LookupReference>>;
30
- resultCount: _angular_core.WritableSignal<number | null>;
31
- searchTerm: string;
32
- canRead: _angular_core.WritableSignal<boolean>;
33
- canCreate: _angular_core.WritableSignal<boolean>;
34
- customActions: _angular_core.WritableSignal<CustomActionDefinition[]>;
35
- isStreaming: _angular_core.WritableSignal<boolean>;
36
- private streamingSub;
37
- private allItems;
38
- streamItems: _angular_core.WritableSignal<PersistentObject[]>;
39
- fetchFn: _angular_core.WritableSignal<BsDatatableFetch<PersistentObject> | null>;
40
- settings: _angular_core.WritableSignal<DatatableSettings>;
41
- constructor();
42
- private onParamsChange;
43
- /**
44
- * A load failure has to render, not just be swallowed: a denied query answers 404
45
- * (audit M-3, so existence is not leaked), which is indistinguishable from a missing
46
- * one -- hence a deliberately generic message rather than a guess at which it was.
47
- */
48
- private reportLoadFailure;
49
- onCustomAction(action: CustomActionDefinition): Promise<void>;
50
- private resolveEntityTypeForQuery;
51
- private extractSourceName;
52
- private singularize;
53
- /**
54
- * Builds the server-side fetch callback the datatable invokes per page/sort.
55
- * Reads `searchTerm` live, so a settings change (or a new fetchFn identity)
56
- * refetches with the current search term.
57
- */
58
- private makeFetch;
43
+ searchTerm: _angular_core.WritableSignal<string>;
44
+ private readonly grid;
59
45
  /**
60
- * Force a refetch (e.g. after a custom action) without changing page/sort.
46
+ * Grid state, surfaced for this page's chrome.
61
47
  *
62
- * Public so a host can drive it, and named to match
63
- * `SparkSubQueryComponent.reload()` the two grids had drifted into having the
64
- * same mechanism under different names, one of them unreachable.
48
+ * Optional `viewChild`, read defensively: the action bar and caption render above the grid, so
49
+ * on the first change-detection pass the query has not resolved yet.
65
50
  */
66
- reload(): void;
67
- onSearchChange(): void;
68
- clearSearch(): void;
51
+ protected readonly query: _angular_core.Signal<_mintplayer_ng_spark_models.SparkQuery | null>;
52
+ protected readonly entityType: _angular_core.Signal<_mintplayer_ng_spark_models.EntityType | null>;
53
+ protected readonly customActions: _angular_core.Signal<CustomActionDefinition[]>;
54
+ protected readonly canCreate: _angular_core.Signal<boolean>;
55
+ protected readonly resultCount: _angular_core.Signal<number | null>;
56
+ protected readonly isVirtualScrolling: _angular_core.Signal<boolean>;
57
+ protected readonly gridError: _angular_core.Signal<string | null>;
58
+ /** Whether an action's selection rule is satisfied. Delegated: the grid holds the selection. */
59
+ protected isActionEnabled(action: CustomActionDefinition): boolean;
60
+ isStreaming: _angular_core.WritableSignal<boolean>;
61
+ private streamingSub;
62
+ private readonly allItems;
63
+ private readonly streamItems;
69
64
  /**
70
- * Whether the first column links to a detail page.
65
+ * Rows handed to the grid, or `null` to let it fetch for itself.
71
66
  *
72
- * Declared by the query, because the framework cannot derive it: `Database.*` rows
73
- * are always real documents, but a `Custom.*` query may return loadable documents
74
- * (Fleet's Stolen_Cars) or rows fabricated in memory (StreamItems). Absent means
75
- * navigable -- defaulting Custom.* to false would strip the working links off every
76
- * custom query that does return documents.
67
+ * Null for a normal query an empty array would read as "here are no rows" and suppress the
68
+ * fetch entirely.
77
69
  */
70
+ protected readonly gridData: _angular_core.Signal<PersistentObject[] | null>;
71
+ constructor();
72
+ private onParamsChange;
78
73
  /**
79
- * Rows the user has ticked. Lives here rather than in the datatable so the action bar can
80
- * read it, and MUST be cleared whenever the source changes otherwise route A's selection
81
- * is POSTed as ids of route B's type.
74
+ * A load failure has to render, not just be swallowed: a denied query answers 404 (audit M-3, so
75
+ * existence is not leaked), which is indistinguishable from a missing onehence a deliberately
76
+ * generic message rather than a guess at which it was.
82
77
  */
83
- selection: _angular_core.WritableSignal<PersistentObject[]>;
84
- private readonly queryRefresh;
85
- /** 'none' unless an action is selection-gated, so unaffected grids gain no checkbox column. */
86
- selectionMode: _angular_core.Signal<_mintplayer_ng_spark_models.SparkSelectionMode>;
87
- /** Whether an action's selection rule is satisfied right now. The server checks it again. */
88
- isActionEnabled(action: CustomActionDefinition): boolean;
89
- isVirtualScrolling: _angular_core.Signal<boolean>;
90
- visibleAttributes: _angular_core.Signal<EntityAttributeDefinition[]>;
91
- getColumnRendererComponent(attr: EntityAttributeDefinition): Type<any> | null;
92
- getColumnRendererInputs(component: Type<any>, item: PersistentObject, attr: EntityAttributeDefinition): Record<string, any>;
93
- private loadLookupReferenceOptions;
94
- onCreate(): void;
78
+ private reportLoadFailure;
79
+ protected onCustomAction(action: CustomActionDefinition): Promise<void>;
80
+ protected onCreate(): void;
81
+ protected clearSearch(): void;
95
82
  private connectStreaming;
96
83
  private disconnectStreaming;
97
84
  private handleStreamingMessage;
@@ -71,6 +71,16 @@ declare class SparkService {
71
71
  get(type: string, id: string): Promise<PersistentObject>;
72
72
  create(type: string, data: Partial<PersistentObject>): Promise<PersistentObject>;
73
73
  update(type: string, id: string, data: Partial<PersistentObject>): Promise<PersistentObject>;
74
+ /**
75
+ * Asks the server to reshape an in-progress object after `triggeredBy`'s value changed.
76
+ *
77
+ * Writes nothing, but goes through the envelope like every other mutating call: a refresh may
78
+ * legitimately emit notifications, and may open the retry-action prompt.
79
+ *
80
+ * `triggeredBy` is the attribute's name. For a trigger inside an AsDetail row it is the same
81
+ * path form the inline validation errors use — `Jobs[2].ProfessionId`.
82
+ */
83
+ refresh(type: string, data: Partial<PersistentObject>, triggeredBy: string): Promise<PersistentObject>;
74
84
  delete(type: string, id: string): Promise<void>;
75
85
  getCustomActions(objectTypeId: string): Promise<CustomActionDefinition[]>;
76
86
  executeCustomAction(objectTypeId: string, actionName: string, parent?: PersistentObject, selectedItems?: PersistentObject[]): Promise<void>;