@mintplayer/ng-spark 22.24.0 → 22.26.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.
Files changed (54) hide show
  1. package/README.md +15 -0
  2. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +5 -3
  3. package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
  4. package/fesm2022/mintplayer-ng-spark-contributions.mjs +721 -0
  5. package/fesm2022/mintplayer-ng-spark-contributions.mjs.map +1 -0
  6. package/fesm2022/mintplayer-ng-spark-grid.mjs +131 -4
  7. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  8. package/fesm2022/mintplayer-ng-spark-history.mjs +1 -14
  9. package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -1
  10. package/fesm2022/mintplayer-ng-spark-models.mjs +41 -2
  11. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  12. package/fesm2022/mintplayer-ng-spark-moderation.mjs +4 -4
  13. package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -1
  14. package/fesm2022/mintplayer-ng-spark-panels.mjs +9 -1
  15. package/fesm2022/mintplayer-ng-spark-panels.mjs.map +1 -1
  16. package/fesm2022/mintplayer-ng-spark-pipes.mjs +39 -3
  17. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  18. package/fesm2022/mintplayer-ng-spark-po-create.mjs +16 -12
  19. package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
  20. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +15 -6
  21. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  22. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +926 -16
  23. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  24. package/fesm2022/mintplayer-ng-spark-po-form.mjs +142 -30
  25. package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
  26. package/fesm2022/mintplayer-ng-spark-query-list.mjs +60 -5
  27. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  28. package/fesm2022/mintplayer-ng-spark-renderers.mjs +28 -1
  29. package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
  30. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +2 -0
  31. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
  32. package/fesm2022/mintplayer-ng-spark-services.mjs +3 -2
  33. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  34. package/fesm2022/mintplayer-ng-spark-shell.mjs +31 -13
  35. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  36. package/fesm2022/mintplayer-ng-spark.mjs +4 -1
  37. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  38. package/package.json +7 -2
  39. package/types/mintplayer-ng-spark-client-operations.d.ts +7 -1
  40. package/types/mintplayer-ng-spark-contributions.d.ts +231 -0
  41. package/types/mintplayer-ng-spark-grid.d.ts +35 -3
  42. package/types/mintplayer-ng-spark-history.d.ts +3 -10
  43. package/types/mintplayer-ng-spark-models.d.ts +60 -3
  44. package/types/mintplayer-ng-spark-panels.d.ts +36 -1
  45. package/types/mintplayer-ng-spark-pipes.d.ts +12 -1
  46. package/types/mintplayer-ng-spark-po-create.d.ts +10 -0
  47. package/types/mintplayer-ng-spark-po-detail.d.ts +4 -1
  48. package/types/mintplayer-ng-spark-po-edit.d.ts +305 -1
  49. package/types/mintplayer-ng-spark-po-form.d.ts +84 -15
  50. package/types/mintplayer-ng-spark-query-list.d.ts +26 -1
  51. package/types/mintplayer-ng-spark-renderers.d.ts +44 -2
  52. package/types/mintplayer-ng-spark-services.d.ts +10 -1
  53. package/types/mintplayer-ng-spark-shell.d.ts +24 -9
  54. package/types/mintplayer-ng-spark.d.ts +12 -0
@@ -1,26 +1,774 @@
1
1
  import * as i0 from "@angular/core";
2
- import { ChangeDetectionStrategy, Component, computed, inject, output, signal } from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, LOCALE_ID, computed, effect, inject, input, output, signal, untracked, viewChild } from "@angular/core";
3
3
  import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
4
- import { CommonModule } from "@angular/common";
4
+ import { CommonModule, NgTemplateOutlet, formatDate } from "@angular/common";
5
5
  import { ActivatedRoute, Router } from "@angular/router";
6
6
  import { Color } from "@mintplayer/ng-bootstrap";
7
7
  import { BsAlertComponent } from "@mintplayer/ng-bootstrap/alert";
8
8
  import { BsContainerComponent } from "@mintplayer/ng-bootstrap/container";
9
9
  import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
10
+ import { SPARK_CONFIG } from "@mintplayer/ng-spark";
10
11
  import { SparkLanguageService, SparkService } from "@mintplayer/ng-spark/services";
11
12
  import { SparkPoFormComponent } from "@mintplayer/ng-spark/po-form";
12
- import { ResolveTranslationPipe, TranslateKeyPipe } from "@mintplayer/ng-spark/pipes";
13
- import { ShowedOn, applyOverlay, dictToNestedPo, fromDateInputValue, hasShowedOnFlag, isDateDataType, nestedPoToDict, toDateInputValue, wireDatesEqual } from "@mintplayer/ng-spark/models";
13
+ import { QueryCellValuePipe, QueryReferenceChipsPipe, ResolveTranslationPipe, TranslateKeyPipe } from "@mintplayer/ng-spark/pipes";
14
+ import { AS_DETAIL_ROW_KEY, ShowedOn, applyOverlay, dictToNestedPo, formValuesEqual, fromDateInputValue, hasShowedOnFlag, isDateDataType, isReservedAsDetailKey, nestedPoToDict, rowMetadata, selfBreadcrumb, toDateInputValue, valueFor, wireDatesEqual } from "@mintplayer/ng-spark/models";
15
+ import { BsModalBodyDirective, BsModalDirective, BsModalFooterDirective, BsModalHeaderDirective, BsModalHostComponent } from "@mintplayer/ng-bootstrap/modal";
16
+ import { BsButtonTypeDirective } from "@mintplayer/ng-bootstrap/button-type";
17
+ import { SparkGridCellComponent, SparkGridRenderers } from "@mintplayer/ng-spark/grid";
18
+ import { cellValue } from "@mintplayer/ng-spark/renderers";
19
+ const CONTRIBUTION_ROW_METADATA = "contribution";
20
+ function contributionRowIsOwn(row) {
21
+ const facts = rowMetadata(row, CONTRIBUTION_ROW_METADATA);
22
+ return facts && typeof facts.own === "boolean" ? facts.own : void 0;
23
+ }
24
+ function mergeThreeWay(base, mine, theirs, schema, choices = {}) {
25
+ const ctx = {
26
+ resolve: schema.resolve,
27
+ choices,
28
+ conflicts: [],
29
+ theirChanges: [],
30
+ contributionNotices: []
31
+ };
32
+ return {
33
+ merged: mergeObject(schema.attributes, base ?? {}, mine ?? {}, theirs ?? {}, "", void 0, void 0, ctx),
34
+ conflicts: ctx.conflicts,
35
+ theirChanges: ctx.theirChanges,
36
+ contributionNotices: ctx.contributionNotices
37
+ };
38
+ }
39
+ function mergeObject(attributes, base, mine, theirs, prefix, root, rowLabel, ctx) {
40
+ const merged = { ...theirs };
41
+ for (const attribute of attributes) {
42
+ const at = {
43
+ path: prefix ? `${prefix}.${attribute.name}` : attribute.name,
44
+ attribute,
45
+ rootAttribute: root ?? attribute,
46
+ group: prefix,
47
+ rowLabel
48
+ };
49
+ merged[attribute.name] = mergeAttribute(at, base[attribute.name], mine[attribute.name], theirs[attribute.name], ctx);
50
+ }
51
+ return merged;
52
+ }
53
+ function mergeAttribute(at, base, mine, theirs, ctx) {
54
+ if (at.attribute.isReadOnly) return theirs;
55
+ if (at.attribute.dataType === "AsDetail" && at.attribute.asDetailType) {
56
+ const rowType = ctx.resolve(at.attribute.asDetailType);
57
+ if (rowType) {
58
+ const typed = {
59
+ ...at,
60
+ rowType: {
61
+ attributes: rowType.attributes ?? [],
62
+ name: rowType.name
63
+ }
64
+ };
65
+ if (at.attribute.isArray) return mergeRows(typed, rowType.attributes ?? [], rowType.name, asRows(base), asRows(mine), asRows(theirs), ctx);
66
+ if (isPlainObject(base) && isPlainObject(mine) && isPlainObject(theirs) && !formValuesEqual(mine, base) && !formValuesEqual(theirs, base)) return mergeObject(rowType.attributes ?? [], base, mine, theirs, at.path, at.rootAttribute, at.rowLabel, ctx);
67
+ return mergeValue(typed, "value", base, mine, theirs, ctx);
68
+ }
69
+ }
70
+ return mergeValue(at, "value", base, mine, theirs, ctx);
71
+ }
72
+ function mergeValue(at, kind, base, mine, theirs, ctx) {
73
+ const mineChanged = !formValuesEqual(mine, base);
74
+ const theirsChanged = !formValuesEqual(theirs, base);
75
+ if (!mineChanged) {
76
+ if (theirsChanged) ctx.theirChanges.push({
77
+ path: at.path,
78
+ attribute: at.attribute,
79
+ rootAttribute: at.rootAttribute
80
+ });
81
+ return theirs;
82
+ }
83
+ if (!theirsChanged || formValuesEqual(mine, theirs)) return theirsChanged ? theirs : mine;
84
+ return conflict(at, kind, base, mine, theirs, ctx);
85
+ }
86
+ function conflict(at, kind, base, mine, theirs, ctx) {
87
+ ctx.conflicts.push({
88
+ path: at.path,
89
+ kind,
90
+ attribute: at.attribute,
91
+ rootAttribute: at.rootAttribute,
92
+ group: at.group,
93
+ rowLabel: at.rowLabel,
94
+ ...at.rowType ? {
95
+ rowAttributes: at.rowType.attributes,
96
+ rowTypeName: at.rowType.name
97
+ } : {},
98
+ base,
99
+ mine,
100
+ theirs
101
+ });
102
+ return ctx.choices[at.path] === "mine" ? mine : theirs;
103
+ }
104
+ function mergeRows(at, rowAttributes, rowTypeName, base, mine, theirs, ctx) {
105
+ const baseByKey = byKey(base);
106
+ const mineByKey = byKey(mine);
107
+ const merged = [];
108
+ const seen = /* @__PURE__ */ new Set();
109
+ const mergeOne = (key, b, m, t) => {
110
+ const rowPath = `${at.path}[${key}]`;
111
+ const label = selfBreadcrumb(t ?? m ?? b, rowTypeName) ?? key;
112
+ const row = mergeRow({
113
+ ...at,
114
+ path: rowPath,
115
+ rowLabel: label
116
+ }, rowAttributes, b, m, t, ctx);
117
+ if (row !== void 0) merged.push(row);
118
+ };
119
+ for (const t of theirs) {
120
+ const key = rowKey(t);
121
+ if (key === void 0) {
122
+ merged.push(t);
123
+ continue;
124
+ }
125
+ if (seen.has(key)) continue;
126
+ seen.add(key);
127
+ mergeOne(key, baseByKey.get(key), mineByKey.get(key), t);
128
+ }
129
+ for (const m of mine) {
130
+ const key = rowKey(m);
131
+ if (key === void 0) {
132
+ merged.push(m);
133
+ continue;
134
+ }
135
+ if (seen.has(key)) continue;
136
+ seen.add(key);
137
+ mergeOne(key, baseByKey.get(key), m, void 0);
138
+ }
139
+ return merged;
140
+ }
141
+ function mergeRow(at, rowAttributes, base, mine, theirs, ctx) {
142
+ if (contributionRowIsOwn(theirs ?? base) === false && !formValuesEqual(mine, base) && !formValuesEqual(theirs, base)) {
143
+ ctx.theirChanges.push({
144
+ path: at.path,
145
+ attribute: at.attribute,
146
+ rootAttribute: at.rootAttribute
147
+ });
148
+ ctx.contributionNotices.push({
149
+ path: at.path,
150
+ attribute: at.attribute,
151
+ rowLabel: at.rowLabel ?? at.path
152
+ });
153
+ return theirs;
154
+ }
155
+ if (base === void 0) {
156
+ if (mine !== void 0 && theirs !== void 0) return formValuesEqual(mine, theirs) ? theirs : conflict(at, "row", void 0, mine, theirs, ctx);
157
+ if (theirs !== void 0) {
158
+ ctx.theirChanges.push({
159
+ path: at.path,
160
+ attribute: at.attribute,
161
+ rootAttribute: at.rootAttribute
162
+ });
163
+ return theirs;
164
+ }
165
+ return mine;
166
+ }
167
+ if (mine === void 0 && theirs === void 0) return void 0;
168
+ if (mine === void 0) {
169
+ if (formValuesEqual(theirs, base)) return void 0;
170
+ return conflict(at, "row", base, void 0, theirs, ctx);
171
+ }
172
+ if (theirs === void 0) {
173
+ if (formValuesEqual(mine, base)) {
174
+ ctx.theirChanges.push({
175
+ path: at.path,
176
+ attribute: at.attribute,
177
+ rootAttribute: at.rootAttribute
178
+ });
179
+ return;
180
+ }
181
+ return conflict(at, "row", base, mine, void 0, ctx);
182
+ }
183
+ return mergeObject(rowAttributes, base, mine, theirs, at.path, at.rootAttribute, at.rowLabel, ctx);
184
+ }
185
+ function rowKey(row) {
186
+ const key = row?.[AS_DETAIL_ROW_KEY];
187
+ return typeof key === "string" && key !== "" ? key : void 0;
188
+ }
189
+ function byKey(rows) {
190
+ const map = /* @__PURE__ */ new Map();
191
+ for (const row of rows) {
192
+ const key = rowKey(row);
193
+ if (key !== void 0 && !map.has(key)) map.set(key, row);
194
+ }
195
+ return map;
196
+ }
197
+ function asRows(value) {
198
+ return Array.isArray(value) ? value.filter(isPlainObject) : [];
199
+ }
200
+ function isPlainObject(value) {
201
+ return typeof value === "object" && value !== null && !Array.isArray(value);
202
+ }
203
+ const cellValuePipe = new QueryCellValuePipe();
204
+ const chipsPipe = new QueryReferenceChipsPipe();
205
+ var SparkPoConflictDialogComponent = class SparkPoConflictDialogComponent {
206
+ language = inject(SparkLanguageService);
207
+ gridRenderers = inject(SparkGridRenderers);
208
+ conflicts = input([], ...ngDevMode ? [{ debugName: "conflicts" }] : /* istanbul ignore next */ []);
209
+ referenceLabels = input({}, ...ngDevMode ? [{ debugName: "referenceLabels" }] : /* istanbul ignore next */ []);
210
+ audit = input(null, ...ngDevMode ? [{ debugName: "audit" }] : /* istanbul ignore next */ []);
211
+ theyChanged = input(null, ...ngDevMode ? [{ debugName: "theyChanged" }] : /* istanbul ignore next */ []);
212
+ resolved = output();
213
+ cancelled = output();
214
+ colors = Color;
215
+ choices = signal({}, ...ngDevMode ? [{ debugName: "choices" }] : /* istanbul ignore next */ []);
216
+ lookupOptions = signal({}, ...ngDevMode ? [{ debugName: "lookupOptions" }] : /* istanbul ignore next */ []);
217
+ complete = computed(() => {
218
+ const choices = this.choices();
219
+ return this.conflicts().every((c) => choices[c.path] !== void 0);
220
+ }, ...ngDevMode ? [{ debugName: "complete" }] : /* istanbul ignore next */ []);
221
+ groups = computed(() => {
222
+ const lookups = this.lookupOptions();
223
+ const labels = this.referenceLabels();
224
+ const groups = /* @__PURE__ */ new Map();
225
+ for (const conflict of this.conflicts()) {
226
+ const key = conflict.kind === "row" ? conflict.path : conflict.group;
227
+ let group = groups.get(key);
228
+ if (!group) {
229
+ group = key === "" ? {
230
+ key,
231
+ entries: []
232
+ } : {
233
+ key,
234
+ attribute: conflict.rootAttribute,
235
+ rowLabel: conflict.kind === "row" ? void 0 : conflict.rowLabel,
236
+ entries: []
237
+ };
238
+ groups.set(key, group);
239
+ }
240
+ group.entries.push({
241
+ conflict,
242
+ mine: this.sideCell(conflict, conflict.mine, conflict.theirs, labels, lookups),
243
+ theirs: this.sideCell(conflict, conflict.theirs, conflict.mine, labels, lookups)
244
+ });
245
+ }
246
+ return [...groups.values()];
247
+ }, ...ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []);
248
+ constructor() {
249
+ effect(() => {
250
+ const conflicts = this.conflicts();
251
+ untracked(() => {
252
+ this.choices.set({});
253
+ const attributes = conflicts.flatMap((c) => [...c.kind === "value" ? [c.attribute] : [], ...c.rowAttributes ?? []]);
254
+ this.gridRenderers.loadLookupOptions(attributes).then((o) => this.lookupOptions.set(o), () => this.lookupOptions.set({}));
255
+ });
256
+ });
257
+ }
258
+ choose(path, side) {
259
+ this.choices.update((c) => ({
260
+ ...c,
261
+ [path]: side
262
+ }));
263
+ }
264
+ chooseAll(side) {
265
+ const all = {};
266
+ for (const c of this.conflicts()) all[c.path] = side;
267
+ this.choices.set(all);
268
+ }
269
+ apply() {
270
+ if (!this.complete()) return;
271
+ this.resolved.emit(this.choices());
272
+ }
273
+ sideCell(conflict, value, other, labels, lookups) {
274
+ if (!(conflict.kind === "row" || conflict.attribute.dataType === "AsDetail")) return this.cell(conflict.attribute, value, labels, lookups);
275
+ const cell = this.cell(conflict.attribute, void 0, labels, lookups);
276
+ if (value === void 0 || value === null) {
277
+ cell.text = value === void 0 && conflict.kind === "row" ? this.language.t("common.conflictRowRemoved") : "-";
278
+ return cell;
279
+ }
280
+ if (!conflict.rowAttributes || typeof value !== "object" || Array.isArray(value)) {
281
+ cell.text = summarize(value) || "-";
282
+ return cell;
283
+ }
284
+ const row = value;
285
+ const otherRow = typeof other === "object" && other !== null && !Array.isArray(other) ? other : void 0;
286
+ cell.fields = conflict.rowAttributes.filter((a) => a.isVisible !== false).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).map((attribute) => ({
287
+ attribute,
288
+ cell: this.cell(attribute, row[attribute.name], labels, lookups),
289
+ differs: otherRow !== void 0 && !formValuesEqual(row[attribute.name], otherRow[attribute.name])
290
+ }));
291
+ return cell;
292
+ }
293
+ cell(attribute, value, labels, lookups) {
294
+ const column = attribute;
295
+ const wire = isDateDataType(attribute.dataType) ? fromDateInputValue(attribute.dataType, value) : value;
296
+ const isReference = attribute.dataType === "Reference";
297
+ const breadcrumb = isReference && typeof wire === "string" ? labels[wire] : void 0;
298
+ const breadcrumbs = isReference && Array.isArray(wire) ? Object.fromEntries(wire.filter((id) => typeof id === "string" && id in labels).map((id) => [id, labels[id]])) : void 0;
299
+ const item = {
300
+ id: "",
301
+ name: "",
302
+ objectTypeId: "",
303
+ attributes: [{
304
+ id: attribute.id,
305
+ name: attribute.name,
306
+ dataType: attribute.dataType,
307
+ isArray: attribute.isArray,
308
+ isRequired: false,
309
+ isVisible: true,
310
+ isReadOnly: true,
311
+ order: 0,
312
+ rules: [],
313
+ value: wire,
314
+ breadcrumb,
315
+ breadcrumbs
316
+ }]
317
+ };
318
+ const cell = {
319
+ column,
320
+ display: "",
321
+ rendererValue: null,
322
+ chips: [],
323
+ item
324
+ };
325
+ if (value === void 0 && attribute.dataType === "AsDetail") return cell;
326
+ if (attribute.dataType === "AsDetail" || typeof value === "object" && value !== null && !Array.isArray(value)) {
327
+ cell.text = summarize(value) || "-";
328
+ return cell;
329
+ }
330
+ const row = item;
331
+ const display = cellValuePipe.transform(column, row, lookups);
332
+ cell.display = attribute.dataType !== "boolean" && (display == null || display === "") ? "-" : display;
333
+ cell.rendererValue = cellValue(valueFor(row, attribute.name));
334
+ cell.chips = chipsPipe.transform(column, row);
335
+ return cell;
336
+ }
337
+ static ɵfac = i0.ɵɵngDeclareFactory({
338
+ minVersion: "12.0.0",
339
+ version: "22.2.0",
340
+ ngImport: i0,
341
+ type: SparkPoConflictDialogComponent,
342
+ deps: [],
343
+ target: i0.ɵɵFactoryTarget.Component
344
+ });
345
+ static ɵcmp = i0.ɵɵngDeclareComponent({
346
+ minVersion: "17.0.0",
347
+ version: "22.2.0",
348
+ type: SparkPoConflictDialogComponent,
349
+ isStandalone: true,
350
+ selector: "spark-po-conflict-dialog",
351
+ inputs: {
352
+ conflicts: {
353
+ classPropertyName: "conflicts",
354
+ publicName: "conflicts",
355
+ isSignal: true,
356
+ isRequired: false,
357
+ transformFunction: null
358
+ },
359
+ referenceLabels: {
360
+ classPropertyName: "referenceLabels",
361
+ publicName: "referenceLabels",
362
+ isSignal: true,
363
+ isRequired: false,
364
+ transformFunction: null
365
+ },
366
+ audit: {
367
+ classPropertyName: "audit",
368
+ publicName: "audit",
369
+ isSignal: true,
370
+ isRequired: false,
371
+ transformFunction: null
372
+ },
373
+ theyChanged: {
374
+ classPropertyName: "theyChanged",
375
+ publicName: "theyChanged",
376
+ isSignal: true,
377
+ isRequired: false,
378
+ transformFunction: null
379
+ }
380
+ },
381
+ outputs: {
382
+ resolved: "resolved",
383
+ cancelled: "cancelled"
384
+ },
385
+ ngImport: i0,
386
+ template: `
387
+ <bs-modal [isOpen]="conflicts().length > 0" (isOpenChange)="!$event && cancelled.emit()">
388
+ <div *bsModal>
389
+ <div bsModalHeader>
390
+ <h5 class="modal-title">{{ 'common.conflictTitle' | t }}</h5>
391
+ </div>
392
+ <div bsModalBody class="spark-conflict-dialog">
393
+ <p>{{ 'common.conflictIntro' | t }}</p>
394
+ @if (audit()) {
395
+ <p class="text-muted spark-conflict-audit">{{ audit() }}</p>
396
+ }
397
+ @if (theyChanged()) {
398
+ <p class="text-muted spark-conflict-they-changed">{{ theyChanged() }}</p>
399
+ }
400
+ @for (group of groups(); track group.key) {
401
+ <div class="mb-3 spark-conflict-group">
402
+ @if (group.attribute) {
403
+ <h6>
404
+ <span>{{ group.attribute.label | resolveTranslation:group.attribute.name }}</span>
405
+ @if (group.rowLabel) {
406
+ <span> — {{ group.rowLabel }}</span>
407
+ }
408
+ </h6>
409
+ }
410
+ <table class="table table-sm align-middle mb-0">
411
+ <thead>
412
+ <tr>
413
+ <th>{{ 'common.conflictField' | t }}</th>
414
+ <th>{{ 'common.conflictMine' | t }}</th>
415
+ <th>{{ 'common.conflictTheirs' | t }}</th>
416
+ </tr>
417
+ </thead>
418
+ <tbody>
419
+ @for (entry of group.entries; track entry.conflict.path) {
420
+ <tr class="spark-conflict" [attr.data-path]="entry.conflict.path">
421
+ <th scope="row">
422
+ @if (entry.conflict.kind === 'row') {
423
+ {{ entry.conflict.rowLabel }}
424
+ } @else {
425
+ {{ entry.conflict.attribute.label | resolveTranslation:entry.conflict.attribute.name }}
426
+ }
427
+ </th>
428
+ <td class="spark-conflict-mine-value">
429
+ <label class="d-flex gap-2 align-items-start">
430
+ <input type="radio" class="form-check-input spark-conflict-mine" [name]="entry.conflict.path"
431
+ [checked]="choices()[entry.conflict.path] === 'mine'" (change)="choose(entry.conflict.path, 'mine')" />
432
+ <ng-container [ngTemplateOutlet]="side" [ngTemplateOutletContext]="{ $implicit: entry.mine }" />
433
+ </label>
434
+ </td>
435
+ <td class="spark-conflict-theirs-value">
436
+ <label class="d-flex gap-2 align-items-start">
437
+ <input type="radio" class="form-check-input spark-conflict-theirs" [name]="entry.conflict.path"
438
+ [checked]="choices()[entry.conflict.path] === 'theirs'" (change)="choose(entry.conflict.path, 'theirs')" />
439
+ <ng-container [ngTemplateOutlet]="side" [ngTemplateOutletContext]="{ $implicit: entry.theirs }" />
440
+ </label>
441
+ </td>
442
+ </tr>
443
+ }
444
+ </tbody>
445
+ </table>
446
+ </div>
447
+ }
448
+ </div>
449
+ <div bsModalFooter>
450
+ <button type="button" class="spark-conflict-all-mine" [color]="colors.secondary" (click)="chooseAll('mine')">{{ 'common.conflictKeepAllMine' | t }}</button>
451
+ <button type="button" class="spark-conflict-all-theirs" [color]="colors.secondary" (click)="chooseAll('theirs')">{{ 'common.conflictTakeAllTheirs' | t }}</button>
452
+ <button type="button" class="spark-conflict-cancel" [color]="colors.secondary" (click)="cancelled.emit()">{{ 'common.cancel' | t }}</button>
453
+ <button type="button" class="spark-conflict-apply" [color]="colors.primary" [disabled]="!complete()" (click)="apply()">{{ 'common.conflictApply' | t }}</button>
454
+ </div>
455
+ </div>
456
+ </bs-modal>
457
+
458
+ <!-- One side's value. A row is its attributes cell by cell, the ones the other side differs in marked. -->
459
+ <ng-template #side let-cell>
460
+ @if (cell.fields) {
461
+ <table class="table table-sm table-borderless mb-0 spark-conflict-row-cells">
462
+ <tbody>
463
+ @for (field of cell.fields; track field.attribute.name) {
464
+ <tr [class.spark-conflict-differs]="field.differs" [attr.data-attribute]="field.attribute.name">
465
+ <th scope="row" class="fw-normal text-muted pe-2">{{ field.attribute.label | resolveTranslation:field.attribute.name }}</th>
466
+ <td [class.fw-bold]="field.differs">
467
+ <ng-container [ngTemplateOutlet]="value" [ngTemplateOutletContext]="{ $implicit: field.cell }" />
468
+ </td>
469
+ </tr>
470
+ }
471
+ </tbody>
472
+ </table>
473
+ } @else {
474
+ <span><ng-container [ngTemplateOutlet]="value" [ngTemplateOutletContext]="{ $implicit: cell }" /></span>
475
+ }
476
+ </ng-template>
477
+ <ng-template #value let-cell>
478
+ @if (cell.text !== undefined) { {{ cell.text }} } @else {
479
+ <spark-grid-cell [column]="cell.column" [display]="cell.display" [rendererValue]="cell.rendererValue"
480
+ [item]="cell.item" [chips]="cell.chips" />
481
+ }
482
+ </ng-template>
483
+ `,
484
+ isInline: true,
485
+ dependencies: [
486
+ {
487
+ kind: "directive",
488
+ type: NgTemplateOutlet,
489
+ selector: "[ngTemplateOutlet]",
490
+ inputs: [
491
+ "ngTemplateOutletContext",
492
+ "ngTemplateOutlet",
493
+ "ngTemplateOutletInjector"
494
+ ]
495
+ },
496
+ {
497
+ kind: "component",
498
+ type: BsModalHostComponent,
499
+ selector: "bs-modal",
500
+ inputs: [
501
+ "isOpen",
502
+ "closeOnEscape",
503
+ "scrollable"
504
+ ],
505
+ outputs: ["isOpenChange"]
506
+ },
507
+ {
508
+ kind: "directive",
509
+ type: BsModalDirective,
510
+ selector: "[bsModal]"
511
+ },
512
+ {
513
+ kind: "directive",
514
+ type: BsModalHeaderDirective,
515
+ selector: "[bsModalHeader]"
516
+ },
517
+ {
518
+ kind: "directive",
519
+ type: BsModalBodyDirective,
520
+ selector: "[bsModalBody]"
521
+ },
522
+ {
523
+ kind: "directive",
524
+ type: BsModalFooterDirective,
525
+ selector: "[bsModalFooter]"
526
+ },
527
+ {
528
+ kind: "directive",
529
+ type: BsButtonTypeDirective,
530
+ selector: "button[color],input[type=\"button\"][color],input[type=\"submit\"][color],a[color]",
531
+ inputs: ["color"]
532
+ },
533
+ {
534
+ kind: "component",
535
+ type: SparkGridCellComponent,
536
+ selector: "spark-grid-cell",
537
+ inputs: [
538
+ "column",
539
+ "display",
540
+ "rendererValue",
541
+ "item",
542
+ "chips",
543
+ "link"
544
+ ]
545
+ },
546
+ {
547
+ kind: "pipe",
548
+ type: ResolveTranslationPipe,
549
+ name: "resolveTranslation"
550
+ },
551
+ {
552
+ kind: "pipe",
553
+ type: TranslateKeyPipe,
554
+ name: "t"
555
+ }
556
+ ],
557
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
558
+ });
559
+ };
560
+ i0.ɵɵngDeclareClassMetadata({
561
+ minVersion: "12.0.0",
562
+ version: "22.2.0",
563
+ ngImport: i0,
564
+ type: SparkPoConflictDialogComponent,
565
+ decorators: [{
566
+ type: Component,
567
+ args: [{
568
+ selector: "spark-po-conflict-dialog",
569
+ imports: [
570
+ NgTemplateOutlet,
571
+ BsModalHostComponent,
572
+ BsModalDirective,
573
+ BsModalHeaderDirective,
574
+ BsModalBodyDirective,
575
+ BsModalFooterDirective,
576
+ BsButtonTypeDirective,
577
+ SparkGridCellComponent,
578
+ ResolveTranslationPipe,
579
+ TranslateKeyPipe
580
+ ],
581
+ template: `
582
+ <bs-modal [isOpen]="conflicts().length > 0" (isOpenChange)="!$event && cancelled.emit()">
583
+ <div *bsModal>
584
+ <div bsModalHeader>
585
+ <h5 class="modal-title">{{ 'common.conflictTitle' | t }}</h5>
586
+ </div>
587
+ <div bsModalBody class="spark-conflict-dialog">
588
+ <p>{{ 'common.conflictIntro' | t }}</p>
589
+ @if (audit()) {
590
+ <p class="text-muted spark-conflict-audit">{{ audit() }}</p>
591
+ }
592
+ @if (theyChanged()) {
593
+ <p class="text-muted spark-conflict-they-changed">{{ theyChanged() }}</p>
594
+ }
595
+ @for (group of groups(); track group.key) {
596
+ <div class="mb-3 spark-conflict-group">
597
+ @if (group.attribute) {
598
+ <h6>
599
+ <span>{{ group.attribute.label | resolveTranslation:group.attribute.name }}</span>
600
+ @if (group.rowLabel) {
601
+ <span> — {{ group.rowLabel }}</span>
602
+ }
603
+ </h6>
604
+ }
605
+ <table class="table table-sm align-middle mb-0">
606
+ <thead>
607
+ <tr>
608
+ <th>{{ 'common.conflictField' | t }}</th>
609
+ <th>{{ 'common.conflictMine' | t }}</th>
610
+ <th>{{ 'common.conflictTheirs' | t }}</th>
611
+ </tr>
612
+ </thead>
613
+ <tbody>
614
+ @for (entry of group.entries; track entry.conflict.path) {
615
+ <tr class="spark-conflict" [attr.data-path]="entry.conflict.path">
616
+ <th scope="row">
617
+ @if (entry.conflict.kind === 'row') {
618
+ {{ entry.conflict.rowLabel }}
619
+ } @else {
620
+ {{ entry.conflict.attribute.label | resolveTranslation:entry.conflict.attribute.name }}
621
+ }
622
+ </th>
623
+ <td class="spark-conflict-mine-value">
624
+ <label class="d-flex gap-2 align-items-start">
625
+ <input type="radio" class="form-check-input spark-conflict-mine" [name]="entry.conflict.path"
626
+ [checked]="choices()[entry.conflict.path] === 'mine'" (change)="choose(entry.conflict.path, 'mine')" />
627
+ <ng-container [ngTemplateOutlet]="side" [ngTemplateOutletContext]="{ $implicit: entry.mine }" />
628
+ </label>
629
+ </td>
630
+ <td class="spark-conflict-theirs-value">
631
+ <label class="d-flex gap-2 align-items-start">
632
+ <input type="radio" class="form-check-input spark-conflict-theirs" [name]="entry.conflict.path"
633
+ [checked]="choices()[entry.conflict.path] === 'theirs'" (change)="choose(entry.conflict.path, 'theirs')" />
634
+ <ng-container [ngTemplateOutlet]="side" [ngTemplateOutletContext]="{ $implicit: entry.theirs }" />
635
+ </label>
636
+ </td>
637
+ </tr>
638
+ }
639
+ </tbody>
640
+ </table>
641
+ </div>
642
+ }
643
+ </div>
644
+ <div bsModalFooter>
645
+ <button type="button" class="spark-conflict-all-mine" [color]="colors.secondary" (click)="chooseAll('mine')">{{ 'common.conflictKeepAllMine' | t }}</button>
646
+ <button type="button" class="spark-conflict-all-theirs" [color]="colors.secondary" (click)="chooseAll('theirs')">{{ 'common.conflictTakeAllTheirs' | t }}</button>
647
+ <button type="button" class="spark-conflict-cancel" [color]="colors.secondary" (click)="cancelled.emit()">{{ 'common.cancel' | t }}</button>
648
+ <button type="button" class="spark-conflict-apply" [color]="colors.primary" [disabled]="!complete()" (click)="apply()">{{ 'common.conflictApply' | t }}</button>
649
+ </div>
650
+ </div>
651
+ </bs-modal>
652
+
653
+ <!-- One side's value. A row is its attributes cell by cell, the ones the other side differs in marked. -->
654
+ <ng-template #side let-cell>
655
+ @if (cell.fields) {
656
+ <table class="table table-sm table-borderless mb-0 spark-conflict-row-cells">
657
+ <tbody>
658
+ @for (field of cell.fields; track field.attribute.name) {
659
+ <tr [class.spark-conflict-differs]="field.differs" [attr.data-attribute]="field.attribute.name">
660
+ <th scope="row" class="fw-normal text-muted pe-2">{{ field.attribute.label | resolveTranslation:field.attribute.name }}</th>
661
+ <td [class.fw-bold]="field.differs">
662
+ <ng-container [ngTemplateOutlet]="value" [ngTemplateOutletContext]="{ $implicit: field.cell }" />
663
+ </td>
664
+ </tr>
665
+ }
666
+ </tbody>
667
+ </table>
668
+ } @else {
669
+ <span><ng-container [ngTemplateOutlet]="value" [ngTemplateOutletContext]="{ $implicit: cell }" /></span>
670
+ }
671
+ </ng-template>
672
+ <ng-template #value let-cell>
673
+ @if (cell.text !== undefined) { {{ cell.text }} } @else {
674
+ <spark-grid-cell [column]="cell.column" [display]="cell.display" [rendererValue]="cell.rendererValue"
675
+ [item]="cell.item" [chips]="cell.chips" />
676
+ }
677
+ </ng-template>
678
+ `,
679
+ changeDetection: ChangeDetectionStrategy.OnPush
680
+ }]
681
+ }],
682
+ ctorParameters: () => [],
683
+ propDecorators: {
684
+ conflicts: [{
685
+ type: i0.Input,
686
+ args: [{
687
+ isSignal: true,
688
+ alias: "conflicts",
689
+ required: false
690
+ }]
691
+ }],
692
+ referenceLabels: [{
693
+ type: i0.Input,
694
+ args: [{
695
+ isSignal: true,
696
+ alias: "referenceLabels",
697
+ required: false
698
+ }]
699
+ }],
700
+ audit: [{
701
+ type: i0.Input,
702
+ args: [{
703
+ isSignal: true,
704
+ alias: "audit",
705
+ required: false
706
+ }]
707
+ }],
708
+ theyChanged: [{
709
+ type: i0.Input,
710
+ args: [{
711
+ isSignal: true,
712
+ alias: "theyChanged",
713
+ required: false
714
+ }]
715
+ }],
716
+ resolved: [{
717
+ type: i0.Output,
718
+ args: ["resolved"]
719
+ }],
720
+ cancelled: [{
721
+ type: i0.Output,
722
+ args: ["cancelled"]
723
+ }]
724
+ }
725
+ });
726
+ function summarize(value) {
727
+ if (value === null || value === void 0) return "";
728
+ if (Array.isArray(value)) return value.map(summarize).filter((s) => s !== "").join("; ");
729
+ if (typeof value === "object") return Object.entries(value).filter(([key]) => !isReservedAsDetailKey(key)).map(([, v]) => summarize(v)).filter((s) => s !== "").join(", ");
730
+ return String(value);
731
+ }
732
+ function addReferenceLabelsOf(po, into) {
733
+ for (const attr of po?.attributes ?? []) {
734
+ if (attr.object) addReferenceLabelsOf(attr.object, into);
735
+ for (const row of attr.objects ?? []) addReferenceLabelsOf(row, into);
736
+ for (const [id, label] of Object.entries(attr.breadcrumbs ?? {})) add(into, id, label);
737
+ if (typeof attr.value === "string") add(into, attr.value, attr.breadcrumb);
738
+ }
739
+ return into;
740
+ }
741
+ function addReferenceLabelsOfOptions(options, into) {
742
+ for (const list of options) for (const item of list ?? []) if (item.id) add(into, item.id, item.breadcrumb);
743
+ return into;
744
+ }
745
+ function add(into, id, label) {
746
+ if (id === "" || id in into || typeof label !== "string" || label.trim() === "") return;
747
+ into[id] = label;
748
+ }
14
749
  var SparkPoEditComponent = class SparkPoEditComponent {
15
750
  route = inject(ActivatedRoute);
16
751
  router = inject(Router);
17
752
  sparkService = inject(SparkService);
18
753
  language = inject(SparkLanguageService);
754
+ showChangedBy = inject(SPARK_CONFIG, { optional: true })?.conflictDialog?.showChangedBy === true;
19
755
  saved = output();
20
756
  cancelled = output();
21
757
  colors = Color;
22
758
  entityType = signal(null, ...ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []);
23
759
  item = signal(null, ...ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []);
760
+ formEntityType = computed(() => {
761
+ const type = this.entityType();
762
+ const readOnlyHere = new Set((this.item()?.attributes ?? []).filter((a) => a.isReadOnly).map((a) => a.name));
763
+ if (!type || readOnlyHere.size === 0) return type;
764
+ return {
765
+ ...type,
766
+ attributes: type.attributes.map((a) => readOnlyHere.has(a.name) && !a.isReadOnly ? {
767
+ ...a,
768
+ isReadOnly: true
769
+ } : a)
770
+ };
771
+ }, ...ngDevMode ? [{ debugName: "formEntityType" }] : /* istanbul ignore next */ []);
24
772
  type = "";
25
773
  id = "";
26
774
  formData = signal({}, ...ngDevMode ? [{ debugName: "formData" }] : /* istanbul ignore next */ []);
@@ -29,6 +777,19 @@ var SparkPoEditComponent = class SparkPoEditComponent {
29
777
  isSaving = signal(false, ...ngDevMode ? [{ debugName: "isSaving" }] : /* istanbul ignore next */ []);
30
778
  allEntityTypes = signal([], ...ngDevMode ? [{ debugName: "allEntityTypes" }] : /* istanbul ignore next */ []);
31
779
  generalErrors = computed(() => this.validationErrors().filter((e) => !e.attributeName), ...ngDevMode ? [{ debugName: "generalErrors" }] : /* istanbul ignore next */ []);
780
+ conflictNoticeText = signal(null, ...ngDevMode ? [{ debugName: "conflictNoticeText" }] : /* istanbul ignore next */ []);
781
+ conflictNotice = computed(() => {
782
+ const text = this.conflictNoticeText();
783
+ if (!text) return null;
784
+ const item = this.item();
785
+ const audit = item ? this.auditLine(item) : null;
786
+ return audit ? `${text} ${audit}` : text;
787
+ }, ...ngDevMode ? [{ debugName: "conflictNotice" }] : /* istanbul ignore next */ []);
788
+ pendingConflict = signal(null, ...ngDevMode ? [{ debugName: "pendingConflict" }] : /* istanbul ignore next */ []);
789
+ pendingConflicts = computed(() => this.pendingConflict()?.result.conflicts ?? [], ...ngDevMode ? [{ debugName: "pendingConflicts" }] : /* istanbul ignore next */ []);
790
+ changedByName = signal(null, ...ngDevMode ? [{ debugName: "changedByName" }] : /* istanbul ignore next */ []);
791
+ form = viewChild(SparkPoFormComponent, ...ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []);
792
+ locale = inject(LOCALE_ID);
32
793
  constructor() {
33
794
  this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => this.onParamsChange(params));
34
795
  }
@@ -52,8 +813,10 @@ var SparkPoEditComponent = class SparkPoEditComponent {
52
813
  }
53
814
  }
54
815
  initFormData() {
816
+ this.formData.set(this.formDataFrom(this.item()));
817
+ }
818
+ formDataFrom(currentItem) {
55
819
  const data = {};
56
- const currentItem = this.item();
57
820
  this.getEditableAttributes().forEach((attr) => {
58
821
  const itemAttr = currentItem?.attributes.find((a) => a.name === attr.name);
59
822
  if (attr.dataType === "Reference") data[attr.name] = itemAttr?.value ?? null;
@@ -64,7 +827,7 @@ var SparkPoEditComponent = class SparkPoEditComponent {
64
827
  else if (isDateDataType(attr.dataType)) data[attr.name] = toDateInputValue(attr.dataType, itemAttr?.value);
65
828
  else data[attr.name] = itemAttr?.value ?? "";
66
829
  });
67
- this.formData.set(data);
830
+ return data;
68
831
  }
69
832
  resolveEntityType() {
70
833
  const cache = this.allEntityTypes();
@@ -73,12 +836,13 @@ var SparkPoEditComponent = class SparkPoEditComponent {
73
836
  }
74
837
  getEditableAttributes() {
75
838
  const overlay = this.refreshOverlay();
76
- return this.entityType()?.attributes.map((a) => applyOverlay(a, overlay[a.name])).filter((a) => a.isVisible && !a.isReadOnly && hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject)).sort((a, b) => a.order - b.order) || [];
839
+ return this.formEntityType()?.attributes.map((a) => applyOverlay(a, overlay[a.name])).filter((a) => a.isVisible && !a.isReadOnly && hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject)).sort((a, b) => a.order - b.order) || [];
77
840
  }
78
841
  async onSave() {
79
842
  const currentItem = this.item();
80
843
  if (!this.entityType() || !currentItem) return;
81
844
  this.validationErrors.set([]);
845
+ this.conflictNoticeText.set(null);
82
846
  this.isSaving.set(true);
83
847
  const resolver = this.resolveEntityType();
84
848
  const attributes = currentItem.attributes.map((attr) => {
@@ -147,12 +911,14 @@ var SparkPoEditComponent = class SparkPoEditComponent {
147
911
  const error = e;
148
912
  const errors = error.error?.result?.errors ?? error.error?.errors;
149
913
  if (error.status === 400 && errors) this.validationErrors.set(errors);
150
- else if (error.status === 409) this.validationErrors.set([{
151
- attributeName: "",
152
- errorMessage: { en: this.language.t("common.concurrencyConflict") },
153
- ruleType: "error"
154
- }]);
155
- else this.validationErrors.set([{
914
+ else if (error.status === 409) {
915
+ this.validationErrors.set([{
916
+ attributeName: "",
917
+ errorMessage: { en: this.language.t("common.concurrencyConflict") },
918
+ ruleType: "error"
919
+ }]);
920
+ await this.resolveConcurrencyConflict();
921
+ } else this.validationErrors.set([{
156
922
  attributeName: "",
157
923
  errorMessage: { en: error.error?.result?.error || error.error?.error || error.message || "An unexpected error occurred" },
158
924
  ruleType: "error"
@@ -167,6 +933,120 @@ var SparkPoEditComponent = class SparkPoEditComponent {
167
933
  this.id
168
934
  ]);
169
935
  }
936
+ async resolveConcurrencyConflict() {
937
+ const base = this.item();
938
+ if (!base) return;
939
+ let theirs;
940
+ try {
941
+ theirs = await this.sparkService.get(this.type, this.id);
942
+ } catch {
943
+ return;
944
+ }
945
+ this.changedByName.set(null);
946
+ if (this.showChangedBy) this.lookUpChangedBy(theirs);
947
+ const attributes = this.getEditableAttributes();
948
+ const baseForm = this.formDataFrom(base);
949
+ const theirsForm = this.formDataFrom(theirs);
950
+ const mine = structuredClone(this.formData());
951
+ const schema = {
952
+ attributes,
953
+ resolve: this.resolveEntityType()
954
+ };
955
+ const result = mergeThreeWay(baseForm, mine, theirsForm, schema);
956
+ if (result.conflicts.length === 0) {
957
+ this.rebase(theirs, result.merged, result.theirChanges, "common.conflictMerged", result.contributionNotices);
958
+ return;
959
+ }
960
+ this.pendingConflict.set({
961
+ loaded: base,
962
+ theirs,
963
+ base: baseForm,
964
+ mine,
965
+ theirsForm,
966
+ schema,
967
+ result
968
+ });
969
+ }
970
+ conflictReferenceLabels = computed(() => {
971
+ const pending = this.pendingConflict();
972
+ if (!pending) return {};
973
+ const labels = {};
974
+ addReferenceLabelsOf(pending.theirs, labels);
975
+ addReferenceLabelsOf(pending.loaded, labels);
976
+ const form = this.form();
977
+ if (form) {
978
+ addReferenceLabelsOfOptions(Object.values(form.referenceOptions()), labels);
979
+ for (const columns of Object.values(form.asDetailReferenceOptions())) addReferenceLabelsOfOptions(Object.values(columns), labels);
980
+ }
981
+ return labels;
982
+ }, ...ngDevMode ? [{ debugName: "conflictReferenceLabels" }] : /* istanbul ignore next */ []);
983
+ async lookUpChangedBy(theirs) {
984
+ this.changedByName.set(null);
985
+ const typeId = this.entityType()?.id;
986
+ if (!typeId) return;
987
+ try {
988
+ if ((await this.sparkService.getPermissions(typeId))?.canViewHistory !== true) return;
989
+ const revisions = await this.sparkService.postEnvelope("/po/revisions", {
990
+ objectTypeId: typeId,
991
+ id: this.id,
992
+ take: 1
993
+ });
994
+ const latest = Array.isArray(revisions) ? revisions[0] : void 0;
995
+ if (!latest || typeof latest.userName !== "string" || latest.userName === "") return;
996
+ if (latest.isCurrent !== true || theirs.etag && latest.changeVector !== theirs.etag) return;
997
+ const by = modifiedBy(theirs);
998
+ if (by && latest.userId && latest.userId !== by) return;
999
+ this.changedByName.set({
1000
+ etag: theirs.etag,
1001
+ name: latest.userName
1002
+ });
1003
+ } catch {}
1004
+ }
1005
+ onConflictResolved(choices) {
1006
+ const pending = this.pendingConflict();
1007
+ if (!pending) return;
1008
+ const result = mergeThreeWay(pending.base, pending.mine, pending.theirsForm, pending.schema, choices);
1009
+ this.pendingConflict.set(null);
1010
+ this.rebase(pending.theirs, result.merged, result.theirChanges, "common.conflictResolved", result.contributionNotices);
1011
+ }
1012
+ onConflictCancelled() {
1013
+ this.pendingConflict.set(null);
1014
+ }
1015
+ rebase(theirs, merged, theirChanges, noticeKey, contributionNotices = []) {
1016
+ this.item.set(theirs);
1017
+ this.formData.set(structuredClone(merged));
1018
+ this.validationErrors.set([]);
1019
+ const fields = [...new Map(theirChanges.map((c) => [c.rootAttribute.name, c.rootAttribute])).values()].map((a) => this.language.resolve(a.label) || a.name);
1020
+ const text = fields.length > 0 ? this.language.t(noticeKey).replace("{fields}", fields.join(", ")) : this.language.t(`${noticeKey}NoFields`);
1021
+ const rows = [...new Set(contributionNotices.map((n) => n.rowLabel))];
1022
+ const contributions = rows.length > 0 ? " " + this.language.t("contributions.theirsKept").replace("{rows}", rows.join(", ")) : "";
1023
+ this.conflictNoticeText.set(text + contributions);
1024
+ }
1025
+ theyChanged = computed(() => {
1026
+ const pending = this.pendingConflict();
1027
+ if (!pending || pending.result.theirChanges.length === 0) return null;
1028
+ const fields = [...new Map(pending.result.theirChanges.map((c) => [c.rootAttribute.name, c.rootAttribute])).values()].map((a) => this.language.resolve(a.label) || a.name);
1029
+ return this.language.t("common.conflictTheyChanged").replace("{fields}", fields.join(", "));
1030
+ }, ...ngDevMode ? [{ debugName: "theyChanged" }] : /* istanbul ignore next */ []);
1031
+ conflictAudit = computed(() => {
1032
+ const pending = this.pendingConflict();
1033
+ return pending ? this.auditLine(pending.theirs) : null;
1034
+ }, ...ngDevMode ? [{ debugName: "conflictAudit" }] : /* istanbul ignore next */ []);
1035
+ auditLine(po) {
1036
+ const resolved = this.changedByName();
1037
+ const at = po.attributes.find((a) => a.name === "ModifiedAt")?.value;
1038
+ const user = this.showChangedBy && resolved && resolved.etag === po.etag ? resolved.name : null;
1039
+ let time = null;
1040
+ if (at) try {
1041
+ time = formatDate(at, "medium", this.locale);
1042
+ } catch {
1043
+ time = String(at);
1044
+ }
1045
+ if (user && time) return this.language.t("common.conflictChangedByAt").replace("{user}", user).replace("{time}", time);
1046
+ if (user) return this.language.t("common.conflictChangedBy").replace("{user}", user);
1047
+ if (time) return this.language.t("common.conflictChangedAt").replace("{time}", time);
1048
+ return null;
1049
+ }
170
1050
  static ɵfac = i0.ɵɵngDeclareFactory({
171
1051
  minVersion: "12.0.0",
172
1052
  version: "22.2.0",
@@ -185,8 +1065,15 @@ var SparkPoEditComponent = class SparkPoEditComponent {
185
1065
  saved: "saved",
186
1066
  cancelled: "cancelled"
187
1067
  },
1068
+ viewQueries: [{
1069
+ propertyName: "form",
1070
+ first: true,
1071
+ predicate: SparkPoFormComponent,
1072
+ descendants: true,
1073
+ isSignal: true
1074
+ }],
188
1075
  ngImport: i0,
189
- template: "<bs-container>\n<div class=\"container\">\n @for (error of generalErrors(); track error.errorMessage) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ error.errorMessage | resolveTranslation }}\n </bs-alert>\n }\n\n @if (entityType(); as et) {\n @if (item()) {\n <h2 class=\"mb-4\">{{ 'common.edit' | t }} {{ (et.description | resolveTranslation) || et.name }}</h2>\n\n <spark-po-form\n [entityType]=\"et\"\n [(formData)]=\"formData\"\n [(refreshOverlay)]=\"refreshOverlay\"\n [objectTypeId]=\"type\"\n [objectId]=\"id\"\n [validationErrors]=\"validationErrors()\"\n [showButtons]=\"true\"\n [isSaving]=\"isSaving()\"\n [parentId]=\"id\"\n [parentType]=\"type\"\n (save)=\"onSave()\"\n (cancel)=\"onCancel()\">\n </spark-po-form>\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n",
1076
+ template: "<bs-container>\n<div class=\"container\">\n @for (error of generalErrors(); track error.errorMessage) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ error.errorMessage | resolveTranslation }}\n </bs-alert>\n }\n\n @if (conflictNotice(); as notice) {\n <bs-alert [type]=\"colors.info\" class=\"mb-3 spark-conflict-notice\">\n {{ notice }}\n </bs-alert>\n }\n\n <spark-po-conflict-dialog\n [conflicts]=\"pendingConflicts()\"\n [referenceLabels]=\"conflictReferenceLabels()\"\n [audit]=\"conflictAudit()\"\n [theyChanged]=\"theyChanged()\"\n (resolved)=\"onConflictResolved($event)\"\n (cancelled)=\"onConflictCancelled()\" />\n\n @if (entityType(); as et) {\n @if (item()) {\n <h2 class=\"mb-4\">{{ 'common.edit' | t }} {{ (et.description | resolveTranslation) || et.name }}</h2>\n\n <spark-po-form\n [entityType]=\"formEntityType() ?? et\"\n [(formData)]=\"formData\"\n [(refreshOverlay)]=\"refreshOverlay\"\n [objectTypeId]=\"type\"\n [objectId]=\"id\"\n [validationErrors]=\"validationErrors()\"\n [showButtons]=\"true\"\n [isSaving]=\"isSaving()\"\n [parentId]=\"id\"\n [parentType]=\"type\"\n (save)=\"onSave()\"\n (cancel)=\"onCancel()\">\n </spark-po-form>\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n",
190
1077
  dependencies: [
191
1078
  {
192
1079
  kind: "ngmodule",
@@ -226,6 +1113,8 @@ var SparkPoEditComponent = class SparkPoEditComponent {
226
1113
  "isSaving",
227
1114
  "parentId",
228
1115
  "parentType",
1116
+ "referenceParentId",
1117
+ "referenceParentType",
229
1118
  "objectTypeId",
230
1119
  "objectId",
231
1120
  "triggerPathPrefix",
@@ -240,6 +1129,18 @@ var SparkPoEditComponent = class SparkPoEditComponent {
240
1129
  "refreshOverlayChange"
241
1130
  ]
242
1131
  },
1132
+ {
1133
+ kind: "component",
1134
+ type: SparkPoConflictDialogComponent,
1135
+ selector: "spark-po-conflict-dialog",
1136
+ inputs: [
1137
+ "conflicts",
1138
+ "referenceLabels",
1139
+ "audit",
1140
+ "theyChanged"
1141
+ ],
1142
+ outputs: ["resolved", "cancelled"]
1143
+ },
243
1144
  {
244
1145
  kind: "pipe",
245
1146
  type: ResolveTranslationPipe,
@@ -269,11 +1170,12 @@ i0.ɵɵngDeclareClassMetadata({
269
1170
  BsContainerComponent,
270
1171
  BsSpinnerComponent,
271
1172
  SparkPoFormComponent,
1173
+ SparkPoConflictDialogComponent,
272
1174
  ResolveTranslationPipe,
273
1175
  TranslateKeyPipe
274
1176
  ],
275
1177
  changeDetection: ChangeDetectionStrategy.OnPush,
276
- template: "<bs-container>\n<div class=\"container\">\n @for (error of generalErrors(); track error.errorMessage) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ error.errorMessage | resolveTranslation }}\n </bs-alert>\n }\n\n @if (entityType(); as et) {\n @if (item()) {\n <h2 class=\"mb-4\">{{ 'common.edit' | t }} {{ (et.description | resolveTranslation) || et.name }}</h2>\n\n <spark-po-form\n [entityType]=\"et\"\n [(formData)]=\"formData\"\n [(refreshOverlay)]=\"refreshOverlay\"\n [objectTypeId]=\"type\"\n [objectId]=\"id\"\n [validationErrors]=\"validationErrors()\"\n [showButtons]=\"true\"\n [isSaving]=\"isSaving()\"\n [parentId]=\"id\"\n [parentType]=\"type\"\n (save)=\"onSave()\"\n (cancel)=\"onCancel()\">\n </spark-po-form>\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n"
1178
+ template: "<bs-container>\n<div class=\"container\">\n @for (error of generalErrors(); track error.errorMessage) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ error.errorMessage | resolveTranslation }}\n </bs-alert>\n }\n\n @if (conflictNotice(); as notice) {\n <bs-alert [type]=\"colors.info\" class=\"mb-3 spark-conflict-notice\">\n {{ notice }}\n </bs-alert>\n }\n\n <spark-po-conflict-dialog\n [conflicts]=\"pendingConflicts()\"\n [referenceLabels]=\"conflictReferenceLabels()\"\n [audit]=\"conflictAudit()\"\n [theyChanged]=\"theyChanged()\"\n (resolved)=\"onConflictResolved($event)\"\n (cancelled)=\"onConflictCancelled()\" />\n\n @if (entityType(); as et) {\n @if (item()) {\n <h2 class=\"mb-4\">{{ 'common.edit' | t }} {{ (et.description | resolveTranslation) || et.name }}</h2>\n\n <spark-po-form\n [entityType]=\"formEntityType() ?? et\"\n [(formData)]=\"formData\"\n [(refreshOverlay)]=\"refreshOverlay\"\n [objectTypeId]=\"type\"\n [objectId]=\"id\"\n [validationErrors]=\"validationErrors()\"\n [showButtons]=\"true\"\n [isSaving]=\"isSaving()\"\n [parentId]=\"id\"\n [parentType]=\"type\"\n (save)=\"onSave()\"\n (cancel)=\"onCancel()\">\n </spark-po-form>\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n"
277
1179
  }]
278
1180
  }],
279
1181
  ctorParameters: () => [],
@@ -285,9 +1187,17 @@ i0.ɵɵngDeclareClassMetadata({
285
1187
  cancelled: [{
286
1188
  type: i0.Output,
287
1189
  args: ["cancelled"]
1190
+ }],
1191
+ form: [{
1192
+ type: i0.ViewChild,
1193
+ args: [i0.forwardRef(() => SparkPoFormComponent), { isSignal: true }]
288
1194
  }]
289
1195
  }
290
1196
  });
291
- export { SparkPoEditComponent };
1197
+ function modifiedBy(po) {
1198
+ const by = po.attributes.find((a) => a.name === "ModifiedBy")?.value;
1199
+ return typeof by === "string" && by !== "" ? by : null;
1200
+ }
1201
+ export { CONTRIBUTION_ROW_METADATA, SparkPoConflictDialogComponent, SparkPoEditComponent, addReferenceLabelsOf, addReferenceLabelsOfOptions, contributionRowIsOwn, mergeThreeWay };
292
1202
 
293
1203
  //# sourceMappingURL=mintplayer-ng-spark-po-edit.mjs.map