@mintplayer/ng-spark 22.17.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;
@@ -552,6 +562,70 @@ interface StreamingErrorMessage {
552
562
  }
553
563
  type StreamingMessage = StreamingSnapshotMessage | StreamingPatchMessage | StreamingErrorMessage;
554
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
+
555
629
  /**
556
630
  * Resolves an `EntityType` by its CLR type name (e.g. `"HR.Entities.Address"`).
557
631
  * Callers typically close over a list they already hold. `getEntityTypes()` is NOT cached --
@@ -629,6 +703,22 @@ declare function resolvedBreadcrumb(value: unknown, typeName?: string): string |
629
703
  * reference-query options page. Prefixed to avoid colliding with a real attribute name.
630
704
  */
631
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";
632
722
  /**
633
723
  * Whether a flattened-row key is one this module reserved rather than a model attribute.
634
724
  *
@@ -791,5 +881,5 @@ interface EvaluableAttribute {
791
881
  */
792
882
  declare function evaluateRules(attr: EvaluableAttribute, value: any): RuleFailure[];
793
883
 
794
- 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 };
795
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[]>>;
@@ -269,7 +278,7 @@ declare class SparkPoFormComponent {
269
278
  removeArrayItem(attr: EntityAttributeDefinition, index: number): Promise<void>;
270
279
  onAsDetailReorder(attr: EntityAttributeDefinition, event: CdkDragDrop<Record<string, any>[]>): void;
271
280
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<SparkPoFormComponent, never>;
272
- 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>;
273
282
  }
274
283
 
275
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;
@@ -72,6 +73,13 @@ declare class SparkService {
72
73
  private readonly baseUrl;
73
74
  private readonly http;
74
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;
75
83
  private readonly dispatcher;
76
84
  getEntityTypes(): Promise<EntityType[]>;
77
85
  getEntityType(id: string): Promise<EntityType>;
@@ -146,8 +154,9 @@ declare class SparkService {
146
154
  * it is editing, and the removal is persisted with the parent. A hook that refuses rejects with a
147
155
  * 400 whose `error.error.result.errors` carries the readable reason, exactly like a save.
148
156
  *
149
- * A POST, because the framework's real delete route is a catch-all that would swallow a sibling
150
- * DELETE — see `DeleteRow.cs`.
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.
151
160
  */
152
161
  deleteRow(type: string, options: DeleteRowOptions): Promise<void>;
153
162
  delete(type: string, id: string): Promise<void>;
@@ -172,8 +181,16 @@ declare class SparkService {
172
181
  updateLookupReferenceValue(name: string, key: string, value: LookupReferenceValue): Promise<LookupReferenceValue>;
173
182
  deleteLookupReferenceValue(name: string, key: string): Promise<void>;
174
183
  private postWithEnvelope;
175
- private putWithEnvelope;
176
- 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;
177
194
  private sendWithEnvelope;
178
195
  private handleEnvelopeRetryError;
179
196
  static ɵfac: i0.ɵɵFactoryDeclaration<SparkService, never>;
@@ -271,5 +288,35 @@ declare class SparkQueryActionsService {
271
288
  static ɵprov: i0.ɵɵInjectableDeclaration<SparkQueryActionsService>;
272
289
  }
273
290
 
274
- 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 };
275
322
  export type { DeleteRowOptions, NewObjectOptions };