@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.
- 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 +70 -5
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +163 -11
- 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 +146 -36
- package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mintplayer-ng-spark-models.d.ts +108 -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 +17 -1
- package/types/mintplayer-ng-spark-po-form.d.ts +73 -5
- package/types/mintplayer-ng-spark-services.d.ts +100 -3
|
@@ -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.
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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 };
|