@mintplayer/ng-spark 22.17.0 → 22.19.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.
- package/fesm2022/mintplayer-ng-spark-models.mjs +177 -4
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-pipes.mjs +37 -1
- package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-create.mjs +18 -4
- package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs +88 -6
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +125 -16
- package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +1 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-services.mjs +122 -40
- package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mintplayer-ng-spark-models.d.ts +91 -1
- package/types/mintplayer-ng-spark-pipes.d.ts +24 -1
- package/types/mintplayer-ng-spark-po-create.d.ts +8 -1
- package/types/mintplayer-ng-spark-po-edit.d.ts +33 -1
- package/types/mintplayer-ng-spark-po-form.d.ts +69 -6
- package/types/mintplayer-ng-spark-services.d.ts +52 -5
|
@@ -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;
|
|
@@ -22,7 +28,33 @@ declare class SparkPoEditComponent {
|
|
|
22
28
|
constructor();
|
|
23
29
|
private onParamsChange;
|
|
24
30
|
initFormData(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Resolves a nested AsDetail type by CLR name, for rebuilding the nested PO wire shape on save.
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ <b>`detailTypes` first, catalogue second.</b> The catalogue from `/spark/types` is
|
|
35
|
+
* <b>Query-gated</b>, and an AsDetail row type usually has no rights of its own — nobody grants
|
|
36
|
+
* `Query/GateSettings`, because a gate is edited through the Repository that owns it. So the
|
|
37
|
+
* catalogue does not contain it, the resolver returned undefined, and the save quietly took the
|
|
38
|
+
* scalar branch: the attribute went out as a raw dict under `value` instead of a nested PO under
|
|
39
|
+
* `object`, the server could not map it, and `EntityMapper`'s conversion `catch` swallowed the
|
|
40
|
+
* failure. The save reported success and wrote nothing.
|
|
41
|
+
* <para>
|
|
42
|
+
* `detailTypes` is the parent's own copy of its row types, carried on the type definition for
|
|
43
|
+
* exactly this reason (#385 fixed the rendering half; this is the save half). It is gated on the
|
|
44
|
+
* parent's right, which is the right that governs editing the row anyway.
|
|
45
|
+
* </para>
|
|
46
|
+
*/
|
|
25
47
|
private resolveEntityType;
|
|
48
|
+
/**
|
|
49
|
+
* The attributes this page will read values from when it builds the save.
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ <b>Overlaid before filtering, not after loading.</b> A refresh hook can reveal an attribute
|
|
52
|
+
* that was hidden when the object was loaded, and the ordinary reason it does so is that the
|
|
53
|
+
* attribute has just become required. Filtering on the loaded state left such an attribute out of
|
|
54
|
+
* both {@link initFormData} and the save payload, so the user filled in a field whose value was
|
|
55
|
+
* then dropped on the floor and refused by the server as missing — with no way out of the form.
|
|
56
|
+
* The overlay is the form's, bound two-way, so the two halves cannot drift again.
|
|
57
|
+
*/
|
|
26
58
|
getEditableAttributes(): _mintplayer_ng_spark_models.EntityAttributeDefinition[];
|
|
27
59
|
onSave(): Promise<void>;
|
|
28
60
|
onCancel(): void;
|
|
@@ -56,6 +56,17 @@ declare class RefreshCoordinator {
|
|
|
56
56
|
trigger(attributeName: string): Promise<void>;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* A refresh a form embedded in another form is asking its host to issue on its behalf.
|
|
61
|
+
*
|
|
62
|
+
* `immediate` mirrors the distinction the host's own editors make: a discrete editor fires on
|
|
63
|
+
* change, free text only marks itself pending and fires on blur.
|
|
64
|
+
*/
|
|
65
|
+
interface NestedTriggerRequest {
|
|
66
|
+
/** The full path the server addresses, e.g. `Gate.ProjectMode`. */
|
|
67
|
+
path: string;
|
|
68
|
+
immediate: boolean;
|
|
69
|
+
}
|
|
59
70
|
declare class SparkPoFormComponent {
|
|
60
71
|
private readonly sparkService;
|
|
61
72
|
private readonly translations;
|
|
@@ -76,6 +87,18 @@ declare class SparkPoFormComponent {
|
|
|
76
87
|
objectTypeId: _angular_core.InputSignal<string | undefined>;
|
|
77
88
|
/** The id of the object being edited; absent for a create. */
|
|
78
89
|
objectId: _angular_core.InputSignal<string | undefined>;
|
|
90
|
+
/**
|
|
91
|
+
* Set when this form renders a single embedded AsDetail object inside a host form: the name of
|
|
92
|
+
* the attribute holding it, e.g. `"Gate"`. Presence of a prefix switches the form from issuing
|
|
93
|
+
* its own refreshes to reporting them upward, because an embedded object is not something the
|
|
94
|
+
* server can be asked about on its own — the request must describe, and be authorized against,
|
|
95
|
+
* the object that owns it.
|
|
96
|
+
*/
|
|
97
|
+
triggerPathPrefix: _angular_core.InputSignal<string | null>;
|
|
98
|
+
/** Raised instead of self-issuing when {@link triggerPathPrefix} is set. */
|
|
99
|
+
nestedTriggerRequested: _angular_core.OutputEmitterRef<NestedTriggerRequest>;
|
|
100
|
+
/** Blur counterpart, flushing a trigger the host only marked pending. */
|
|
101
|
+
nestedTriggerBlurred: _angular_core.OutputEmitterRef<string>;
|
|
79
102
|
/**
|
|
80
103
|
* What the last refresh changed about each attribute's presentation, keyed by attribute name.
|
|
81
104
|
*
|
|
@@ -83,8 +106,17 @@ declare class SparkPoFormComponent {
|
|
|
83
106
|
* `entityType` identity and `SparkService` caches nothing, so re-setting it would re-issue every
|
|
84
107
|
* reference query and lookup fetch on every refresh; mutating it in place would not re-render at
|
|
85
108
|
* all.
|
|
109
|
+
*
|
|
110
|
+
* ⚠️ A `model`, not a plain signal, because the page that owns the save has to see it. This form
|
|
111
|
+
* renders from the overlaid attribute list; `spark-po-edit` and `spark-po-create` build the
|
|
112
|
+
* payload from their own copy of the same filter. While the overlay stopped here, those two
|
|
113
|
+
* filtered on the attribute's state *as loaded* — so an attribute a refresh hook revealed was
|
|
114
|
+
* rendered, filled in by the user, and then silently left out of the save. When the same hook also
|
|
115
|
+
* made it required (the ordinary case — a field appears *because* it is now needed) the server
|
|
116
|
+
* refused the save for the very field the user had just filled, and the form became a dead end
|
|
117
|
+
* with no way forward from inside it.
|
|
86
118
|
*/
|
|
87
|
-
refreshOverlay: _angular_core.
|
|
119
|
+
refreshOverlay: _angular_core.ModelSignal<RefreshOverlay>;
|
|
88
120
|
isRefreshing: _angular_core.WritableSignal<boolean>;
|
|
89
121
|
colors: typeof Color;
|
|
90
122
|
referenceOptions: _angular_core.WritableSignal<Record<string, QueryResultItem[]>>;
|
|
@@ -167,9 +199,8 @@ declare class SparkPoFormComponent {
|
|
|
167
199
|
/**
|
|
168
200
|
* The single funnel every scalar / boolean / inline-cell edit passes through.
|
|
169
201
|
*
|
|
170
|
-
* `attr` is optional
|
|
171
|
-
*
|
|
172
|
-
* no refresh can fire.
|
|
202
|
+
* `attr` is optional because some editors have nothing useful to say about which attribute moved.
|
|
203
|
+
* A caller that knows should always say so — without it no refresh can fire.
|
|
173
204
|
*/
|
|
174
205
|
onFieldChange(attr?: EntityAttributeDefinition): void;
|
|
175
206
|
/**
|
|
@@ -178,8 +209,27 @@ declare class SparkPoFormComponent {
|
|
|
178
209
|
* addressing scheme being invented for it.
|
|
179
210
|
*/
|
|
180
211
|
onInlineCellChange(attr: EntityAttributeDefinition, rowIndex: number, col: EntityAttributeDefinition): void;
|
|
181
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Which embedded target the in-flight refresh belongs to, if any.
|
|
214
|
+
*
|
|
215
|
+
* ⚠️ Tagged, not a nullable `rowIndex`. The two arms write to different places — a row inside
|
|
216
|
+
* `formData`, versus the modal's working copy in `asDetailFormData` — and an index that happens
|
|
217
|
+
* to be absent is not the same statement as "this attribute holds one object". With a nullable
|
|
218
|
+
* index, a row refresh carrying a bad index would fall silently into the object arm.
|
|
219
|
+
*/
|
|
182
220
|
private pendingNestedTrigger;
|
|
221
|
+
/**
|
|
222
|
+
* A trigger inside a single embedded AsDetail object, raised by the modal's recursive form.
|
|
223
|
+
*
|
|
224
|
+
* The child form does not issue the request itself, for two reasons that both bite. Its
|
|
225
|
+
* `entityType` is the embedded type, so a payload built there describes `GateSettings` while the
|
|
226
|
+
* request must describe the Repository that owns it. And nested AsDetail types carry no rights of
|
|
227
|
+
* their own — the server authorizes a nested refresh on the OWNING type — so a child-issued
|
|
228
|
+
* request would be asking the wrong question of security.json.
|
|
229
|
+
*/
|
|
230
|
+
onEmbeddedTrigger(attributeName: string, event: NestedTriggerRequest): void;
|
|
231
|
+
/** Blur counterpart of {@link onEmbeddedTrigger}, for the child's free-text editors. */
|
|
232
|
+
onEmbeddedTriggerBlur(path: string): void;
|
|
183
233
|
/**
|
|
184
234
|
* Applies a refresh that ran against a detail row: the row's own values, and the column metadata
|
|
185
235
|
* for the grid it lives in.
|
|
@@ -195,6 +245,8 @@ declare class SparkPoFormComponent {
|
|
|
195
245
|
/** Blur handler for free-text editors — sends the refresh their keystrokes only marked pending. */
|
|
196
246
|
onFieldBlur(attr: EntityAttributeDefinition): void;
|
|
197
247
|
private noteChange;
|
|
248
|
+
/** `Gate.ProjectMode` — the index-free counterpart of the inline grid's `Jobs[1].Kind`. */
|
|
249
|
+
private nestedTriggerPath;
|
|
198
250
|
private canRefresh;
|
|
199
251
|
/**
|
|
200
252
|
* Per-instance, never a service: the retry-action modal renders its own `spark-po-form`, and a
|
|
@@ -203,6 +255,16 @@ declare class SparkPoFormComponent {
|
|
|
203
255
|
*/
|
|
204
256
|
protected readonly refreshCoordinator: RefreshCoordinator;
|
|
205
257
|
private buildRefreshPayload;
|
|
258
|
+
/**
|
|
259
|
+
* The values to post, with the open modal's working copy substituted for the attribute it edits.
|
|
260
|
+
*
|
|
261
|
+
* ⚠️ Without this the server receives the embedded object as it was when the modal opened — `{}`
|
|
262
|
+
* for a gate that has never been set — and the hook decides against values the user cannot see it
|
|
263
|
+
* ignoring. `formData` deliberately does not hold the in-progress edit (cancelling the modal has
|
|
264
|
+
* to discard it), so the refresh has to reach into `asDetailFormData` instead. Only the attribute
|
|
265
|
+
* under edit is substituted; everything else is the form's own state.
|
|
266
|
+
*/
|
|
267
|
+
private embeddedValues;
|
|
206
268
|
/**
|
|
207
269
|
* Folds replaced option lists into the signals the editors already read, so a refreshed dropdown
|
|
208
270
|
* renders through the same path as a loaded one.
|
|
@@ -269,7 +331,7 @@ declare class SparkPoFormComponent {
|
|
|
269
331
|
removeArrayItem(attr: EntityAttributeDefinition, index: number): Promise<void>;
|
|
270
332
|
onAsDetailReorder(attr: EntityAttributeDefinition, event: CdkDragDrop<Record<string, any>[]>): void;
|
|
271
333
|
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>;
|
|
334
|
+
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; }; "triggerPathPrefix": { "alias": "triggerPathPrefix"; "required": false; "isSignal": true; }; "refreshOverlay": { "alias": "refreshOverlay"; "required": false; "isSignal": true; }; }, { "formData": "formDataChange"; "save": "save"; "cancel": "cancel"; "nestedTriggerRequested": "nestedTriggerRequested"; "nestedTriggerBlurred": "nestedTriggerBlurred"; "refreshOverlay": "refreshOverlayChange"; }, never, never, true, never>;
|
|
273
335
|
}
|
|
274
336
|
|
|
275
337
|
/**
|
|
@@ -348,3 +410,4 @@ declare class SparkLookupPickerComponent {
|
|
|
348
410
|
}
|
|
349
411
|
|
|
350
412
|
export { SparkLookupPickerComponent, SparkPoFormComponent, SparkReferencePickerComponent };
|
|
413
|
+
export type { NestedTriggerRequest };
|
|
@@ -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
|
|
150
|
-
* DELETE
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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 };
|