@mintplayer/ng-spark 22.24.0 → 22.25.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/README.md +15 -0
- package/fesm2022/mintplayer-ng-spark-client-operations.mjs +5 -3
- package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-contributions.mjs +721 -0
- package/fesm2022/mintplayer-ng-spark-contributions.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-grid.mjs +131 -4
- package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-history.mjs +1 -14
- package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs +41 -2
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-moderation.mjs +4 -4
- package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-panels.mjs +9 -1
- package/fesm2022/mintplayer-ng-spark-panels.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-pipes.mjs +39 -3
- package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +15 -6
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs +924 -16
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +78 -20
- package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +60 -5
- package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs +28 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-shell.mjs +31 -13
- package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark.mjs +4 -1
- package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
- package/package.json +7 -2
- package/types/mintplayer-ng-spark-client-operations.d.ts +7 -1
- package/types/mintplayer-ng-spark-contributions.d.ts +231 -0
- package/types/mintplayer-ng-spark-grid.d.ts +35 -3
- package/types/mintplayer-ng-spark-history.d.ts +3 -10
- package/types/mintplayer-ng-spark-models.d.ts +60 -3
- package/types/mintplayer-ng-spark-panels.d.ts +36 -1
- package/types/mintplayer-ng-spark-pipes.d.ts +12 -1
- package/types/mintplayer-ng-spark-po-detail.d.ts +4 -1
- package/types/mintplayer-ng-spark-po-edit.d.ts +305 -1
- package/types/mintplayer-ng-spark-po-form.d.ts +64 -15
- package/types/mintplayer-ng-spark-query-list.d.ts +26 -1
- package/types/mintplayer-ng-spark-renderers.d.ts +44 -2
- package/types/mintplayer-ng-spark-shell.d.ts +24 -9
- package/types/mintplayer-ng-spark.d.ts +12 -0
|
@@ -72,6 +72,11 @@ export interface PersistentObject {
|
|
|
72
72
|
* hook again at submit and refuses a disabled action with 403.
|
|
73
73
|
*/
|
|
74
74
|
disabledActions?: string[];
|
|
75
|
+
/**
|
|
76
|
+
* Read-side facts an add-on attached to this object on load, keyed by the add-on (e.g.
|
|
77
|
+
* `{ contribution: { own: true } }` on a contribution row). Server-written; never sent back.
|
|
78
|
+
*/
|
|
79
|
+
metadata?: Record<string, any>;
|
|
75
80
|
}
|
|
76
81
|
export interface PersistentObjectAttribute {
|
|
77
82
|
id: string;
|
|
@@ -203,6 +208,19 @@ export declare enum EReferenceDisplayType {
|
|
|
203
208
|
/** Renders a readonly textbox + "…" button that opens a searchable modal grid picker. */
|
|
204
209
|
Modal = "Modal"
|
|
205
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* When changing an attribute asks the server to reshape the object (mirrors the .NET
|
|
213
|
+
* `ERefreshTrigger`, serialized PascalCase like {@link EReferenceDisplayType}).
|
|
214
|
+
*
|
|
215
|
+
* - `None` — no refresh; the same as absent.
|
|
216
|
+
* - `Auto` — free text refreshes on blur; discrete editors (lookup, reference, boolean, date,
|
|
217
|
+
* datetime, enum, color) refresh immediately.
|
|
218
|
+
* - `ValueChanged` — refreshes on every change; free text is debounced (300 ms) and flushed by
|
|
219
|
+
* blur or save.
|
|
220
|
+
* - `Blur` — refreshes on blur. A discrete editor never blurs meaningfully, so there it acts as
|
|
221
|
+
* `ValueChanged` (and `--spark-verify-model` warns).
|
|
222
|
+
*/
|
|
223
|
+
export type RefreshTrigger = 'None' | 'Auto' | 'ValueChanged' | 'Blur';
|
|
206
224
|
export interface EntityAttributeDefinition {
|
|
207
225
|
id: string;
|
|
208
226
|
name: string;
|
|
@@ -231,12 +249,13 @@ export interface EntityAttributeDefinition {
|
|
|
231
249
|
/** For array AsDetail attributes: when true, rows can be drag-reordered (order = array position) */
|
|
232
250
|
isSortable?: boolean;
|
|
233
251
|
/**
|
|
234
|
-
* When
|
|
235
|
-
* `/spark/po/{objectTypeId}/refresh` and applies the reshaped result as an overlay
|
|
252
|
+
* When set (and not `'None'`), changing this attribute's value posts the in-progress object to
|
|
253
|
+
* `/spark/po/{objectTypeId}/refresh` and applies the reshaped result as an overlay; the value
|
|
254
|
+
* decides when (see {@link RefreshTrigger}). Absent means no refresh.
|
|
236
255
|
* Schema-only by design — it never travels on a PersistentObjectAttribute, so a client
|
|
237
256
|
* cannot claim a trigger the model did not declare.
|
|
238
257
|
*/
|
|
239
|
-
triggersRefresh?:
|
|
258
|
+
triggersRefresh?: RefreshTrigger;
|
|
240
259
|
/** For LookupReference attributes, specifies the lookup reference type name */
|
|
241
260
|
lookupReferenceType?: string;
|
|
242
261
|
/**
|
|
@@ -658,6 +677,8 @@ export interface EntityPermissions {
|
|
|
658
677
|
/** History (#460): `History/T` (list and read revisions) and `Revert/T` together with `Edit/T`. */
|
|
659
678
|
canViewHistory?: boolean;
|
|
660
679
|
canRevert?: boolean;
|
|
680
|
+
/** Contributions: `RevertContribution/T` (make a contribution current again; a generated contribution type only). */
|
|
681
|
+
canRevertContribution?: boolean;
|
|
661
682
|
}
|
|
662
683
|
/**
|
|
663
684
|
* The opening message of a stream: the column metadata plus the rows so far.
|
|
@@ -850,6 +871,14 @@ export declare const AS_DETAIL_ORIGINAL_DATES_KEY = "__sparkOriginalDates";
|
|
|
850
871
|
* own breadcrumb as though they were fields.
|
|
851
872
|
*/
|
|
852
873
|
export declare function isReservedAsDetailKey(key: string): boolean;
|
|
874
|
+
/**
|
|
875
|
+
* Reserved key under which a flattened row keeps the {@link PersistentObject.metadata} the server
|
|
876
|
+
* attached to it on load (an add-on's read-side facts — contributions' `{ contribution: { own } }`).
|
|
877
|
+
* Never sent back: `dictToNestedPo` walks the entity type's attributes, never the dict's keys.
|
|
878
|
+
*/
|
|
879
|
+
export declare const AS_DETAIL_METADATA_KEY = "__sparkMetadata";
|
|
880
|
+
/** The metadata the server attached to a flattened row under `key`, or undefined. */
|
|
881
|
+
export declare function rowMetadata<T = any>(row: Record<string, any> | null | undefined, key: string): T | undefined;
|
|
853
882
|
/**
|
|
854
883
|
* Like {@link nestedPoToDict}, but for the read-only detail display path. In addition to each
|
|
855
884
|
* attribute's value it preserves the server-resolved per-reference `breadcrumb` under
|
|
@@ -869,6 +898,34 @@ export declare function nestedPoToDisplayRow(po: PersistentObject | null | undef
|
|
|
869
898
|
* handled recursively.
|
|
870
899
|
*/
|
|
871
900
|
export declare function dictToNestedPo(dict: Record<string, any> | null | undefined, entityType: EntityType, resolve: EntityTypeResolver): PersistentObject;
|
|
901
|
+
/**
|
|
902
|
+
* The one answer to "do these two reads hold the same content?", shared by History's revision diff
|
|
903
|
+
* and po-edit's three-way conflict merge.
|
|
904
|
+
*
|
|
905
|
+
* Two reads of identical content are NOT byte-identical: their envelopes carry per-read fields
|
|
906
|
+
* (etags, breadcrumbs, row keys, the wire values a flattened row keeps for its date cells) that
|
|
907
|
+
* differ between reads, and comparing them reports every AsDetail attribute as changed. Both
|
|
908
|
+
* functions below reduce a value to its content first. They come in two shapes because the two
|
|
909
|
+
* callers hold two shapes: History compares wire attributes, po-edit compares the flat form dict.
|
|
910
|
+
*/
|
|
911
|
+
/**
|
|
912
|
+
* A comparable form of a wire attribute's content.
|
|
913
|
+
*
|
|
914
|
+
* Nested objects (AsDetail rows, references rendered as objects) are reduced to their attributes'
|
|
915
|
+
* values: their envelopes carry per-read fields (etags, breadcrumbs, row keys) that differ between two
|
|
916
|
+
* reads of identical content and would report every AsDetail attribute as changed.
|
|
917
|
+
*/
|
|
918
|
+
export declare function comparableValue(attribute: PersistentObjectAttribute | undefined): string;
|
|
919
|
+
/**
|
|
920
|
+
* A comparable form of a value in the flat form dict ({@link nestedPoToDict}'s shape).
|
|
921
|
+
*
|
|
922
|
+
* The reserved keys a flattened row carries (its row key, its breadcrumb, the wire values of its
|
|
923
|
+
* dates) are dropped, and object keys are sorted, so two flattenings of the same content compare
|
|
924
|
+
* equal whatever order their attributes arrived in. `undefined` and `null` are the same absence.
|
|
925
|
+
*/
|
|
926
|
+
export declare function comparableFormValue(value: unknown): string;
|
|
927
|
+
/** Whether two form-dict values hold the same content (see {@link comparableFormValue}). */
|
|
928
|
+
export declare function formValuesEqual(a: unknown, b: unknown): boolean;
|
|
872
929
|
/** Whether `showedOn` offers the action on a query: `"query"` or `"both"`, in any casing. */
|
|
873
930
|
export declare function isShowedOnQuery(showedOn: string | undefined | null): boolean;
|
|
874
931
|
/** Whether `showedOn` offers the action on a detail page: `"detail"` or `"both"`, in any casing. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { InjectionToken, Provider, Type } from "@angular/core";
|
|
2
|
-
import { EntityPermissions, EntityType, PersistentObject, SparkDeletedFilter, SparkQuery } from "@mintplayer/ng-spark/models";
|
|
2
|
+
import { EntityPermissions, EntityType, PersistentObject, QueryResultItem, SparkDeletedFilter, SparkQuery } from "@mintplayer/ng-spark/models";
|
|
3
3
|
/**
|
|
4
4
|
* What a detail-page extension is told about the object on screen (#460).
|
|
5
5
|
*
|
|
@@ -84,6 +84,41 @@ export interface SparkQueryListAction {
|
|
|
84
84
|
/** Priority in the action bar's overflow ordering; defaults to 60. */
|
|
85
85
|
priority?: number;
|
|
86
86
|
}
|
|
87
|
+
/** What a grid row action (`SPARK_QUERY_ROW_ACTIONS`) is asked whether it is offered. */
|
|
88
|
+
export interface SparkQueryRowActionScope {
|
|
89
|
+
query: SparkQuery;
|
|
90
|
+
entityType: EntityType;
|
|
91
|
+
permissions: EntityPermissions | null;
|
|
92
|
+
/** The grid's soft-deletion mode (null when the grid sends none). */
|
|
93
|
+
deleted: SparkDeletedFilter | null;
|
|
94
|
+
}
|
|
95
|
+
/** What a grid row action runs with: the scope, the one row, and the grid's refresh. */
|
|
96
|
+
export interface SparkQueryRowActionContext extends SparkQueryRowActionScope {
|
|
97
|
+
row: QueryResultItem;
|
|
98
|
+
/** Re-runs the grid's query (page, sort and filters kept). */
|
|
99
|
+
reload(): void;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* An add-on entry of a Spark grid's per-row `⋮` menu (the query page and the sub-query card): an
|
|
103
|
+
* action an add-on package serves on its own route, so it is not in the type's custom-action
|
|
104
|
+
* catalogue (e.g. Contributions' "Revert to this version").
|
|
105
|
+
*
|
|
106
|
+
* `run` executes in the grid's injection context, so it may `inject()` synchronously before its first
|
|
107
|
+
* `await`. A rejection is shown in the grid's alert (the server's message when it sent one).
|
|
108
|
+
*/
|
|
109
|
+
export interface SparkQueryRowAction {
|
|
110
|
+
id: string;
|
|
111
|
+
/** Translation key of the menu label. */
|
|
112
|
+
labelKey: string;
|
|
113
|
+
/** Order among the row menu's entries (custom actions are 10+); defaults to 50. */
|
|
114
|
+
priority?: number;
|
|
115
|
+
isOffered(scope: SparkQueryRowActionScope): boolean;
|
|
116
|
+
run(context: SparkQueryRowActionContext): Promise<void>;
|
|
117
|
+
}
|
|
118
|
+
/** Multi-provider: add-on entries of every Spark grid's row menu. */
|
|
119
|
+
export declare const SPARK_QUERY_ROW_ACTIONS: InjectionToken<SparkQueryRowAction[]>;
|
|
120
|
+
/** Registers grid row actions (multi — every call adds). */
|
|
121
|
+
export declare function provideSparkQueryRowActions(...actions: SparkQueryRowAction[]): Provider[];
|
|
87
122
|
/** Multi-provider: panels under the attributes of the routed detail page. */
|
|
88
123
|
export declare const SPARK_DETAIL_PANELS: InjectionToken<SparkDetailPanel[]>;
|
|
89
124
|
/** Multi-provider: extra buttons in the routed detail page's action bar. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as i0 from "@angular/core";
|
|
2
2
|
import { PipeTransform } from "@angular/core";
|
|
3
3
|
import { ELookupDisplayType, EntityAttributeDefinition, EntityPermissions, EntityType, LookupReference, LookupReferenceValue, PersistentObject, ProgramUnit, QueryColumn, QueryResultItem, SparkRow, TranslatedString, ValidationError } from "@mintplayer/ng-spark/models";
|
|
4
|
+
import { SparkAttributeRendererRegistration, SparkResolvedRowRenderer } from "@mintplayer/ng-spark/renderers";
|
|
4
5
|
export declare class TranslateKeyPipe implements PipeTransform {
|
|
5
6
|
private readonly lang;
|
|
6
7
|
transform(key: string): string;
|
|
@@ -113,11 +114,21 @@ export declare class AsDetailTypePipe implements PipeTransform {
|
|
|
113
114
|
static ɵfac: i0.ɵɵFactoryDeclaration<AsDetailTypePipe, never>;
|
|
114
115
|
static ɵpipe: i0.ɵɵPipeDeclaration<AsDetailTypePipe, "asDetailType", true>;
|
|
115
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* The columns of an AsDetail table: the row type's visible attributes, in order — minus those a
|
|
119
|
+
* registered row renderer draws (`rowComponent`), when the host passes its renderer registry.
|
|
120
|
+
*/
|
|
116
121
|
export declare class AsDetailColumnsPipe implements PipeTransform {
|
|
117
|
-
transform(attr: EntityAttributeDefinition, asDetailTypes: Record<string, EntityType
|
|
122
|
+
transform(attr: EntityAttributeDefinition, asDetailTypes: Record<string, EntityType>, renderers?: readonly SparkAttributeRendererRegistration[] | null): EntityAttributeDefinition[];
|
|
118
123
|
static ɵfac: i0.ɵɵFactoryDeclaration<AsDetailColumnsPipe, never>;
|
|
119
124
|
static ɵpipe: i0.ɵɵPipeDeclaration<AsDetailColumnsPipe, "asDetailColumns", true>;
|
|
120
125
|
}
|
|
126
|
+
/** The row renderer an AsDetail attribute's row type names (`rowComponent`), or null. */
|
|
127
|
+
export declare class AsDetailRowRendererPipe implements PipeTransform {
|
|
128
|
+
transform(attr: EntityAttributeDefinition, asDetailTypes: Record<string, EntityType>, renderers: readonly SparkAttributeRendererRegistration[] | null | undefined): SparkResolvedRowRenderer | null;
|
|
129
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<AsDetailRowRendererPipe, never>;
|
|
130
|
+
static ɵpipe: i0.ɵɵPipeDeclaration<AsDetailRowRendererPipe, "asDetailRowRenderer", true>;
|
|
131
|
+
}
|
|
121
132
|
/** What a cell can resolve to: the column's own type, or `null` when it has no value. */
|
|
122
133
|
export type AsDetailCellValue = string | number | boolean | Date | null;
|
|
123
134
|
/**
|
|
@@ -2,6 +2,7 @@ import * as i0 from "@angular/core";
|
|
|
2
2
|
import { TemplateRef, Type } from "@angular/core";
|
|
3
3
|
import { Color } from "@mintplayer/ng-bootstrap";
|
|
4
4
|
import { SparkLanguageService } from "@mintplayer/ng-spark/services";
|
|
5
|
+
import { SparkResolvedRowRenderer } from "@mintplayer/ng-spark/renderers";
|
|
5
6
|
import { AttributeGroup, AttributeTab, CustomActionDefinition, EntityAttributeDefinition, EntityPermissions, EntityType, LookupReference, PersistentObject, QueryResultItem, SparkDeletedFilter } from "@mintplayer/ng-spark/models";
|
|
6
7
|
import { SparkDetailContext } from "@mintplayer/ng-spark/panels";
|
|
7
8
|
export declare class SparkPoDetailComponent {
|
|
@@ -12,7 +13,7 @@ export declare class SparkPoDetailComponent {
|
|
|
12
13
|
private readonly queryRefresh;
|
|
13
14
|
private readonly attributeRefresh;
|
|
14
15
|
protected readonly lang: SparkLanguageService;
|
|
15
|
-
|
|
16
|
+
protected readonly rendererRegistry: import("@mintplayer/ng-spark/renderers").SparkAttributeRendererRegistration[];
|
|
16
17
|
/**
|
|
17
18
|
* Add-on panels and action-bar buttons (#460, `SPARK_DETAIL_PANELS` / `SPARK_DETAIL_ACTIONS`).
|
|
18
19
|
* Multi-provided, so History, SoftDelete and Moderation can each add theirs to the page
|
|
@@ -110,6 +111,8 @@ export declare class SparkPoDetailComponent {
|
|
|
110
111
|
groupsForTab(tab: AttributeTab): AttributeGroup[];
|
|
111
112
|
attrsForGroup(group: AttributeGroup): EntityAttributeDefinition[];
|
|
112
113
|
getDetailRendererComponent(attr: EntityAttributeDefinition): Type<any> | null;
|
|
114
|
+
/** Inputs of an AsDetail row renderer (`rowComponent`): the row and the object that owns it. */
|
|
115
|
+
getRowRendererInputs(renderer: SparkResolvedRowRenderer, row: Record<string, any>, item: PersistentObject): Record<string, any>;
|
|
113
116
|
getDetailRendererInputs(component: Type<any>, attr: EntityAttributeDefinition, item: PersistentObject): Record<string, any>;
|
|
114
117
|
/** Column renderer for a cell of an AsDetail sub-table (so embedded rows honor `col.renderer` too). */
|
|
115
118
|
getAsDetailCellRendererComponent(col: EntityAttributeDefinition): Type<any> | null;
|
|
@@ -1,16 +1,154 @@
|
|
|
1
1
|
import { Color } from "@mintplayer/ng-bootstrap";
|
|
2
|
-
import { EntityType, PersistentObject, RefreshOverlay, ValidationError } from "@mintplayer/ng-spark/models";
|
|
2
|
+
import { EntityAttributeDefinition, EntityType, EntityTypeResolver, PersistentObject, QueryColumn, QueryResultItem, RefreshOverlay, ValidationError } from "@mintplayer/ng-spark/models";
|
|
3
3
|
import * as i0 from "@angular/core";
|
|
4
|
+
import { ReferenceChip } from "@mintplayer/ng-spark/pipes";
|
|
5
|
+
/**
|
|
6
|
+
* Three-way merge of an edit form after a 409 (contributions PRD §5, Q10).
|
|
7
|
+
*
|
|
8
|
+
* The three sides are all in the form's own flat-dict shape (`nestedPoToDict`): **base** is the
|
|
9
|
+
* object as this page loaded it, **mine** is the form now, **theirs** is the object re-fetched after
|
|
10
|
+
* the server refused the save. Comparing in the form's shape rather than on the wire matters: the
|
|
11
|
+
* form coerces while it loads (an unset string becomes `''`, a date becomes a wall clock), so a
|
|
12
|
+
* wire-shaped base would report every such field as "changed by me" and turn an edit the other
|
|
13
|
+
* user made to it into a false conflict. All three sides go through the same flattening, so only
|
|
14
|
+
* real edits differ.
|
|
15
|
+
*
|
|
16
|
+
* Pure: no Angular, no I/O. Values are compared with `formValuesEqual`, which ignores the per-read
|
|
17
|
+
* fields a flattened row carries (row key, breadcrumb, original dates).
|
|
18
|
+
*/
|
|
19
|
+
/** Which side a conflict is resolved to. */
|
|
20
|
+
export type ConflictSide = 'mine' | 'theirs';
|
|
21
|
+
export interface MergeSchema {
|
|
22
|
+
/** The attributes being merged: the ones the form edits. */
|
|
23
|
+
attributes: EntityAttributeDefinition[];
|
|
24
|
+
/** Resolves AsDetail row types by CLR name, to merge rows per attribute. */
|
|
25
|
+
resolve: EntityTypeResolver;
|
|
26
|
+
}
|
|
27
|
+
export interface MergeConflict {
|
|
28
|
+
/**
|
|
29
|
+
* Where the conflict sits, and the key its choice is given under: `Title`, `Address.Street`,
|
|
30
|
+
* `Lyrics[ko/Kore]` for a whole row, `Lyrics[ko/Kore].Text` for one attribute of a row.
|
|
31
|
+
*/
|
|
32
|
+
path: string;
|
|
33
|
+
/** `value`: both sides changed one attribute differently. `row`: a row keep/remove or add/add clash. */
|
|
34
|
+
kind: 'value' | 'row';
|
|
35
|
+
/** The attribute the values belong to — for a row conflict, the AsDetail list attribute. */
|
|
36
|
+
attribute: EntityAttributeDefinition;
|
|
37
|
+
/** The top-level attribute this sits under (the attribute itself at the root). */
|
|
38
|
+
rootAttribute: EntityAttributeDefinition;
|
|
39
|
+
/** The path of the object this sits in: `''` at the root, `Lyrics[ko/Kore]` inside a row. */
|
|
40
|
+
group: string;
|
|
41
|
+
/** A readable name for the row this sits in, when it is inside one. */
|
|
42
|
+
rowLabel?: string;
|
|
43
|
+
/**
|
|
44
|
+
* For a row conflict, or a value conflict on an embedded (AsDetail) object: the row type's
|
|
45
|
+
* attributes, so the values can be shown cell by cell. Absent when the row type did not resolve.
|
|
46
|
+
*/
|
|
47
|
+
rowAttributes?: EntityAttributeDefinition[];
|
|
48
|
+
/** The row type's name, to filter the server's type-name breadcrumb placeholder. */
|
|
49
|
+
rowTypeName?: string;
|
|
50
|
+
/** The values, in the form's shape. For a row conflict, the row dict, or `undefined` when absent. */
|
|
51
|
+
base: unknown;
|
|
52
|
+
mine: unknown;
|
|
53
|
+
theirs: unknown;
|
|
54
|
+
}
|
|
55
|
+
/** Something the other side changed that the merge took over. */
|
|
56
|
+
export interface TheirChange {
|
|
57
|
+
path: string;
|
|
58
|
+
attribute: EntityAttributeDefinition;
|
|
59
|
+
rootAttribute: EntityAttributeDefinition;
|
|
60
|
+
}
|
|
61
|
+
export interface MergeResult {
|
|
62
|
+
/** The form values to continue editing with. Unresolved conflicts hold THEIR value. */
|
|
63
|
+
merged: Record<string, any>;
|
|
64
|
+
/** Only true conflicts: both sides changed the same thing to different content. */
|
|
65
|
+
conflicts: MergeConflict[];
|
|
66
|
+
/** What the other side changed that was taken over without a conflict. */
|
|
67
|
+
theirChanges: TheirChange[];
|
|
68
|
+
/**
|
|
69
|
+
* Contribution rows (a `[Contribution]` property, contributions M5b) that I changed but that another
|
|
70
|
+
* contributor changed too: theirs won, my edit of them was dropped — say so.
|
|
71
|
+
*/
|
|
72
|
+
contributionNotices: ContributionNotice[];
|
|
73
|
+
}
|
|
74
|
+
/** A contribution row taken from theirs over my own edit, because another contributor wrote it. */
|
|
75
|
+
export interface ContributionNotice {
|
|
76
|
+
/** `Lyrics[en/Latn]`. */
|
|
77
|
+
path: string;
|
|
78
|
+
/** The list attribute. */
|
|
79
|
+
attribute: EntityAttributeDefinition;
|
|
80
|
+
/** A readable name for the row (its breadcrumb, else its key). */
|
|
81
|
+
rowLabel: string;
|
|
82
|
+
}
|
|
83
|
+
/** The key under which the server marks a contribution row (`metadata.contribution`). */
|
|
84
|
+
export declare const CONTRIBUTION_ROW_METADATA = "contribution";
|
|
85
|
+
/**
|
|
86
|
+
* Whether a flattened row is a contribution row whose shown version the caller wrote: `true` / `false`
|
|
87
|
+
* for a contribution row, `undefined` for any other row (the server marks contribution rows on load).
|
|
88
|
+
*/
|
|
89
|
+
export declare function contributionRowIsOwn(row: Record<string, any> | undefined): boolean | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Merges `mine` onto `theirs` against `base`.
|
|
92
|
+
*
|
|
93
|
+
* - An attribute changed only by me keeps mine; changed only by them takes theirs; changed by both
|
|
94
|
+
* to the same content is no conflict; changed by both differently is a conflict.
|
|
95
|
+
* - A read-only attribute always takes theirs, silently: the user cannot have edited it.
|
|
96
|
+
* - AsDetail lists are matched per row by the row key (`[ValueKey]`). A row added, removed or changed
|
|
97
|
+
* on one side only comes from that side. The same key added on both sides with different content,
|
|
98
|
+
* or a row removed on one side and edited on the other, is a row conflict. A row changed on both
|
|
99
|
+
* sides is merged per attribute. The order is theirs, then the rows only I have.
|
|
100
|
+
*
|
|
101
|
+
* `choices` resolves conflicts by path; a conflict without a choice resolves to theirs. Run it once
|
|
102
|
+
* without choices to find the conflicts, and again with the user's choices to build the result —
|
|
103
|
+
* the conflicts reported do not depend on the choices.
|
|
104
|
+
*/
|
|
105
|
+
export declare function mergeThreeWay(base: Record<string, any>, mine: Record<string, any>, theirs: Record<string, any>, schema: MergeSchema, choices?: Readonly<Record<string, ConflictSide>>): MergeResult;
|
|
106
|
+
/**
|
|
107
|
+
* Reference labels by referenced id, for rendering a Reference in the conflict dialog.
|
|
108
|
+
*
|
|
109
|
+
* The form keeps only a reference's id (`nestedPoToDict` drops the breadcrumbs), so a conflict value
|
|
110
|
+
* on its own renders as the raw id. The labels live elsewhere: on the objects as read — the object as
|
|
111
|
+
* loaded and the re-fetched one each carry the server-resolved breadcrumb of every reference they
|
|
112
|
+
* hold, rows included — and, for a reference the user just picked, in the candidate list the picker
|
|
113
|
+
* chose it from (a picker only ever selects from those options). Ids are document ids, unique across
|
|
114
|
+
* collections, so one flat map serves every attribute and every row.
|
|
115
|
+
*/
|
|
116
|
+
export type ReferenceLabels = Record<string, string>;
|
|
117
|
+
/** Adds the labels of every reference in `po` (recursing into AsDetail rows). The first label for an id wins. */
|
|
118
|
+
export declare function addReferenceLabelsOf(po: PersistentObject | null | undefined, into: ReferenceLabels): ReferenceLabels;
|
|
119
|
+
/** Adds the labels of reference candidates (`QueryResultItem.breadcrumb` by `id`). The first label for an id wins. */
|
|
120
|
+
export declare function addReferenceLabelsOfOptions(options: Iterable<readonly QueryResultItem[] | undefined>, into: ReferenceLabels): ReferenceLabels;
|
|
121
|
+
/** A 409 whose merge found true conflicts: everything needed to re-run it with the user's choices. */
|
|
122
|
+
interface PendingConflict {
|
|
123
|
+
/** The object as loaded, for its reference labels. */
|
|
124
|
+
loaded: PersistentObject;
|
|
125
|
+
theirs: PersistentObject;
|
|
126
|
+
base: Record<string, any>;
|
|
127
|
+
mine: Record<string, any>;
|
|
128
|
+
theirsForm: Record<string, any>;
|
|
129
|
+
schema: MergeSchema;
|
|
130
|
+
result: MergeResult;
|
|
131
|
+
}
|
|
4
132
|
export declare class SparkPoEditComponent {
|
|
5
133
|
private readonly route;
|
|
6
134
|
private readonly router;
|
|
7
135
|
private readonly sparkService;
|
|
8
136
|
private readonly language;
|
|
137
|
+
/** `SparkConfig.conflictDialog.showChangedBy`: whether the conflict notice may name a user at all. */
|
|
138
|
+
private readonly showChangedBy;
|
|
9
139
|
saved: import("@angular/core").OutputEmitterRef<PersistentObject>;
|
|
10
140
|
cancelled: import("@angular/core").OutputEmitterRef<void>;
|
|
11
141
|
colors: typeof Color;
|
|
12
142
|
entityType: import("@angular/core").WritableSignal<EntityType | null>;
|
|
13
143
|
item: import("@angular/core").WritableSignal<PersistentObject | null>;
|
|
144
|
+
/**
|
|
145
|
+
* The type as this form edits it: the loaded object can be stricter than the type, because a
|
|
146
|
+
* server hook may mark an attribute read-only for this row and this caller (QnA: a translator who
|
|
147
|
+
* is not the question's author edits only its translations; the server drops the rest from the
|
|
148
|
+
* save anyway). Shared by the form and {@link getEditableAttributes}, so what is drawn, what is
|
|
149
|
+
* initialised and what is saved agree. A refresh overlay can still lift it.
|
|
150
|
+
*/
|
|
151
|
+
formEntityType: import("@angular/core").Signal<EntityType | null>;
|
|
14
152
|
type: string;
|
|
15
153
|
id: string;
|
|
16
154
|
formData: import("@angular/core").WritableSignal<Record<string, any>>;
|
|
@@ -23,9 +161,30 @@ export declare class SparkPoEditComponent {
|
|
|
23
161
|
isSaving: import("@angular/core").WritableSignal<boolean>;
|
|
24
162
|
private allEntityTypes;
|
|
25
163
|
generalErrors: import("@angular/core").Signal<ValidationError[]>;
|
|
164
|
+
/** After a 409 was merged: what they changed, and that nothing has been saved yet. */
|
|
165
|
+
private readonly conflictNoticeText;
|
|
166
|
+
/** The notice with its "Changed by X at T", which fills in the name once History resolved it. */
|
|
167
|
+
readonly conflictNotice: import("@angular/core").Signal<string | null>;
|
|
168
|
+
/** A 409 whose merge has true conflicts, waiting on the dialog. */
|
|
169
|
+
pendingConflict: import("@angular/core").WritableSignal<PendingConflict | null>;
|
|
170
|
+
protected readonly pendingConflicts: import("@angular/core").Signal<import("@mintplayer/ng-spark/po-edit").MergeConflict[]>;
|
|
171
|
+
/**
|
|
172
|
+
* The name History resolved for whoever wrote the version with `etag`, or null. Only ever set from
|
|
173
|
+
* the revision list, so it is there only when `SparkConfig.conflictDialog.showChangedBy` is true and
|
|
174
|
+
* the caller holds `History/T` (see {@link lookUpChangedBy}).
|
|
175
|
+
*/
|
|
176
|
+
private readonly changedByName;
|
|
177
|
+
private readonly form;
|
|
178
|
+
private readonly locale;
|
|
26
179
|
constructor();
|
|
27
180
|
private onParamsChange;
|
|
28
181
|
initFormData(): void;
|
|
182
|
+
/**
|
|
183
|
+
* The form's flat-dict shape of `currentItem`. Also how a conflict merge brings the object as
|
|
184
|
+
* loaded and as re-fetched into the form's shape, so all three sides went through the same
|
|
185
|
+
* coercions and only real edits differ.
|
|
186
|
+
*/
|
|
187
|
+
private formDataFrom;
|
|
29
188
|
/**
|
|
30
189
|
* Resolves a nested AsDetail type by CLR name, for rebuilding the nested PO wire shape on save.
|
|
31
190
|
*
|
|
@@ -56,9 +215,154 @@ export declare class SparkPoEditComponent {
|
|
|
56
215
|
getEditableAttributes(): import("@mintplayer/ng-spark/models").EntityAttributeDefinition[];
|
|
57
216
|
onSave(): Promise<void>;
|
|
58
217
|
onCancel(): void;
|
|
218
|
+
/**
|
|
219
|
+
* After a 409: re-fetch the object (a normal read, so read rights and row security apply) and
|
|
220
|
+
* merge this form onto it, three-way against the object as loaded (contributions PRD §5, Q10).
|
|
221
|
+
* Disjoint edits merge silently and the page says what they changed; true conflicts open the
|
|
222
|
+
* dialog. Either way the form is rebased onto the fresh etag and **nothing is saved** — the user
|
|
223
|
+
* reviews and saves, and the server's validation and business rules run again on the result.
|
|
224
|
+
*/
|
|
225
|
+
private resolveConcurrencyConflict;
|
|
226
|
+
/**
|
|
227
|
+
* Reference labels for the dialog, by id. The form keeps only ids; the labels are on the two reads
|
|
228
|
+
* (theirs first: it is the fresher) and, for a reference the user just picked, in the candidate
|
|
229
|
+
* lists the form's pickers choose from — top-level and per AsDetail column.
|
|
230
|
+
*/
|
|
231
|
+
protected readonly conflictReferenceLabels: import("@angular/core").Signal<ReferenceLabels>;
|
|
232
|
+
/**
|
|
233
|
+
* Resolves who wrote their version to a display name, through History: the newest revision from
|
|
234
|
+
* `POST /spark/po/revisions` (page size 1) carries the name the app's `IHistoryUserNameResolver`
|
|
235
|
+
* gave it. Taken only when that revision is the version the merge ran against, and — when the
|
|
236
|
+
* object has `ModifiedBy` — written by that same user.
|
|
237
|
+
*
|
|
238
|
+
* Rights: asked only with `History/T` (the endpoint refuses anyone else anyway), so a caller who may
|
|
239
|
+
* not read the history never sees a name from it. Without History installed, or on any failure,
|
|
240
|
+
* nothing is set and the line names no user. Called only when `SparkConfig.conflictDialog.showChangedBy`
|
|
241
|
+
* is true.
|
|
242
|
+
*/
|
|
243
|
+
private lookUpChangedBy;
|
|
244
|
+
/** The dialog's choices, applied on top of the merge. */
|
|
245
|
+
onConflictResolved(choices: Record<string, ConflictSide>): void;
|
|
246
|
+
/** Closing the dialog changes nothing: the form keeps its values and the conflict message. */
|
|
247
|
+
onConflictCancelled(): void;
|
|
248
|
+
/**
|
|
249
|
+
* Makes `theirs` the object this page edits: its etag is what the next save sends, and it is the
|
|
250
|
+
* base a second conflict is merged against. `isValueChanged` is computed against it on save, so
|
|
251
|
+
* only what differs from their version is sent as changed.
|
|
252
|
+
*/
|
|
253
|
+
private rebase;
|
|
254
|
+
/** The dialog's "they also changed" line, or null when the merge took nothing of theirs. */
|
|
255
|
+
protected readonly theyChanged: import("@angular/core").Signal<string | null>;
|
|
256
|
+
protected readonly conflictAudit: import("@angular/core").Signal<string | null>;
|
|
257
|
+
/**
|
|
258
|
+
* "Changed by X at T" for an `IAuditable` target. History stamps `ModifiedBy`/`ModifiedAt` on the
|
|
259
|
+
* entity, and they reach this page only when the model declares them as attributes. `ModifiedBy` is
|
|
260
|
+
* a user id (History stores ids, never names), and that id is never shown. X is the name History
|
|
261
|
+
* resolved for exactly this version ({@link lookUpChangedBy}), which is only looked up when
|
|
262
|
+
* `SparkConfig.conflictDialog.showChangedBy` is true; with no resolved name there is no user part.
|
|
263
|
+
* Either half is shown alone when only it is present, and nothing when neither is.
|
|
264
|
+
*/
|
|
265
|
+
private auditLine;
|
|
59
266
|
static ɵfac: i0.ɵɵFactoryDeclaration<SparkPoEditComponent, never>;
|
|
60
267
|
static ɵcmp: i0.ɵɵComponentDeclaration<SparkPoEditComponent, "spark-po-edit", never, {}, {
|
|
61
268
|
"saved": "saved";
|
|
62
269
|
"cancelled": "cancelled";
|
|
63
270
|
}, never, never, true, never>;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* One side of a conflict: ready for `<spark-grid-cell>`, or, for a whole row or embedded object, its
|
|
274
|
+
* attributes cell by cell (`fields`), or plain text (a removed row, or a value with no type to show).
|
|
275
|
+
*/
|
|
276
|
+
interface ConflictCell {
|
|
277
|
+
column: QueryColumn;
|
|
278
|
+
display: unknown;
|
|
279
|
+
rendererValue: unknown;
|
|
280
|
+
chips: ReferenceChip[];
|
|
281
|
+
item: PersistentObject;
|
|
282
|
+
text?: string;
|
|
283
|
+
fields?: ConflictField[];
|
|
284
|
+
}
|
|
285
|
+
/** One attribute of a row on one side, and whether the other side holds something else there. */
|
|
286
|
+
interface ConflictField {
|
|
287
|
+
attribute: EntityAttributeDefinition;
|
|
288
|
+
cell: ConflictCell;
|
|
289
|
+
differs: boolean;
|
|
290
|
+
}
|
|
291
|
+
interface ConflictGroup {
|
|
292
|
+
key: string;
|
|
293
|
+
/** The root attribute and row this group sits in; empty for plain top-level attributes. */
|
|
294
|
+
attribute?: EntityAttributeDefinition;
|
|
295
|
+
rowLabel?: string;
|
|
296
|
+
entries: {
|
|
297
|
+
conflict: MergeConflict;
|
|
298
|
+
mine: ConflictCell;
|
|
299
|
+
theirs: ConflictCell;
|
|
300
|
+
}[];
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* The true conflicts of a three-way merge after a 409, Mine or Theirs per conflict (grouped by
|
|
304
|
+
* AsDetail row), rendered with the same `<spark-grid-cell>` a query grid and History use. It only
|
|
305
|
+
* collects choices: applying them, and rebasing onto the fresh etag, is the edit page's job — and
|
|
306
|
+
* nothing is saved from here.
|
|
307
|
+
*/
|
|
308
|
+
export declare class SparkPoConflictDialogComponent {
|
|
309
|
+
private readonly language;
|
|
310
|
+
private readonly gridRenderers;
|
|
311
|
+
/** The true conflicts; the dialog is open while there are any. */
|
|
312
|
+
conflicts: import("@angular/core").InputSignal<MergeConflict[]>;
|
|
313
|
+
/**
|
|
314
|
+
* Reference labels by id — the form keeps only ids. Built by the edit page from the object as
|
|
315
|
+
* loaded, the re-fetched one, and the form's reference candidates (see `reference-labels.ts`).
|
|
316
|
+
* An id without a label shows as the id.
|
|
317
|
+
*/
|
|
318
|
+
referenceLabels: import("@angular/core").InputSignal<ReferenceLabels>;
|
|
319
|
+
/** "Changed by X at T", when the object is audited. */
|
|
320
|
+
audit: import("@angular/core").InputSignal<string | null>;
|
|
321
|
+
/** "They also changed: …", the changes merged without a conflict. */
|
|
322
|
+
theyChanged: import("@angular/core").InputSignal<string | null>;
|
|
323
|
+
resolved: import("@angular/core").OutputEmitterRef<Record<string, ConflictSide>>;
|
|
324
|
+
cancelled: import("@angular/core").OutputEmitterRef<void>;
|
|
325
|
+
colors: typeof Color;
|
|
326
|
+
protected readonly choices: import("@angular/core").WritableSignal<Record<string, ConflictSide>>;
|
|
327
|
+
private readonly lookupOptions;
|
|
328
|
+
protected readonly complete: import("@angular/core").Signal<boolean>;
|
|
329
|
+
protected readonly groups: import("@angular/core").Signal<ConflictGroup[]>;
|
|
330
|
+
constructor();
|
|
331
|
+
choose(path: string, side: ConflictSide): void;
|
|
332
|
+
chooseAll(side: ConflictSide): void;
|
|
333
|
+
apply(): void;
|
|
334
|
+
/**
|
|
335
|
+
* One side of a conflict. A whole row (or an embedded object, when its type resolved) is shown
|
|
336
|
+
* attribute by attribute with the same cells as a scalar, marking where `other` — the other side —
|
|
337
|
+
* holds something else; a row absent on this side is "(removed)", which is what choosing it means.
|
|
338
|
+
*/
|
|
339
|
+
private sideCell;
|
|
340
|
+
/** One attribute's value, in the form's shape, as a `<spark-grid-cell>`. */
|
|
341
|
+
private cell;
|
|
342
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<SparkPoConflictDialogComponent, never>;
|
|
343
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<SparkPoConflictDialogComponent, "spark-po-conflict-dialog", never, {
|
|
344
|
+
"conflicts": {
|
|
345
|
+
"alias": "conflicts";
|
|
346
|
+
"required": false;
|
|
347
|
+
"isSignal": true;
|
|
348
|
+
};
|
|
349
|
+
"referenceLabels": {
|
|
350
|
+
"alias": "referenceLabels";
|
|
351
|
+
"required": false;
|
|
352
|
+
"isSignal": true;
|
|
353
|
+
};
|
|
354
|
+
"audit": {
|
|
355
|
+
"alias": "audit";
|
|
356
|
+
"required": false;
|
|
357
|
+
"isSignal": true;
|
|
358
|
+
};
|
|
359
|
+
"theyChanged": {
|
|
360
|
+
"alias": "theyChanged";
|
|
361
|
+
"required": false;
|
|
362
|
+
"isSignal": true;
|
|
363
|
+
};
|
|
364
|
+
}, {
|
|
365
|
+
"resolved": "resolved";
|
|
366
|
+
"cancelled": "cancelled";
|
|
367
|
+
}, never, never, true, never>;
|
|
64
368
|
}
|