@mintplayer/ng-spark 22.4.0 → 22.6.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 (30) hide show
  1. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +12 -12
  2. package/fesm2022/mintplayer-ng-spark-grid.mjs +21 -21
  3. package/fesm2022/mintplayer-ng-spark-icon.mjs +3 -3
  4. package/fesm2022/mintplayer-ng-spark-models.mjs +185 -1
  5. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  6. package/fesm2022/mintplayer-ng-spark-pipes.mjs +82 -70
  7. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  8. package/fesm2022/mintplayer-ng-spark-po-create.mjs +4 -4
  9. package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
  10. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +3 -3
  11. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +4 -4
  12. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  13. package/fesm2022/mintplayer-ng-spark-po-form.mjs +356 -18
  14. package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
  15. package/fesm2022/mintplayer-ng-spark-query-list.mjs +3 -3
  16. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +4 -4
  17. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
  18. package/fesm2022/mintplayer-ng-spark-services.mjs +27 -15
  19. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  20. package/fesm2022/mintplayer-ng-spark-shell.mjs +379 -0
  21. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -0
  22. package/fesm2022/mintplayer-ng-spark.mjs +17 -1
  23. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  24. package/package.json +5 -1
  25. package/types/mintplayer-ng-spark-models.d.ts +106 -2
  26. package/types/mintplayer-ng-spark-pipes.d.ts +9 -1
  27. package/types/mintplayer-ng-spark-po-form.d.ts +132 -4
  28. package/types/mintplayer-ng-spark-services.d.ts +10 -0
  29. package/types/mintplayer-ng-spark-shell.d.ts +242 -0
  30. package/types/mintplayer-ng-spark.d.ts +17 -2
@@ -144,6 +144,13 @@ interface EntityAttributeDefinition {
144
144
  referenceDisplayType?: EReferenceDisplayType;
145
145
  /** For array AsDetail attributes: when true, rows can be drag-reordered (order = array position) */
146
146
  isSortable?: boolean;
147
+ /**
148
+ * When true, changing this attribute's value posts the in-progress object to
149
+ * `/spark/po/{objectTypeId}/refresh` and applies the reshaped result as an overlay.
150
+ * Schema-only by design — it never travels on a PersistentObjectAttribute, so a client
151
+ * cannot claim a trigger the model did not declare.
152
+ */
153
+ triggersRefresh?: boolean;
147
154
  /** For LookupReference attributes, specifies the lookup reference type name */
148
155
  lookupReferenceType?: string;
149
156
  /**
@@ -234,9 +241,21 @@ interface ProgramUnit {
234
241
  id: string;
235
242
  name: TranslatedString;
236
243
  icon?: string;
244
+ /**
245
+ * Canonical unit type, exact-cased by the server's loader: 'query' | 'persistentObject' | 'url'.
246
+ * Kept as string rather than a union so an older client tolerates a newer server.
247
+ */
237
248
  type: string;
238
249
  queryId?: string;
239
250
  persistentObjectId?: string;
251
+ /**
252
+ * For a persistentObject unit: the specific object to open — the menu entry deep-links to
253
+ * `/po/{type}/{objectId}`. Absent means the type's default list. For a composed page this is
254
+ * whatever stable string the app declared; the server's Actions class may ignore it.
255
+ */
256
+ objectId?: string;
257
+ /** For a url unit: the external address, rendered as a plain anchor (never a router link). */
258
+ url?: string;
240
259
  order: number;
241
260
  alias?: string;
242
261
  }
@@ -446,5 +465,90 @@ type SparkSelectionMode = 'none' | 'single' | 'multiple';
446
465
  */
447
466
  declare function selectionModeFor(actions: CustomActionDefinition[]): SparkSelectionMode;
448
467
 
449
- export { AS_DETAIL_BREADCRUMBS_KEY, AS_DETAIL_SELF_BREADCRUMB_KEY, ELookupDisplayType, EReferenceDisplayType, ShowedOn, currentLanguage, dictToNestedPo, filterQueryActions, hasShowedOnFlag, nestedPoToDict, nestedPoToDisplayRow, parseSelectionRule, resolveTranslation, selectionModeFor, selfBreadcrumb };
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 };
468
+ /**
469
+ * One selectable value, as replaced by a refresh hook. Mirrors the server's
470
+ * `PersistentObjectAttributeOption`.
471
+ */
472
+ interface RefreshedOption {
473
+ key: string;
474
+ label?: Record<string, string>;
475
+ }
476
+ /**
477
+ * What a refresh changed about one attribute's *presentation*.
478
+ *
479
+ * Kept separate from `EntityType` on purpose. The form's option loading hangs off a single effect
480
+ * keyed on `entityType` identity, and `SparkService` caches nothing — so applying a refresh by
481
+ * setting a new `EntityType` re-issues every reference query, every lookup fetch, a full
482
+ * `getEntityTypes()` and a `getPermissions()` per array-AsDetail attribute, on every refresh.
483
+ * Mutating the existing object instead is inert, because the rendering computed would not re-run.
484
+ * An overlay is the only shape that is both reactive and free.
485
+ */
486
+ interface AttributeOverlay {
487
+ isRequired?: boolean;
488
+ isReadOnly?: boolean;
489
+ isVisible?: boolean;
490
+ rules?: ValidationRule[];
491
+ query?: string;
492
+ /** `undefined` means the hook did not touch the options; an empty array means there are none. */
493
+ options?: RefreshedOption[];
494
+ }
495
+ type RefreshOverlay = Record<string, AttributeOverlay>;
496
+ /** Applies an overlay to one attribute definition, returning a new object when anything changed. */
497
+ declare function applyOverlay(attr: EntityAttributeDefinition, overlay: AttributeOverlay | undefined): EntityAttributeDefinition;
498
+ /**
499
+ * Reads a refresh response into an overlay.
500
+ *
501
+ * Everything here is presentation the server owns outright, so it is taken verbatim — there is no
502
+ * merging to do on this half, only on values.
503
+ */
504
+ declare function overlayFromResponse(response: PersistentObject): RefreshOverlay;
505
+ /**
506
+ * Merges a refresh response's values into the live form.
507
+ *
508
+ * The rule, and the reason for it: a refresh is not instant, and the user keeps typing during it —
509
+ * the form is deliberately never frozen. So for each attribute we ask whether the *server* changed
510
+ * it, by comparing the response against the values that were **sent**, not against what is on
511
+ * screen now.
512
+ *
513
+ * - server value equals what we sent → the hook did not touch it, so whatever is in the form now
514
+ * wins, including anything typed while the request was in flight;
515
+ * - server value differs → the hook deliberately changed it, and it wins over a concurrent edit.
516
+ *
517
+ * Comparing against the displayed value instead is the classic "refresh eats my typing" bug;
518
+ * refusing to overwrite anything the user touched is the equally wrong opposite, where a dependent
519
+ * field the hook computed never appears.
520
+ *
521
+ * @param sent values as they were POSTed, captured before the request left
522
+ * @param current values as they are now, which may have moved on
523
+ * @param response the reshaped object
524
+ */
525
+ declare function mergeRefreshValues(sent: Record<string, any>, current: Record<string, any>, response: PersistentObject): Record<string, any>;
526
+
527
+ /** One rule failure, in the shape the form already renders per field. */
528
+ interface RuleFailure {
529
+ attributeName: string;
530
+ ruleType: string;
531
+ message: string;
532
+ }
533
+ interface EvaluableAttribute {
534
+ name: string;
535
+ label?: TranslatedString;
536
+ isRequired?: boolean;
537
+ rules?: ValidationRule[];
538
+ }
539
+ /**
540
+ * Evaluates an attribute's rules against a value, mirroring the server's `ValidationService`.
541
+ *
542
+ * ⚠️ **Parity with the server is the point, and disagreement is worse than silence.** A client that
543
+ * rejects something the server would accept blocks legitimate work with no recourse; one that
544
+ * accepts something the server rejects merely defers the error to the round-trip, which is where it
545
+ * used to live anyway. So the rule set here is deliberately limited to the types the server
546
+ * implements, and an unrecognised rule type is ignored rather than guessed at.
547
+ *
548
+ * This exists because a refresh hook that imposes a rule needs it to bite before Save — previously
549
+ * `rules` was carried on the wire and never evaluated in the browser at all.
550
+ */
551
+ declare function evaluateRules(attr: EvaluableAttribute, value: any): RuleFailure[];
552
+
553
+ export { AS_DETAIL_BREADCRUMBS_KEY, AS_DETAIL_SELF_BREADCRUMB_KEY, ELookupDisplayType, EReferenceDisplayType, ShowedOn, applyOverlay, currentLanguage, dictToNestedPo, evaluateRules, filterQueryActions, hasShowedOnFlag, mergeRefreshValues, nestedPoToDict, nestedPoToDisplayRow, overlayFromResponse, parseSelectionRule, resolveTranslation, selectionModeFor, selfBreadcrumb };
554
+ export type { AttributeGroup, AttributeOverlay, AttributeTab, CustomActionDefinition, EntityAttributeDefinition, EntityPermissions, EntityType, EntityTypeResolver, EvaluableAttribute, LookupReference, LookupReferenceListItem, LookupReferenceValue, PersistentObject, PersistentObjectAttribute, PersistentObjectPermissions, ProgramUnit, ProgramUnitGroup, ProgramUnitsConfiguration, QueryResult, RefreshOverlay, RefreshedOption, RetryActionPayload, RetryActionResult, RuleFailure, SparkQuery, SparkQueryRenderMode, SparkQuerySortColumn, SparkSelectionMode, StreamingErrorMessage, StreamingMessage, StreamingPatchItem, StreamingPatchMessage, StreamingSnapshotMessage, TranslatedString, ValidationError, ValidationErrorResponse, ValidationRule };
@@ -70,8 +70,16 @@ declare class ReferenceChipsPipe implements PipeTransform {
70
70
  static ɵpipe: i0.ɵɵPipeDeclaration<ReferenceChipsPipe, "referenceChips", true>;
71
71
  }
72
72
 
73
+ /**
74
+ * Maps a program unit to the router commands that open it. Type comparisons are exact: the
75
+ * server's loader canonicalizes `type` casing, so tolerating variants here would only mask a
76
+ * server that stopped doing so.
77
+ *
78
+ * Returns null for a `url` unit — an external address is an `<a href>`, not a router link; the
79
+ * component rendering the menu owns that branch.
80
+ */
73
81
  declare class RouterLinkPipe implements PipeTransform {
74
- transform(unit: ProgramUnit): string[];
82
+ transform(unit: ProgramUnit): string[] | null;
75
83
  static ɵfac: i0.ɵɵFactoryDeclaration<RouterLinkPipe, never>;
76
84
  static ɵpipe: i0.ɵɵPipeDeclaration<RouterLinkPipe, "routerLink", true>;
77
85
  }
@@ -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
  /**
@@ -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>;
@@ -0,0 +1,242 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { TemplateRef } from '@angular/core';
3
+ import { BsShellState } from '@mintplayer/ng-bootstrap/shell';
4
+ import { Breakpoint } from '@mintplayer/ng-bootstrap';
5
+ import { ShellStateChangeEventDetail } from '@mintplayer/web-components/shell';
6
+ import { ProgramUnitGroup } from '@mintplayer/ng-spark/models';
7
+ import { SparkLanguageService } from '@mintplayer/ng-spark/services';
8
+
9
+ /**
10
+ * Region slots for `<spark-shell>`.
11
+ *
12
+ * Each directive marks a template that REPLACES one region of the shell chrome (or fills an
13
+ * empty one). An omitted slot is not an empty slot: the shell renders its default — the toggler,
14
+ * the language selector, the title heading — so a host that supplies nothing still gets a
15
+ * complete working shell, and a host that supplies one slot leaves the rest alone.
16
+ *
17
+ * The menu itself is deliberately NOT a slot. Navigation is sourced entirely from
18
+ * `programUnits.json` through the rights-filtered `/spark/program-units` endpoint and re-fetched
19
+ * on sign-in/out; a host that finds itself writing unit anchors in a slot should be adding units
20
+ * to `programUnits.json` instead. Slots exist for the content AROUND the menu: an auth bar, a
21
+ * user chip, branding, a one-off extra link, an alert strip above the routed content.
22
+ *
23
+ * Naming follows the house convention (prefix, component, slot — see `*sparkQueryIcon` in
24
+ * `@mintplayer/ng-spark/grid`): `sparkShell` + region. Every slot also exists as a `TemplateRef`
25
+ * input on `SparkShellComponent` for hosts that cannot use content projection.
26
+ *
27
+ * ```html
28
+ * <spark-shell title="My App">
29
+ * <spark-auth-bar *sparkShellTopbarEnd />
30
+ * <div *sparkShellSidebarTop>
31
+ * <a routerLink="/github-projects" class="nav-link">GitHub projects</a>
32
+ * </div>
33
+ * <router-outlet />
34
+ * </spark-shell>
35
+ * ```
36
+ */
37
+ /** Topbar, leading edge. Default: a `bs-navbar-toggler` mirroring the shell's open state. */
38
+ declare class SparkShellTopbarStartDirective {
39
+ readonly templateRef: TemplateRef<unknown>;
40
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkShellTopbarStartDirective, never>;
41
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SparkShellTopbarStartDirective, "[sparkShellTopbarStart]", never, {}, {}, never, never, true, never>;
42
+ }
43
+ /**
44
+ * Topbar, trailing edge. Default: the language selector (which hides itself when the app has
45
+ * one language). This is where an auth bar goes — the shell cannot ship one itself, since
46
+ * `@mintplayer/ng-spark` does not (and must not) depend on `@mintplayer/ng-spark-auth`.
47
+ */
48
+ declare class SparkShellTopbarEndDirective {
49
+ readonly templateRef: TemplateRef<unknown>;
50
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkShellTopbarEndDirective, never>;
51
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SparkShellTopbarEndDirective, "[sparkShellTopbarEnd]", never, {}, {}, never, never, true, never>;
52
+ }
53
+ /** Sidebar, above everything. Default: `<h5>{{ title }}</h5>`. */
54
+ declare class SparkShellSidebarHeaderDirective {
55
+ readonly templateRef: TemplateRef<unknown>;
56
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkShellSidebarHeaderDirective, never>;
57
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SparkShellSidebarHeaderDirective, "[sparkShellSidebarHeader]", never, {}, {}, never, never, true, never>;
58
+ }
59
+ /** Sidebar, between the header and the program-units menu. No default. */
60
+ declare class SparkShellSidebarTopDirective {
61
+ readonly templateRef: TemplateRef<unknown>;
62
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkShellSidebarTopDirective, never>;
63
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SparkShellSidebarTopDirective, "[sparkShellSidebarTop]", never, {}, {}, never, never, true, never>;
64
+ }
65
+ /**
66
+ * An extra accordion tab in the sidebar menu, rendered after the generated program-unit groups
67
+ * and sharing their single-open behavior.
68
+ *
69
+ * A tab is contributed as DATA (header + body template), not as markup, and that is load-bearing:
70
+ * `bs-accordion` discovers its tabs with an Angular content query, which matches by declaration
71
+ * view, so a `<bs-accordion-tab>` written in a host's template and inserted into the library's
72
+ * accordion is never registered — it would land at index -1, get no hoisted header and no slot.
73
+ * Declaring a second `<bs-accordion>` instead is what puts the tab in its own exclusivity group:
74
+ * `mp-accordion` enforces single-open per element, over children it owns and over
75
+ * `<details name>`, whose grouping cannot cross a shadow root. So the tab element must be created
76
+ * by the menu itself, from what this directive carries.
77
+ *
78
+ * ```html
79
+ * <ng-container *sparkShellTab="'Component demos'; icon: 'palette'">
80
+ * <a routerLink="/query-slots" routerLinkActive="active" class="nav-link">Query card slots</a>
81
+ * </ng-container>
82
+ * ```
83
+ *
84
+ * Navigation still belongs in `programUnits.json` — this is for pages the model cannot describe
85
+ * (client-side demos, external tools). For sidebar content that is NOT an accordion tab, use
86
+ * `*sparkShellSidebarTop` or `*sparkShellSidebarFooter`.
87
+ */
88
+ declare class SparkShellTabDirective {
89
+ readonly templateRef: TemplateRef<unknown>;
90
+ /** The tab's header label. */
91
+ readonly header: _angular_core.InputSignal<string>;
92
+ /** Bootstrap icon name for the header, as in `programUnits.json`. Defaults to a folder. */
93
+ readonly icon: _angular_core.InputSignal<string | undefined>;
94
+ /** Replaces the icon+label header entirely, for a header that needs its own markup. */
95
+ readonly headerTemplate: _angular_core.InputSignal<TemplateRef<unknown> | null>;
96
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkShellTabDirective, never>;
97
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SparkShellTabDirective, "[sparkShellTab]", never, { "header": { "alias": "sparkShellTab"; "required": true; "isSignal": true; }; "icon": { "alias": "sparkShellTabIcon"; "required": false; "isSignal": true; }; "headerTemplate": { "alias": "sparkShellTabHeader"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
98
+ }
99
+ /**
100
+ * A sidebar accordion tab in the shape the menu renders it. Hosts normally contribute tabs with
101
+ * `*sparkShellTab`; this is the same thing as data, for a host that computes its tabs.
102
+ */
103
+ interface SparkSidebarTab {
104
+ readonly header: string;
105
+ readonly icon?: string;
106
+ readonly headerTemplate?: TemplateRef<unknown> | null;
107
+ readonly content: TemplateRef<unknown>;
108
+ }
109
+ /** Sidebar, at the very bottom. No default. */
110
+ declare class SparkShellSidebarFooterDirective {
111
+ readonly templateRef: TemplateRef<unknown>;
112
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkShellSidebarFooterDirective, never>;
113
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SparkShellSidebarFooterDirective, "[sparkShellSidebarFooter]", never, {}, {}, never, never, true, never>;
114
+ }
115
+ /** Main region, above the projected content (the host's `<router-outlet>`). No default. */
116
+ declare class SparkShellMainHeaderDirective {
117
+ readonly templateRef: TemplateRef<unknown>;
118
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkShellMainHeaderDirective, never>;
119
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SparkShellMainHeaderDirective, "[sparkShellMainHeader]", never, {}, {}, never, never, true, never>;
120
+ }
121
+
122
+ /**
123
+ * The application frame: topbar + sidebar + main, wrapping ng-bootstrap's `bs-shell` (whose
124
+ * `mp-shell` web component owns ALL responsive behavior — breakpoints, the overlay drawer,
125
+ * dismiss-on-navigate — in CSS; nothing here re-derives a pixel width). The sidebar renders the
126
+ * server-driven program-units menu; the host projects its `<router-outlet>` as the default
127
+ * content and customizes the chrome through the `*sparkShell*` slots (see `spark-shell-slots.ts`
128
+ * for the doctrine: an omitted slot renders its default, and the menu itself is never a slot).
129
+ *
130
+ * ```html
131
+ * <spark-shell title="My App">
132
+ * <spark-auth-bar *sparkShellTopbarEnd />
133
+ * <router-outlet />
134
+ * </spark-shell>
135
+ * ```
136
+ *
137
+ * The one piece of state the shell keeps is the toggler↔drawer mirror: the built-in hamburger is
138
+ * hidden (`::part(hamburger)`) in favor of a `bs-navbar-toggler` in the topbar, so the shell
139
+ * listens to `statechange` to keep the toggler's icon truthful in `auto` mode and only forces
140
+ * `show`/`hide` on explicit toggles.
141
+ *
142
+ * Theming: the chrome colors are CSS custom properties with the classic dark-sidebar defaults —
143
+ * `--spark-shell-topbar-bg`, `--spark-shell-sidebar-bg`, `--spark-shell-main-bg` — overridable on
144
+ * the `<spark-shell>` element. `sidebarTheme` flips the sidebar's `data-bs-theme` (which is what
145
+ * recolors the accordion internals across the shadow boundary) together with its default palette.
146
+ */
147
+ declare class SparkShellComponent {
148
+ /** The sidebar heading. Ignored when a `*sparkShellSidebarHeader` slot is supplied. */
149
+ readonly title: _angular_core.InputSignal<string>;
150
+ /** Forwarded to `bs-shell`: below it the sidebar is an overlay drawer. */
151
+ readonly breakpoint: _angular_core.InputSignal<Breakpoint>;
152
+ /**
153
+ * `data-bs-theme` for the sidebar — what flips the accordion's shadow-DOM internals between
154
+ * palettes — plus the matching default background. `null` sets no theme (inherit the page's).
155
+ */
156
+ readonly sidebarTheme: _angular_core.InputSignal<"dark" | "light" | null>;
157
+ /** Forwarded to the menu: any changed value re-fetches the program units. */
158
+ readonly reloadToken: _angular_core.InputSignal<unknown>;
159
+ /** Extra sidebar tabs as data, for hosts that compute them; `*sparkShellTab` is the usual way. */
160
+ readonly sidebarTabs: _angular_core.InputSignal<readonly SparkSidebarTab[]>;
161
+ readonly topbarStartTemplate: _angular_core.InputSignal<TemplateRef<unknown> | null>;
162
+ readonly topbarEndTemplate: _angular_core.InputSignal<TemplateRef<unknown> | null>;
163
+ readonly sidebarHeaderTemplate: _angular_core.InputSignal<TemplateRef<unknown> | null>;
164
+ readonly sidebarTopTemplate: _angular_core.InputSignal<TemplateRef<unknown> | null>;
165
+ readonly sidebarFooterTemplate: _angular_core.InputSignal<TemplateRef<unknown> | null>;
166
+ readonly mainHeaderTemplate: _angular_core.InputSignal<TemplateRef<unknown> | null>;
167
+ private readonly topbarStartSlot;
168
+ private readonly topbarEndSlot;
169
+ private readonly sidebarHeaderSlot;
170
+ private readonly sidebarTopSlot;
171
+ private readonly sidebarFooterSlot;
172
+ private readonly mainHeaderSlot;
173
+ protected readonly topbarStartTpl: _angular_core.Signal<TemplateRef<unknown> | null>;
174
+ protected readonly topbarEndTpl: _angular_core.Signal<TemplateRef<unknown> | null>;
175
+ protected readonly sidebarHeaderTpl: _angular_core.Signal<TemplateRef<unknown> | null>;
176
+ protected readonly sidebarTopTpl: _angular_core.Signal<TemplateRef<unknown> | null>;
177
+ protected readonly sidebarFooterTpl: _angular_core.Signal<TemplateRef<unknown> | null>;
178
+ protected readonly mainHeaderTpl: _angular_core.Signal<TemplateRef<unknown> | null>;
179
+ /**
180
+ * Extra accordion tabs, forwarded to the menu so IT creates the `<bs-accordion-tab>` elements —
181
+ * the only way they share the generated groups' single-open behavior (see
182
+ * `SparkShellTabDirective`). Data-supplied tabs come first, then projected ones in declaration
183
+ * order.
184
+ */
185
+ private readonly tabSlots;
186
+ protected readonly tabs: _angular_core.Signal<readonly SparkSidebarTab[]>;
187
+ protected readonly shellState: _angular_core.WritableSignal<BsShellState>;
188
+ protected readonly isSidebarVisible: _angular_core.WritableSignal<boolean>;
189
+ protected toggleSidebar(open: boolean): void;
190
+ protected onShellToggle(detail: ShellStateChangeEventDetail): void;
191
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkShellComponent, never>;
192
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SparkShellComponent, "spark-shell", never, { "title": { "alias": "title"; "required": false; "isSignal": true; }; "breakpoint": { "alias": "breakpoint"; "required": false; "isSignal": true; }; "sidebarTheme": { "alias": "sidebarTheme"; "required": false; "isSignal": true; }; "reloadToken": { "alias": "reloadToken"; "required": false; "isSignal": true; }; "sidebarTabs": { "alias": "sidebarTabs"; "required": false; "isSignal": true; }; "topbarStartTemplate": { "alias": "topbarStartTemplate"; "required": false; "isSignal": true; }; "topbarEndTemplate": { "alias": "topbarEndTemplate"; "required": false; "isSignal": true; }; "sidebarHeaderTemplate": { "alias": "sidebarHeaderTemplate"; "required": false; "isSignal": true; }; "sidebarTopTemplate": { "alias": "sidebarTopTemplate"; "required": false; "isSignal": true; }; "sidebarFooterTemplate": { "alias": "sidebarFooterTemplate"; "required": false; "isSignal": true; }; "mainHeaderTemplate": { "alias": "mainHeaderTemplate"; "required": false; "isSignal": true; }; }, {}, ["topbarStartSlot", "topbarEndSlot", "sidebarHeaderSlot", "sidebarTopSlot", "sidebarFooterSlot", "mainHeaderSlot", "tabSlots"], ["*"], true, never>;
193
+ }
194
+
195
+ /**
196
+ * The server-driven navigation menu: an accordion of program-unit groups fetched from
197
+ * `GET /spark/program-units`, which the server has already filtered to what the caller's rights
198
+ * allow. Hosts write ZERO router links for navigation — every group, unit, icon, label and link
199
+ * comes from `programUnits.json`; content around the menu belongs in `<spark-shell>`'s slots,
200
+ * and a host tempted to hand-write a unit anchor should add a unit to `programUnits.json`
201
+ * instead.
202
+ *
203
+ * Because the response is caller-scoped it must be re-fetched when the caller changes: the
204
+ * component tracks the optional `SPARK_AUTH_STATE` signal (supplied by ng-spark-auth's
205
+ * `provideSparkAuth()`, or by the app's own auth stack) and reloads on every change. Without a
206
+ * provider it fetches once. `reloadToken` is the manual escape hatch (any changed value triggers
207
+ * a reload), and `reload()` the imperative one.
208
+ *
209
+ * Usually rendered by `<spark-shell>`; exported standalone for hosts that own their own layout.
210
+ */
211
+ declare class SparkProgramUnitsComponent {
212
+ private readonly sparkService;
213
+ private readonly authState;
214
+ /** Any changed value triggers a reload — for apps whose auth state isn't a provided signal. */
215
+ readonly reloadToken: _angular_core.InputSignal<unknown>;
216
+ /**
217
+ * Extra tabs to render after the generated groups, normally forwarded by `<spark-shell>` from
218
+ * its `*sparkShellTab` directives. They must be rendered by THIS template — see
219
+ * `SparkShellTabDirective` for why a host-declared `<bs-accordion-tab>` cannot work.
220
+ */
221
+ readonly extraTabs: _angular_core.InputSignal<readonly SparkSidebarTab[]>;
222
+ protected readonly groups: _angular_core.WritableSignal<ProgramUnitGroup[]>;
223
+ constructor();
224
+ /** Re-fetches the menu. The response is already rights-filtered per caller. */
225
+ reload(): Promise<void>;
226
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkProgramUnitsComponent, never>;
227
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SparkProgramUnitsComponent, "spark-program-units", never, { "reloadToken": { "alias": "reloadToken"; "required": false; "isSignal": true; }; "extraTabs": { "alias": "extraTabs"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
228
+ }
229
+
230
+ /**
231
+ * The culture switcher: a `bs-select` over `SparkLanguageService`'s languages, persisting the
232
+ * choice. Renders nothing when the app declares one language (or none), so hosts can include it
233
+ * unconditionally — `<spark-shell>`'s topbar does exactly that as its trailing default.
234
+ */
235
+ declare class SparkLanguageSelectorComponent {
236
+ protected readonly lang: SparkLanguageService;
237
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkLanguageSelectorComponent, never>;
238
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SparkLanguageSelectorComponent, "spark-language-selector", never, {}, {}, never, never, true, never>;
239
+ }
240
+
241
+ export { SparkLanguageSelectorComponent, SparkProgramUnitsComponent, SparkShellComponent, SparkShellMainHeaderDirective, SparkShellSidebarFooterDirective, SparkShellSidebarHeaderDirective, SparkShellSidebarTopDirective, SparkShellTabDirective, SparkShellTopbarEndDirective, SparkShellTopbarStartDirective };
242
+ export type { SparkSidebarTab };
@@ -1,4 +1,4 @@
1
- import { InjectionToken, Provider } from '@angular/core';
1
+ import { InjectionToken, Signal, Provider } from '@angular/core';
2
2
 
3
3
  interface SparkConfig {
4
4
  baseUrl: string;
@@ -6,7 +6,22 @@ interface SparkConfig {
6
6
  declare const SPARK_CONFIG: InjectionToken<SparkConfig>;
7
7
  declare const defaultSparkConfig: SparkConfig;
8
8
 
9
+ /**
10
+ * A signal that changes whenever the authenticated user changes — the bridge that lets ng-spark
11
+ * components react to sign-in/out without a dependency on `@mintplayer/ng-spark-auth` (no
12
+ * dependency exists between the two packages, in either direction, on purpose).
13
+ *
14
+ * `@mintplayer/ng-spark-auth`'s `provideSparkAuth()` supplies it from `SparkAuthService.user`;
15
+ * an app with its own auth stack provides any signal that changes on sign-in/out. Consumers
16
+ * inject it `{ optional: true }` — absent, auth-sensitive data (the program-units menu) is
17
+ * fetched once and never re-fetched.
18
+ *
19
+ * The signal's VALUE is deliberately opaque (`unknown`): consumers only track it for change,
20
+ * never read it — what "the user" looks like belongs to the auth package.
21
+ */
22
+ declare const SPARK_AUTH_STATE: InjectionToken<Signal<unknown>>;
23
+
9
24
  declare function provideSpark(config?: Partial<SparkConfig>): Provider[];
10
25
 
11
- export { SPARK_CONFIG, defaultSparkConfig, provideSpark };
26
+ export { SPARK_AUTH_STATE, SPARK_CONFIG, defaultSparkConfig, provideSpark };
12
27
  export type { SparkConfig };