@mintplayer/ng-spark 22.16.0 → 22.18.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.
@@ -53,6 +53,16 @@ interface PersistentObject {
53
53
  name: string;
54
54
  objectTypeId: string;
55
55
  breadcrumb?: string;
56
+ /**
57
+ * Optimistic-concurrency token -- RavenDB's change vector for the document behind this object.
58
+ * The server stamps it on every `/po/load` and expects it back on update.
59
+ *
60
+ * ⚠️ The check is opt-in BY PRESENCE of this field. Dropping it does not fail loudly; it makes
61
+ * the save unconditional, so two people editing the same record both succeed and the second
62
+ * silently overwrites the first. That is what used to happen here: `SparkClient` round-tripped
63
+ * the token and got a 409, the browser did not and got last-write-wins, on the same endpoint.
64
+ */
65
+ etag?: string;
56
66
  attributes: PersistentObjectAttribute[];
57
67
  /** Per-row edit/delete affordances; undefined = fall back to type-level permissions. */
58
68
  can?: PersistentObjectPermissions;
@@ -250,6 +260,23 @@ interface EntityType {
250
260
  * `queries` or `alias`.
251
261
  */
252
262
  detailTypes?: EntityType[];
263
+ /**
264
+ * Whether adding or removing a row of this type asks the server first.
265
+ *
266
+ * Absent or false — the default — keeps the purely client-side behaviour: New pushes a blank row,
267
+ * Delete splices it out, and the parent's save is the first the server hears of either. On, the
268
+ * grid calls `newObject` before showing a row and `deleteRow` before removing one, so
269
+ * `OnNewAsync` can default it and the delete hook can refuse.
270
+ *
271
+ * ⚠️ Read it off the **row type**, never the parent: the type that owns the hooks owns the
272
+ * decision. For a row type resolved out of `detailTypes` the flag survives the server-side
273
+ * pruning, so both sources agree.
274
+ *
275
+ * ⚠️ It governs the round trip and nothing else. It is not a permission, and it is outside the
276
+ * model hash, so nothing that gates a write may be conditional on it — the save path enforces the
277
+ * row type's New/Edit/Delete rights on every embedded collection whether this is on or off.
278
+ */
279
+ serverSideRowLifecycle?: boolean;
253
280
  }
254
281
 
255
282
  type SparkQueryRenderMode = 'Pagination' | 'VirtualScrolling';
@@ -535,6 +562,70 @@ interface StreamingErrorMessage {
535
562
  }
536
563
  type StreamingMessage = StreamingSnapshotMessage | StreamingPatchMessage | StreamingErrorMessage;
537
564
 
565
+ /**
566
+ * Conversions between the wire format for a timestamp and what an `<input type="datetime-local">`
567
+ * or `<input type="date">` will accept.
568
+ *
569
+ * A `DateTimeOffset` in Spark means an **instant**. The wire carries a full ISO-8601 string with an
570
+ * offset (`2026-12-31T23:59:00-08:00`); an HTML date/time control speaks only bare local wall clock
571
+ * (`2026-12-31T23:59`). Neither end can consume the other's format, so both directions need
572
+ * converting, and the browser is deliberately the side that does it: its timezone rules are kept
573
+ * fresh by the operating system, while a container's are frozen at image build time.
574
+ *
575
+ * Assigning a wire value straight to a `datetime-local` input does not throw and does not warn -- the
576
+ * control simply renders **blank**, and saving the untouched form then writes that blank back.
577
+ */
578
+ /**
579
+ * Parses a wire timestamp. Returns `null` for anything unusable, matching the contract of
580
+ * `parsedDate` / `queryCellValue` -- a bad value renders as empty rather than as `Invalid Date`.
581
+ */
582
+ declare function parseWireDate(value: unknown): Date | null;
583
+ /**
584
+ * Wire value -> the `yyyy-MM-ddTHH:mm` a `datetime-local` input needs, in the **viewer's** zone.
585
+ *
586
+ * The conversion is date-sensitive, not a fixed shift: a value is rendered using the zone's offset
587
+ * *on that value's own date*, so a timestamp in July and one in January convert differently in a
588
+ * zone that observes DST.
589
+ */
590
+ declare function toDateTimeLocalInput(value: unknown): string;
591
+ /** Wire value -> the `yyyy-MM-dd` a `date` input needs, in the viewer's zone. */
592
+ declare function toDateInput(value: unknown): string;
593
+ /**
594
+ * `yyyy-MM-ddTHH:mm` from the control -> a complete ISO-8601 string carrying the viewer's offset
595
+ * **for the entered date**.
596
+ *
597
+ * Building the `Date` from its parts rather than parsing the string is what makes this correct: it
598
+ * lets the browser apply its own timezone rules for that date, including DST. Using the *current*
599
+ * offset instead would be wrong for any value outside the present season -- entering a January date
600
+ * during summer would record an offset an hour off, and the stored instant would be an hour wrong.
601
+ *
602
+ * Across a DST discontinuity the browser resolves the wall clock itself, taking the offset in force
603
+ * before a fall-back and after a spring-forward. That is specified ECMAScript behaviour, and the
604
+ * server's conversion helper deliberately reproduces the same choice so the two cannot drift.
605
+ */
606
+ declare function fromDateTimeLocalInput(local: unknown): string | null;
607
+ /** `yyyy-MM-dd` from a `date` control -> an ISO string at local midnight with the viewer's offset. */
608
+ declare function fromDateInput(local: unknown): string | null;
609
+ /**
610
+ * Compares two wire timestamps **by instant**, which is what a `DateTimeOffset` means in Spark.
611
+ *
612
+ * Round-tripping a value through a date control legitimately rewrites its text -- a document holding
613
+ * `2026-12-31T23:59:00-08:00` comes back as `2027-01-01T08:59:00+01:00` from a viewer in Brussels --
614
+ * so a string comparison would mark every untouched date as edited and send a needless write on every
615
+ * save. Both of those name the same moment.
616
+ *
617
+ * Note this is the *opposite* of the trap on the server side, where `DateTimeOffset.Equals` compares
618
+ * the instant and silently hides a destroyed offset. Here comparing by instant is the correct thing:
619
+ * we are asking "did the user change this value", not "is the offset intact".
620
+ */
621
+ declare function wireDatesEqual(a: unknown, b: unknown): boolean;
622
+ /** True for the attribute data types that are edited through a native date/time control. */
623
+ declare function isDateDataType(dataType: string | null | undefined): dataType is 'date' | 'datetime';
624
+ /** Wire -> control, picking the right shape for the data type. */
625
+ declare function toDateInputValue(dataType: string | null | undefined, value: unknown): string;
626
+ /** Control -> wire, picking the right shape for the data type. */
627
+ declare function fromDateInputValue(dataType: string | null | undefined, value: unknown): string | null;
628
+
538
629
  /**
539
630
  * Resolves an `EntityType` by its CLR type name (e.g. `"HR.Entities.Address"`).
540
631
  * Callers typically close over a list they already hold. `getEntityTypes()` is NOT cached --
@@ -612,6 +703,22 @@ declare function resolvedBreadcrumb(value: unknown, typeName?: string): string |
612
703
  * reference-query options page. Prefixed to avoid colliding with a real attribute name.
613
704
  */
614
705
  declare const AS_DETAIL_BREADCRUMBS_KEY = "__sparkBreadcrumbs";
706
+ /**
707
+ * Reserved key under which a flattened row keeps the **wire** values of its date cells, keyed by
708
+ * attribute name.
709
+ *
710
+ * A date cell is edited through a native control that speaks only a bare local wall clock, so
711
+ * flattening converts the stored instant into the viewer's zone and saving converts it back. That
712
+ * round trip preserves the instant but necessarily rewrites the **offset** to the viewer's. For a
713
+ * cell the user actually edited that is correct — the offset is not business data. For one nobody
714
+ * touched it is pure loss: merely opening a row and saving would relabel a Seattle timestamp as a
715
+ * Brussels one.
716
+ *
717
+ * Keeping the original lets {@link dictToNestedPo} send back exactly what it was given whenever the
718
+ * instant is unchanged. Safe to carry for the same reason as the other reserved keys: the rebuild
719
+ * walks the entity type's attributes, never the dict's keys.
720
+ */
721
+ declare const AS_DETAIL_ORIGINAL_DATES_KEY = "__sparkOriginalDates";
615
722
  /**
616
723
  * Whether a flattened-row key is one this module reserved rather than a model attribute.
617
724
  *
@@ -774,5 +881,5 @@ interface EvaluableAttribute {
774
881
  */
775
882
  declare function evaluateRules(attr: EvaluableAttribute, value: any): RuleFailure[];
776
883
 
777
- export { AS_DETAIL_BREADCRUMBS_KEY, AS_DETAIL_ROW_KEY, AS_DETAIL_SELF_BREADCRUMB_KEY, ELookupDisplayType, EReferenceDisplayType, ShowedOn, applyOverlay, currentLanguage, dictToNestedPo, evaluateRules, filterQueryActions, hasShowedOnFlag, isPersistentObject, isQueryRow, isReservedAsDetailKey, mergeRefreshValues, nestedPoToDict, nestedPoToDisplayRow, overlayFromResponse, parseSelectionRule, resolveTranslation, resolvedBreadcrumb, selectionModeFor, selfBreadcrumb, valueFor };
884
+ export { AS_DETAIL_BREADCRUMBS_KEY, AS_DETAIL_ORIGINAL_DATES_KEY, AS_DETAIL_ROW_KEY, AS_DETAIL_SELF_BREADCRUMB_KEY, ELookupDisplayType, EReferenceDisplayType, ShowedOn, applyOverlay, currentLanguage, dictToNestedPo, evaluateRules, filterQueryActions, fromDateInput, fromDateInputValue, fromDateTimeLocalInput, hasShowedOnFlag, isDateDataType, isPersistentObject, isQueryRow, isReservedAsDetailKey, mergeRefreshValues, nestedPoToDict, nestedPoToDisplayRow, overlayFromResponse, parseSelectionRule, parseWireDate, resolveTranslation, resolvedBreadcrumb, selectionModeFor, selfBreadcrumb, toDateInput, toDateInputValue, toDateTimeLocalInput, valueFor, wireDatesEqual };
778
885
  export type { AttributeGroup, AttributeOverlay, AttributeTab, CustomActionDefinition, EntityAttributeDefinition, EntityPermissions, EntityType, EntityTypeResolver, EvaluableAttribute, LookupReference, LookupReferenceListItem, LookupReferenceValue, PersistentObject, PersistentObjectAttribute, PersistentObjectPermissions, ProgramUnit, ProgramUnitGroup, ProgramUnitsConfiguration, QueryColumn, QueryResult, QueryResultItem, QueryResultItemValue, RefreshOverlay, RefreshedOption, RetryActionPayload, RetryActionResult, RuleFailure, SparkCellColumn, SparkQuery, SparkQueryRenderMode, SparkQuerySortColumn, SparkRow, SparkSelectionMode, StreamingErrorMessage, StreamingMessage, StreamingPatchItem, StreamingPatchMessage, StreamingSnapshotMessage, TranslatedString, ValidationError, ValidationErrorResponse, ValidationRule };
@@ -21,6 +21,29 @@ declare class InputTypePipe implements PipeTransform {
21
21
  static ɵpipe: i0.ɵɵPipeDeclaration<InputTypePipe, "inputType", true>;
22
22
  }
23
23
 
24
+ /**
25
+ * Parses a wire value into a `Date`, or `null` when it is absent or unparseable.
26
+ *
27
+ * Exists so the detail page and the grid agree. The grid gets its `Date` from `queryCellValue`, which
28
+ * needs a cell and a column and so cannot be reused on a detail attribute; before this, the detail page
29
+ * had no parsing step at all and printed the raw ISO string — `2026-12-31T23:59:00-08:00` in a
30
+ * definition list, while the grid showed `01/01/2027, 08:59` for the same document.
31
+ *
32
+ * The parsing contract is deliberately identical to `queryCellValue`'s: `new Date(...)`, and an
33
+ * unparseable value yields `null` so the caller can fall back to its own text rather than rendering
34
+ * "Invalid Date".
35
+ *
36
+ * Note what `new Date(...)` does to an offset, because it is the whole reason this is a separate step:
37
+ * it collapses the value to an instant and forgets the offset it was written with. That is correct for
38
+ * display — a viewer wants the moment in their own zone — but it means a `Date` can never be the thing
39
+ * you reach for when you need the *originating* wall clock. Read that from the wire value instead.
40
+ */
41
+ declare class ParsedDatePipe implements PipeTransform {
42
+ transform(value: unknown): Date | null;
43
+ static ɵfac: i0.ɵɵFactoryDeclaration<ParsedDatePipe, never>;
44
+ static ɵpipe: i0.ɵɵPipeDeclaration<ParsedDatePipe, "parsedDate", true>;
45
+ }
46
+
24
47
  declare class AttributeValuePipe implements PipeTransform {
25
48
  transform(attrName: string, item: PersistentObject | null, entityType: EntityType | null, lookupRefOptions: Record<string, LookupReference>, allEntityTypes: EntityType[]): any;
26
49
  private formatAsDetailValue;
@@ -265,5 +288,5 @@ declare class QueryReferenceChipsPipe implements PipeTransform {
265
288
  static ɵpipe: i0.ɵɵPipeDeclaration<QueryReferenceChipsPipe, "queryReferenceChips", true>;
266
289
  }
267
290
 
268
- export { ArrayValuePipe, AsDetailCellValuePipe, AsDetailColumnsPipe, AsDetailDisplayValuePipe, AsDetailTypePipe, AttributeValuePipe, CanCreateDetailRowPipe, CanDeleteDetailRowPipe, CanEditDetailRowPipe, ErrorForAttributePipe, IconNamePipe, InlineRefOptionsPipe, InputTypePipe, LookupDisplayTypePipe, LookupDisplayValuePipe, LookupOptionsPipe, QueryCellValuePipe, QueryReferenceChipsPipe, RawAttributeValuePipe, ReferenceAttrValuePipe, ReferenceChipsPipe, ReferenceDisplayValuePipe, ReferenceLinkRoutePipe, ResolveTranslationPipe, RouterLinkPipe, TranslateKeyPipe };
291
+ export { ArrayValuePipe, AsDetailCellValuePipe, AsDetailColumnsPipe, AsDetailDisplayValuePipe, AsDetailTypePipe, AttributeValuePipe, CanCreateDetailRowPipe, CanDeleteDetailRowPipe, CanEditDetailRowPipe, ErrorForAttributePipe, IconNamePipe, InlineRefOptionsPipe, InputTypePipe, LookupDisplayTypePipe, LookupDisplayValuePipe, LookupOptionsPipe, ParsedDatePipe, QueryCellValuePipe, QueryReferenceChipsPipe, RawAttributeValuePipe, ReferenceAttrValuePipe, ReferenceChipsPipe, ReferenceDisplayValuePipe, ReferenceLinkRoutePipe, ResolveTranslationPipe, RouterLinkPipe, TranslateKeyPipe };
269
292
  export type { AsDetailCellValue, ReferenceChip };
@@ -1,5 +1,5 @@
1
1
  import * as _mintplayer_ng_spark_models from '@mintplayer/ng-spark/models';
2
- import { PersistentObject, EntityType, ValidationError } from '@mintplayer/ng-spark/models';
2
+ import { PersistentObject, EntityType, RefreshOverlay, ValidationError } from '@mintplayer/ng-spark/models';
3
3
  import * as _angular_core from '@angular/core';
4
4
  import { Color } from '@mintplayer/ng-bootstrap';
5
5
 
@@ -13,6 +13,8 @@ declare class SparkPoCreateComponent {
13
13
  entityType: _angular_core.WritableSignal<EntityType | null>;
14
14
  type: _angular_core.WritableSignal<string>;
15
15
  formData: _angular_core.WritableSignal<Record<string, any>>;
16
+ /** Bound two-way to the form. See {@link getEditableAttributes}. */
17
+ refreshOverlay: _angular_core.WritableSignal<RefreshOverlay>;
16
18
  validationErrors: _angular_core.WritableSignal<ValidationError[]>;
17
19
  isSaving: _angular_core.WritableSignal<boolean>;
18
20
  private allEntityTypes;
@@ -20,6 +22,11 @@ declare class SparkPoCreateComponent {
20
22
  constructor();
21
23
  private onParamsChange;
22
24
  initFormData(): void;
25
+ /**
26
+ * ⚠️ Overlaid before filtering — same reason as <c>spark-po-edit</c>, and worse here: this list is
27
+ * the <em>only</em> source of the create payload, so an attribute a refresh hook revealed was not
28
+ * merely sent stale, it was absent from the new object entirely.
29
+ */
23
30
  getEditableAttributes(): _mintplayer_ng_spark_models.EntityAttributeDefinition[];
24
31
  onSave(): Promise<void>;
25
32
  onCancel(): void;
@@ -1,5 +1,5 @@
1
1
  import * as _mintplayer_ng_spark_models from '@mintplayer/ng-spark/models';
2
- import { PersistentObject, EntityType, ValidationError } from '@mintplayer/ng-spark/models';
2
+ import { PersistentObject, EntityType, RefreshOverlay, ValidationError } from '@mintplayer/ng-spark/models';
3
3
  import * as _angular_core from '@angular/core';
4
4
  import { Color } from '@mintplayer/ng-bootstrap';
5
5
 
@@ -7,6 +7,7 @@ declare class SparkPoEditComponent {
7
7
  private readonly route;
8
8
  private readonly router;
9
9
  private readonly sparkService;
10
+ private readonly language;
10
11
  saved: _angular_core.OutputEmitterRef<PersistentObject>;
11
12
  cancelled: _angular_core.OutputEmitterRef<void>;
12
13
  colors: typeof Color;
@@ -15,6 +16,11 @@ declare class SparkPoEditComponent {
15
16
  type: string;
16
17
  id: string;
17
18
  formData: _angular_core.WritableSignal<Record<string, any>>;
19
+ /**
20
+ * Bound two-way to the form, which is where refreshes land. Read by
21
+ * {@link getEditableAttributes} so the save sees the object the user was actually shown.
22
+ */
23
+ refreshOverlay: _angular_core.WritableSignal<RefreshOverlay>;
18
24
  validationErrors: _angular_core.WritableSignal<ValidationError[]>;
19
25
  isSaving: _angular_core.WritableSignal<boolean>;
20
26
  private allEntityTypes;
@@ -23,6 +29,16 @@ declare class SparkPoEditComponent {
23
29
  private onParamsChange;
24
30
  initFormData(): void;
25
31
  private resolveEntityType;
32
+ /**
33
+ * The attributes this page will read values from when it builds the save.
34
+ *
35
+ * ⚠️ <b>Overlaid before filtering, not after loading.</b> A refresh hook can reveal an attribute
36
+ * that was hidden when the object was loaded, and the ordinary reason it does so is that the
37
+ * attribute has just become required. Filtering on the loaded state left such an attribute out of
38
+ * both {@link initFormData} and the save payload, so the user filled in a field whose value was
39
+ * then dropped on the floor and refused by the server as missing — with no way out of the form.
40
+ * The overlay is the form's, bound two-way, so the two halves cannot drift again.
41
+ */
26
42
  getEditableAttributes(): _mintplayer_ng_spark_models.EntityAttributeDefinition[];
27
43
  onSave(): Promise<void>;
28
44
  onCancel(): void;
@@ -83,8 +83,17 @@ declare class SparkPoFormComponent {
83
83
  * `entityType` identity and `SparkService` caches nothing, so re-setting it would re-issue every
84
84
  * reference query and lookup fetch on every refresh; mutating it in place would not re-render at
85
85
  * all.
86
+ *
87
+ * ⚠️ A `model`, not a plain signal, because the page that owns the save has to see it. This form
88
+ * renders from the overlaid attribute list; `spark-po-edit` and `spark-po-create` build the
89
+ * payload from their own copy of the same filter. While the overlay stopped here, those two
90
+ * filtered on the attribute's state *as loaded* — so an attribute a refresh hook revealed was
91
+ * rendered, filled in by the user, and then silently left out of the save. When the same hook also
92
+ * made it required (the ordinary case — a field appears *because* it is now needed) the server
93
+ * refused the save for the very field the user had just filled, and the form became a dead end
94
+ * with no way forward from inside it.
86
95
  */
87
- refreshOverlay: _angular_core.WritableSignal<RefreshOverlay>;
96
+ refreshOverlay: _angular_core.ModelSignal<RefreshOverlay>;
88
97
  isRefreshing: _angular_core.WritableSignal<boolean>;
89
98
  colors: typeof Color;
90
99
  referenceOptions: _angular_core.WritableSignal<Record<string, QueryResultItem[]>>;
@@ -97,6 +106,21 @@ declare class SparkPoFormComponent {
97
106
  showAsDetailModal: _angular_core.WritableSignal<boolean>;
98
107
  editingArrayIndex: _angular_core.WritableSignal<number | null>;
99
108
  asDetailPermissions: _angular_core.WritableSignal<Record<string, EntityPermissions>>;
109
+ /**
110
+ * What the server said when it refused to construct or release a row.
111
+ *
112
+ * A local signal rather than the `validationErrors` input, because this is the one class of error
113
+ * the form raises on its own behalf: `validationErrors` is owned by the host (po-edit / po-create)
114
+ * and arrives from a save. Cleared at the start of each lifecycle call, so a stale refusal never
115
+ * outlives the click that caused it.
116
+ *
117
+ * Keyed by attribute name so a message renders against the grid it came from. A single global
118
+ * alert would have been less code and worse: a form with two detail grids would show one grid's
119
+ * refusal above the other, and the user's next click is on the grid, not the top of the page.
120
+ */
121
+ rowLifecycleErrors: _angular_core.WritableSignal<Record<string, ValidationError[]>>;
122
+ /** The refusal to render under `attrName`'s grid, if any. */
123
+ rowLifecycleErrorsFor(attrName: string): ValidationError[];
100
124
  asDetailReferenceOptions: _angular_core.WritableSignal<Record<string, Record<string, QueryResultItem[]>>>;
101
125
  ELookupDisplayType: typeof ELookupDisplayType;
102
126
  EReferenceDisplayType: typeof EReferenceDisplayType;
@@ -204,13 +228,57 @@ declare class SparkPoFormComponent {
204
228
  openAsDetailEditor(attr: EntityAttributeDefinition): void;
205
229
  saveAsDetailObject(): void;
206
230
  closeAsDetailModal(): void;
207
- addInlineRow(attr: EntityAttributeDefinition): void;
208
- addArrayItem(attr: EntityAttributeDefinition): void;
231
+ /**
232
+ * Whether `attr`'s row type asks the server before a row is added or removed.
233
+ *
234
+ * Read off the ROW type, never the parent. `asDetailTypes` already resolves it from the Query-gated
235
+ * catalogue first and the parent's `detailTypes` second, and the flag survives the server-side
236
+ * pruning of the embedded copy, so both sources give the same answer.
237
+ */
238
+ private roundTripsRowLifecycle;
239
+ /** The type id to address the row type by on the wire. */
240
+ private rowTypeId;
241
+ /**
242
+ * How to name the PARENT's type on the wire.
243
+ *
244
+ * ⚠️ The fallback must be the type **id**, never `clrType`. The server resolves this through
245
+ * `ModelLoader.ResolveEntityType`, which accepts a GUID or a declared alias and nothing else — so
246
+ * a CLR name resolves to null and the request is refused, indistinguishably from an unknown type.
247
+ * The user would see "the server refused this change" on a perfectly ordinary Add.
248
+ *
249
+ * It is a live path, not a theoretical one: `spark-po-create` binds no `parentType` (there is no
250
+ * parent route segment on a create page), so every host that creates an object with an opted-in
251
+ * detail grid lands here.
252
+ */
253
+ private parentTypeForWire;
254
+ /**
255
+ * Builds the dict a new row starts from: `{}` as before, or the object the server constructed.
256
+ *
257
+ * Returns null when the server refused — the caller must then add nothing, which is the whole
258
+ * difference between a veto and a message.
259
+ */
260
+ private buildNewRow;
261
+ /**
262
+ * Asks whether a stored row may be removed. True when it may — including every case where there
263
+ * is nothing to ask about: a type that has not opted in, a parent that has never been saved, or a
264
+ * row with no key yet because it was added in this same editing session and never persisted.
265
+ */
266
+ private mayRemoveRow;
267
+ /**
268
+ * Turns a rejected lifecycle call into something the user can read.
269
+ *
270
+ * The envelope puts validation errors under `result`, the same place the save path reads them
271
+ * from — reading `error.error.errors` matched nothing and made every veto silent.
272
+ */
273
+ private clearRowLifecycleError;
274
+ private captureRowLifecycleError;
275
+ addInlineRow(attr: EntityAttributeDefinition): Promise<void>;
276
+ addArrayItem(attr: EntityAttributeDefinition): Promise<void>;
209
277
  editArrayItem(attr: EntityAttributeDefinition, index: number): void;
210
- removeArrayItem(attr: EntityAttributeDefinition, index: number): void;
278
+ removeArrayItem(attr: EntityAttributeDefinition, index: number): Promise<void>;
211
279
  onAsDetailReorder(attr: EntityAttributeDefinition, event: CdkDragDrop<Record<string, any>[]>): void;
212
280
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkPoFormComponent, 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>;
281
+ 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; }; "refreshOverlay": { "alias": "refreshOverlay"; "required": false; "isSignal": true; }; }, { "formData": "formDataChange"; "save": "save"; "cancel": "cancel"; "refreshOverlay": "refreshOverlayChange"; }, never, never, true, never>;
214
282
  }
215
283
 
216
284
  /**
@@ -3,6 +3,7 @@ import { RetryActionPayload, RetryActionResult, TranslatedString, StreamingMessa
3
3
  import { Observable } from 'rxjs';
4
4
  import { SortColumn } from '@mintplayer/pagination';
5
5
  import { SafeHtml } from '@angular/platform-browser';
6
+ import { HttpInterceptorFn, HttpFeature, HttpFeatureKind } from '@angular/common/http';
6
7
 
7
8
  declare class RetryActionService {
8
9
  private resolveRetry;
@@ -41,12 +42,44 @@ declare class SparkStreamingService {
41
42
  static ɵprov: i0.ɵɵInjectableDeclaration<SparkStreamingService>;
42
43
  }
43
44
 
45
+ /**
46
+ * Context for {@link SparkService.newObject}. Everything is optional: a standalone New sends an
47
+ * empty body, and an AsDetail row on a parent that has never been saved sends no `parentId` —
48
+ * which is the honest answer, not a gap. The server hands the hook a null parent rather than
49
+ * trusting the client's copy of one.
50
+ */
51
+ interface NewObjectOptions {
52
+ /** Name of the parent's AsDetail attribute the row is for; absent for a standalone New. */
53
+ asDetailAttribute?: string;
54
+ /** The parent's entity type — required whenever `asDetailAttribute` is set. */
55
+ parentType?: string;
56
+ /** The parent's id, absent while the parent is itself unsaved. */
57
+ parentId?: string;
58
+ /** Free-form arguments, e.g. which variant a New menu chose. */
59
+ parameters?: Record<string, string>;
60
+ }
61
+ /** Context for {@link SparkService.deleteRow}. Every field is required — see `DeleteRow.cs`. */
62
+ interface DeleteRowOptions {
63
+ asDetailAttribute: string;
64
+ parentType: string;
65
+ parentId: string;
66
+ /** The row's `__sparkRowKey`, as it arrived from the server. */
67
+ rowKey: string;
68
+ parameters?: Record<string, string>;
69
+ }
44
70
  declare class SparkService {
45
71
  #private;
46
72
  private readonly config;
47
73
  private readonly baseUrl;
48
74
  private readonly http;
49
75
  private readonly retryActionService;
76
+ /**
77
+ * How many prompts one request will answer before giving up. Matches the .NET client's
78
+ * `SparkClient.MaxRetryDepth`, and for the same reason: this is a termination bound, not a
79
+ * capacity one. Far above any real conversation (Fleet's longest is two) and far below anything
80
+ * that could hide a spin.
81
+ */
82
+ private static readonly MAX_RETRY_DEPTH;
50
83
  private readonly dispatcher;
51
84
  getEntityTypes(): Promise<EntityType[]>;
52
85
  getEntityType(id: string): Promise<EntityType>;
@@ -101,6 +134,31 @@ declare class SparkService {
101
134
  * path form the inline validation errors use — `Jobs[2].ProfessionId`.
102
135
  */
103
136
  refresh(type: string, data: Partial<PersistentObject>, triggeredBy: string): Promise<PersistentObject>;
137
+ /**
138
+ * Asks the server to construct a new object of `type`, so `OnNewAsync` can default it.
139
+ *
140
+ * Writes nothing — the object comes back unsaved, and for an AsDetail row the parent still owns
141
+ * the save. Only called for a row type whose `serverSideRowLifecycle` is on; every other type
142
+ * keeps building its blank row locally, which is why switching the flag off costs no request.
143
+ *
144
+ * ⚠️ The row comes back **keyed**: the server constructs the CLR instance, so the row-key field
145
+ * initializer runs and `po.id` carries the key. Flattening it with `nestedPoToDict` therefore
146
+ * populates `__sparkRowKey`, and the row is matchable on save from the moment it is added — the
147
+ * whole reason this round-trip is worth a request.
148
+ */
149
+ newObject(type: string, options?: NewObjectOptions): Promise<PersistentObject>;
150
+ /**
151
+ * Asks whether a stored row may leave its parent's AsDetail collection.
152
+ *
153
+ * Deletes nothing: a resolved promise means the caller may splice the row out of the collection
154
+ * it is editing, and the removal is persisted with the parent. A hook that refuses rejects with a
155
+ * 400 whose `error.error.result.errors` carries the readable reason, exactly like a save.
156
+ *
157
+ * A POST — as everything is now. It used to be worth explaining: the framework's real delete route
158
+ * was a catch-all that would have swallowed a sibling DELETE. The catch-all is gone with the rest
159
+ * of the route variables.
160
+ */
161
+ deleteRow(type: string, options: DeleteRowOptions): Promise<void>;
104
162
  delete(type: string, id: string): Promise<void>;
105
163
  getCustomActions(objectTypeId: string): Promise<CustomActionDefinition[]>;
106
164
  /**
@@ -123,8 +181,16 @@ declare class SparkService {
123
181
  updateLookupReferenceValue(name: string, key: string, value: LookupReferenceValue): Promise<LookupReferenceValue>;
124
182
  deleteLookupReferenceValue(name: string, key: string): Promise<void>;
125
183
  private postWithEnvelope;
126
- private putWithEnvelope;
127
- private deleteWithEnvelope;
184
+ /**
185
+ * A read: posts the request, and returns the response **as-is** rather than unwrapping an
186
+ * envelope.
187
+ *
188
+ * The read endpoints moved from GET to POST so their hooks could prompt, but their success
189
+ * responses are still bare — a `PersistentObject`, a `QueryResult`. Only the 449 is enveloped,
190
+ * because a retry has nowhere else to live. So this shares the retry loop with the mutating calls
191
+ * and nothing else.
192
+ */
193
+ private sendRead;
128
194
  private sendWithEnvelope;
129
195
  private handleEnvelopeRetryError;
130
196
  static ɵfac: i0.ɵɵFactoryDeclaration<SparkService, never>;
@@ -222,4 +288,35 @@ declare class SparkQueryActionsService {
222
288
  static ɵprov: i0.ɵɵInjectableDeclaration<SparkQueryActionsService>;
223
289
  }
224
290
 
225
- export { RetryActionService, SparkIconRegistry, SparkLanguageService, SparkQueryActionsService, SparkService, SparkStreamingService };
291
+ /**
292
+ * The header carrying the viewer's IANA timezone id, e.g. `Europe/Brussels`.
293
+ * Must stay in lockstep with `RequestTimeZoneResolver.HeaderName` on the server.
294
+ */
295
+ declare const SPARK_TIMEZONE_HEADER = "X-Spark-Timezone";
296
+ /**
297
+ * Returns the browser's IANA zone id, or `null` when the environment cannot name one.
298
+ *
299
+ * An id is deliberately sent rather than a numeric offset: an offset is only valid at one instant,
300
+ * so a server asked to render "next Tuesday at 09:00" from an offset captured today gets it wrong
301
+ * across a DST boundary. A zone id carries the rules, not one sample of them.
302
+ */
303
+ declare function resolveBrowserTimeZone(): string | null;
304
+ /**
305
+ * Attaches {@link SPARK_TIMEZONE_HEADER} to same-origin requests.
306
+ *
307
+ * **This does not participate in reading or writing timestamps.** A `DateTimeOffset` means an
308
+ * instant, and the browser converts in both directions on those paths. The header exists for work
309
+ * the server starts on its own — a scheduled export, a notification email — where there is no
310
+ * browser in the loop to do the converting but the output still has to read as local time.
311
+ */
312
+ declare const sparkTimezoneInterceptor: HttpInterceptorFn;
313
+ /**
314
+ * `provideHttpClient(...withSparkTimezone())` — registers {@link sparkTimezoneInterceptor}.
315
+ *
316
+ * Spreadable alongside `withSparkAuth()`:
317
+ * `provideHttpClient(...withSparkAuth(), ...withSparkTimezone())`.
318
+ */
319
+ declare function withSparkTimezone(): HttpFeature<HttpFeatureKind>[];
320
+
321
+ export { RetryActionService, SPARK_TIMEZONE_HEADER, SparkIconRegistry, SparkLanguageService, SparkQueryActionsService, SparkService, SparkStreamingService, resolveBrowserTimeZone, sparkTimezoneInterceptor, withSparkTimezone };
322
+ export type { DeleteRowOptions, NewObjectOptions };