@mintplayer/ng-spark 22.23.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.
Files changed (71) hide show
  1. package/README.md +196 -0
  2. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs +100 -47
  3. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs.map +1 -1
  4. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +310 -329
  5. package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
  6. package/fesm2022/mintplayer-ng-spark-column-filter.mjs +175 -135
  7. package/fesm2022/mintplayer-ng-spark-column-filter.mjs.map +1 -1
  8. package/fesm2022/mintplayer-ng-spark-contributions.mjs +721 -0
  9. package/fesm2022/mintplayer-ng-spark-contributions.mjs.map +1 -0
  10. package/fesm2022/mintplayer-ng-spark-grid.mjs +2385 -1118
  11. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  12. package/fesm2022/mintplayer-ng-spark-history.mjs +598 -0
  13. package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -0
  14. package/fesm2022/mintplayer-ng-spark-icon.mjs +64 -27
  15. package/fesm2022/mintplayer-ng-spark-icon.mjs.map +1 -1
  16. package/fesm2022/mintplayer-ng-spark-models.mjs +405 -762
  17. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  18. package/fesm2022/mintplayer-ng-spark-moderation.mjs +1212 -0
  19. package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -0
  20. package/fesm2022/mintplayer-ng-spark-panels.mjs +60 -0
  21. package/fesm2022/mintplayer-ng-spark-panels.mjs.map +1 -0
  22. package/fesm2022/mintplayer-ng-spark-pipes.mjs +1158 -765
  23. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  24. package/fesm2022/mintplayer-ng-spark-po-create.mjs +273 -169
  25. package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
  26. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +795 -410
  27. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  28. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +1196 -280
  29. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  30. package/fesm2022/mintplayer-ng-spark-po-form.mjs +2244 -1224
  31. package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
  32. package/fesm2022/mintplayer-ng-spark-query-list.mjs +571 -303
  33. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  34. package/fesm2022/mintplayer-ng-spark-renderers.mjs +44 -55
  35. package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
  36. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +216 -177
  37. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
  38. package/fesm2022/mintplayer-ng-spark-routes.mjs +24 -28
  39. package/fesm2022/mintplayer-ng-spark-routes.mjs.map +1 -1
  40. package/fesm2022/mintplayer-ng-spark-services.mjs +625 -640
  41. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  42. package/fesm2022/mintplayer-ng-spark-shell.mjs +1031 -397
  43. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  44. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +294 -0
  45. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs.map +1 -0
  46. package/fesm2022/mintplayer-ng-spark.mjs +14 -48
  47. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  48. package/package.json +23 -2
  49. package/types/mintplayer-ng-spark-attribute-description.d.ts +25 -18
  50. package/types/mintplayer-ng-spark-client-operations.d.ts +139 -160
  51. package/types/mintplayer-ng-spark-column-filter.d.ts +73 -55
  52. package/types/mintplayer-ng-spark-contributions.d.ts +231 -0
  53. package/types/mintplayer-ng-spark-grid.d.ts +917 -448
  54. package/types/mintplayer-ng-spark-history.d.ts +216 -0
  55. package/types/mintplayer-ng-spark-icon.d.ts +15 -13
  56. package/types/mintplayer-ng-spark-models.d.ts +681 -573
  57. package/types/mintplayer-ng-spark-moderation.d.ts +402 -0
  58. package/types/mintplayer-ng-spark-panels.d.ts +143 -0
  59. package/types/mintplayer-ng-spark-pipes.d.ts +131 -149
  60. package/types/mintplayer-ng-spark-po-create.d.ts +46 -37
  61. package/types/mintplayer-ng-spark-po-detail.d.ts +180 -117
  62. package/types/mintplayer-ng-spark-po-edit.d.ts +367 -64
  63. package/types/mintplayer-ng-spark-po-form.d.ts +558 -373
  64. package/types/mintplayer-ng-spark-query-list.d.ts +143 -82
  65. package/types/mintplayer-ng-spark-renderers.d.ts +121 -85
  66. package/types/mintplayer-ng-spark-retry-action-modal.d.ts +31 -34
  67. package/types/mintplayer-ng-spark-routes.d.ts +7 -11
  68. package/types/mintplayer-ng-spark-services.d.ts +377 -261
  69. package/types/mintplayer-ng-spark-shell.d.ts +228 -132
  70. package/types/mintplayer-ng-spark-soft-delete.d.ts +90 -0
  71. package/types/mintplayer-ng-spark.d.ts +19 -12
@@ -1,285 +1,1201 @@
1
- import * as i0 from '@angular/core';
2
- import { inject, output, signal, computed, ChangeDetectionStrategy, Component } from '@angular/core';
3
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
- import { CommonModule } from '@angular/common';
5
- import { ActivatedRoute, Router } from '@angular/router';
6
- import { Color } from '@mintplayer/ng-bootstrap';
7
- import { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';
8
- import { BsContainerComponent } from '@mintplayer/ng-bootstrap/container';
9
- import { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';
10
- import { SparkService, SparkLanguageService } from '@mintplayer/ng-spark/services';
11
- import { SparkPoFormComponent } from '@mintplayer/ng-spark/po-form';
12
- import { ResolveTranslationPipe, TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';
13
- import { nestedPoToDict, isDateDataType, toDateInputValue, applyOverlay, hasShowedOnFlag, ShowedOn, dictToNestedPo, fromDateInputValue, wireDatesEqual } from '@mintplayer/ng-spark/models';
1
+ import * as i0 from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, LOCALE_ID, computed, effect, inject, input, output, signal, untracked, viewChild } from "@angular/core";
3
+ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
4
+ import { CommonModule, NgTemplateOutlet, formatDate } from "@angular/common";
5
+ import { ActivatedRoute, Router } from "@angular/router";
6
+ import { Color } from "@mintplayer/ng-bootstrap";
7
+ import { BsAlertComponent } from "@mintplayer/ng-bootstrap/alert";
8
+ import { BsContainerComponent } from "@mintplayer/ng-bootstrap/container";
9
+ import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
10
+ import { SPARK_CONFIG } from "@mintplayer/ng-spark";
11
+ import { SparkLanguageService, SparkService } from "@mintplayer/ng-spark/services";
12
+ import { SparkPoFormComponent } from "@mintplayer/ng-spark/po-form";
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>
14
457
 
15
- class SparkPoEditComponent {
16
- route = inject(ActivatedRoute);
17
- router = inject(Router);
18
- sparkService = inject(SparkService);
19
- language = inject(SparkLanguageService);
20
- saved = output();
21
- cancelled = output();
22
- colors = Color;
23
- entityType = signal(null, /* @ts-ignore */
24
- ...(ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []));
25
- item = signal(null, /* @ts-ignore */
26
- ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
27
- type = '';
28
- id = '';
29
- formData = signal({}, /* @ts-ignore */
30
- ...(ngDevMode ? [{ debugName: "formData" }] : /* istanbul ignore next */ []));
31
- /**
32
- * Bound two-way to the form, which is where refreshes land. Read by
33
- * {@link getEditableAttributes} so the save sees the object the user was actually shown.
34
- */
35
- refreshOverlay = signal({}, /* @ts-ignore */
36
- ...(ngDevMode ? [{ debugName: "refreshOverlay" }] : /* istanbul ignore next */ []));
37
- validationErrors = signal([], /* @ts-ignore */
38
- ...(ngDevMode ? [{ debugName: "validationErrors" }] : /* istanbul ignore next */ []));
39
- isSaving = signal(false, /* @ts-ignore */
40
- ...(ngDevMode ? [{ debugName: "isSaving" }] : /* istanbul ignore next */ []));
41
- // Cached list of every entity type — needed by the AsDetail save path to resolve
42
- // nested type schemas when rebuilding the nested PO wire shape from the flat form dict.
43
- allEntityTypes = signal([], /* @ts-ignore */
44
- ...(ngDevMode ? [{ debugName: "allEntityTypes" }] : /* istanbul ignore next */ []));
45
- generalErrors = computed(() => this.validationErrors().filter(e => !e.attributeName), /* @ts-ignore */
46
- ...(ngDevMode ? [{ debugName: "generalErrors" }] : /* istanbul ignore next */ []));
47
- constructor() {
48
- this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => this.onParamsChange(params));
49
- }
50
- async onParamsChange(params) {
51
- this.type = params.get('type') || '';
52
- this.id = params.get('id') || '';
53
- try {
54
- const [types, item] = await Promise.all([
55
- this.sparkService.getEntityTypes(),
56
- this.sparkService.get(this.type, this.id)
57
- ]);
58
- const entityType = types.find(t => t.id === this.type || t.alias === this.type) || null;
59
- this.entityType.set(entityType);
60
- this.allEntityTypes.set(types);
61
- this.item.set(item);
62
- this.initFormData();
63
- }
64
- catch (e) {
65
- const error = e;
66
- this.validationErrors.set([{
67
- attributeName: '',
68
- errorMessage: { en: error.error?.error || error.message || 'An unexpected error occurred' },
69
- ruleType: 'error'
70
- }]);
71
- }
72
- }
73
- initFormData() {
74
- const data = {};
75
- const currentItem = this.item();
76
- this.getEditableAttributes().forEach(attr => {
77
- const itemAttr = currentItem?.attributes.find(a => a.name === attr.name);
78
- if (attr.dataType === 'Reference') {
79
- data[attr.name] = itemAttr?.value ?? null;
80
- }
81
- else if (attr.dataType === 'AsDetail') {
82
- // Server emits nested PO(s) in attr.object / attr.objects. Flatten back into the
83
- // Record<string, any> shape the form has always used so the rest of the component
84
- // tree stays unchanged.
85
- if (attr.isArray) {
86
- data[attr.name] = (itemAttr?.objects ?? []).map(po => nestedPoToDict(po));
87
- }
88
- else {
89
- data[attr.name] = itemAttr?.object ? nestedPoToDict(itemAttr.object) : {};
90
- }
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>
91
470
  }
92
- else if (attr.dataType === 'boolean' && !attr.lookupReferenceType) {
93
- // `?? false` only for a real two-state checkbox. A lookup-backed boolean has a third state
94
- // that is expressed as the absence of a value, and coercing it here destroyed that BEFORE
95
- // the control rendered: merely opening the form and saving turned "unset" into an explicit
96
- // false, permanently and with nothing shown to the user.
97
- data[attr.name] = itemAttr?.value ?? false;
98
- }
99
- else if (isDateDataType(attr.dataType)) {
100
- // The wire carries a full ISO-8601 instant with an offset; a native date/time control accepts
101
- // only a bare local wall clock. Assigning the wire value directly does not fail loudly -- the
102
- // control just renders blank, and saving the untouched form writes that blank back.
103
- data[attr.name] = toDateInputValue(attr.dataType, itemAttr?.value);
104
- }
105
- else {
106
- data[attr.name] = itemAttr?.value ?? '';
107
- }
108
- });
109
- this.formData.set(data);
110
- }
111
- /**
112
- * Resolves a nested AsDetail type by CLR name, for rebuilding the nested PO wire shape on save.
113
- *
114
- * ⚠️ <b>`detailTypes` first, catalogue second.</b> The catalogue from `/spark/types` is
115
- * <b>Query-gated</b>, and an AsDetail row type usually has no rights of its own — nobody grants
116
- * `Query/GateSettings`, because a gate is edited through the Repository that owns it. So the
117
- * catalogue does not contain it, the resolver returned undefined, and the save quietly took the
118
- * scalar branch: the attribute went out as a raw dict under `value` instead of a nested PO under
119
- * `object`, the server could not map it, and `EntityMapper`'s conversion `catch` swallowed the
120
- * failure. The save reported success and wrote nothing.
121
- * <para>
122
- * `detailTypes` is the parent's own copy of its row types, carried on the type definition for
123
- * exactly this reason (#385 fixed the rendering half; this is the save half). It is gated on the
124
- * parent's right, which is the right that governs editing the row anyway.
125
- * </para>
126
- */
127
- resolveEntityType() {
128
- const cache = this.allEntityTypes();
129
- const detailTypes = this.entityType()?.detailTypes ?? [];
130
- return (clrName) => detailTypes.find(t => t.clrType === clrName) ?? cache.find(t => t.clrType === clrName);
131
- }
132
- /**
133
- * The attributes this page will read values from when it builds the save.
134
- *
135
- * ⚠️ <b>Overlaid before filtering, not after loading.</b> A refresh hook can reveal an attribute
136
- * that was hidden when the object was loaded, and the ordinary reason it does so is that the
137
- * attribute has just become required. Filtering on the loaded state left such an attribute out of
138
- * both {@link initFormData} and the save payload, so the user filled in a field whose value was
139
- * then dropped on the floor and refused by the server as missing — with no way out of the form.
140
- * The overlay is the form's, bound two-way, so the two halves cannot drift again.
141
- */
142
- getEditableAttributes() {
143
- const overlay = this.refreshOverlay();
144
- return this.entityType()?.attributes
145
- .map(a => applyOverlay(a, overlay[a.name]))
146
- .filter(a => a.isVisible && !a.isReadOnly && hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject))
147
- .sort((a, b) => a.order - b.order) || [];
148
- }
149
- async onSave() {
150
- const currentItem = this.item();
151
- if (!this.entityType() || !currentItem)
152
- return;
153
- this.validationErrors.set([]);
154
- this.isSaving.set(true);
155
- const resolver = this.resolveEntityType();
156
- const attributes = currentItem.attributes.map(attr => {
157
- const editableAttr = this.getEditableAttributes().find(a => a.name === attr.name);
158
- // AsDetail: formData[name] is a flat dict (single) or array of dicts (array). Rebuild
159
- // the nested PO wire shape so the server's polymorphic converter hydrates it into a
160
- // PersistentObjectAttributeAsDetail.
161
- if (editableAttr?.dataType === 'AsDetail' && editableAttr.asDetailType) {
162
- const nestedType = resolver(editableAttr.asDetailType);
163
- if (nestedType) {
164
- const raw = this.formData()[attr.name];
165
- if (editableAttr.isArray) {
166
- const items = Array.isArray(raw) ? raw : [];
167
- return {
168
- ...attr,
169
- value: null,
170
- object: null,
171
- objects: items.map(item => dictToNestedPo((item ?? {}), nestedType, resolver)),
172
- asDetailType: editableAttr.asDetailType,
173
- isValueChanged: true,
174
- };
175
- }
176
- return {
177
- ...attr,
178
- value: null,
179
- object: raw ? dictToNestedPo(raw, nestedType, resolver) : null,
180
- objects: null,
181
- asDetailType: editableAttr.asDetailType,
182
- isValueChanged: true,
183
- };
184
- }
185
- }
186
- // `in`, not a truthiness or `?? attr.value` check: an attribute the refresh revealed has no
187
- // slot until its control writes one, and an attribute the user cleared has a slot holding ''.
188
- // Coalescing would resurrect the loaded value on exactly the edit that removed it.
189
- const formData = this.formData();
190
- const rawValue = editableAttr && attr.name in formData ? formData[attr.name] : attr.value;
191
- if (editableAttr && isDateDataType(editableAttr.dataType)) {
192
- // Back out of the control's bare wall clock into a complete ISO-8601 instant, carrying the
193
- // viewer's offset for the entered date.
194
- const converted = fromDateInputValue(editableAttr.dataType, rawValue);
195
- // Compare by instant, not by text: the round trip rewrites the offset to the viewer's even
196
- // when nothing was edited.
197
- const changed = !wireDatesEqual(converted, attr.value);
198
- return {
199
- ...attr,
200
- // When the value did not actually change, send back exactly what was loaded. Sending the
201
- // re-converted string would preserve the instant but rewrite the stored offset to the
202
- // viewer's, so merely opening a record and saving it would relabel a Seattle registration
203
- // as a Brussels one. The offset is not business data (so the instant is what we guarantee),
204
- // but there is no reason to discard it on a save that changed nothing.
205
- value: changed ? converted : attr.value,
206
- isValueChanged: changed,
207
- };
208
- }
209
- const newValue = rawValue;
210
- return {
211
- ...attr,
212
- value: newValue,
213
- isValueChanged: editableAttr ? newValue !== attr.value : false
214
- };
215
- });
216
- const po = {
217
- id: currentItem.id,
218
- // Send back the token this object was loaded with. Left out, the server skips the
219
- // concurrency check entirely -- it is opt-in by presence -- and a save over somebody else's
220
- // edit succeeds silently. Left as undefined when the server sent none, which drops the key
221
- // from the JSON and restores exactly the old behaviour rather than sending an empty string
222
- // that could never match.
223
- etag: currentItem.etag,
224
- name: this.formData()['Name'] || currentItem.name,
225
- objectTypeId: this.entityType().id,
226
- attributes
227
- };
228
- try {
229
- const result = await this.sparkService.update(this.type, this.id, po);
230
- this.isSaving.set(false);
231
- this.saved.emit(result);
232
- this.router.navigate(['/po', this.type, this.id]);
233
- }
234
- catch (e) {
235
- this.isSaving.set(false);
236
- const error = e;
237
- // ⚠️ The errors live under `result`, because every endpoint wraps its body in a
238
- // ClientOperationEnvelope: `{ result: { errors: [...] }, operations: [] }`. Reading
239
- // `error.error.errors` matched nothing, so this branch was dead and EVERY save-time
240
- // validation failure fell through to the raw Angular HTTP string below.
241
- const errors = error.error?.result?.errors ?? error.error?.errors;
242
- if (error.status === 400 && errors) {
243
- this.validationErrors.set(errors);
244
- }
245
- else if (error.status === 409) {
246
- // Somebody saved this record between the load and this save. The server's own body says
247
- // only "Concurrency conflict" -- deliberately, since the real message carries the change
248
- // vector -- which is accurate, untranslated, and tells the user nothing to do about it.
249
- // The form keeps its values, so the typing is not lost.
250
- this.validationErrors.set([{
251
- attributeName: '',
252
- errorMessage: { en: this.language.t('common.concurrencyConflict') },
253
- ruleType: 'error'
254
- }]);
255
- }
256
- else {
257
- this.validationErrors.set([{
258
- attributeName: '',
259
- // Prefer the server's own message, as the load path already does. A refusal arrives as
260
- // 404 by design (an anti-oracle measure), and `error.message` renders that as
261
- // "Http failure response for /spark/po/...: 404 Not Found" inside the validation summary.
262
- errorMessage: { en: error.error?.result?.error || error.error?.error || error.message || 'An unexpected error occurred' },
263
- ruleType: 'error'
264
- }]);
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>
265
665
  }
266
- }
267
- }
268
- onCancel() {
269
- this.cancelled.emit();
270
- this.router.navigate(['/po', this.type, this.id]);
271
- }
272
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkPoEditComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
273
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkPoEditComponent, isStandalone: true, selector: "spark-po-edit", outputs: { saved: "saved", cancelled: "cancelled" }, ngImport: i0, 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", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: BsAlertComponent, selector: "bs-alert", inputs: ["type", "announce", "isVisible"], outputs: ["isVisibleChange", "afterOpenedOrClosed"] }, { kind: "component", type: BsContainerComponent, selector: "bs-container" }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: SparkPoFormComponent, selector: "spark-po-form", inputs: ["entityType", "formData", "validationErrors", "showButtons", "isSaving", "parentId", "parentType", "objectTypeId", "objectId", "triggerPathPrefix", "refreshOverlay"], outputs: ["formDataChange", "save", "cancel", "nestedTriggerRequested", "nestedTriggerBlurred", "refreshOverlayChange"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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);
274
731
  }
275
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkPoEditComponent, decorators: [{
276
- type: Component,
277
- args: [{ selector: 'spark-po-edit', imports: [CommonModule, BsAlertComponent, BsContainerComponent, BsSpinnerComponent, SparkPoFormComponent, ResolveTranslationPipe, TranslateKeyPipe], changeDetection: ChangeDetectionStrategy.OnPush, 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" }]
278
- }], ctorParameters: () => [], propDecorators: { saved: [{ type: i0.Output, args: ["saved"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }] } });
279
-
280
- /**
281
- * Generated bundle index. Do not edit.
282
- */
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
+ }
749
+ var SparkPoEditComponent = class SparkPoEditComponent {
750
+ route = inject(ActivatedRoute);
751
+ router = inject(Router);
752
+ sparkService = inject(SparkService);
753
+ language = inject(SparkLanguageService);
754
+ showChangedBy = inject(SPARK_CONFIG, { optional: true })?.conflictDialog?.showChangedBy === true;
755
+ saved = output();
756
+ cancelled = output();
757
+ colors = Color;
758
+ entityType = signal(null, ...ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []);
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 */ []);
772
+ type = "";
773
+ id = "";
774
+ formData = signal({}, ...ngDevMode ? [{ debugName: "formData" }] : /* istanbul ignore next */ []);
775
+ refreshOverlay = signal({}, ...ngDevMode ? [{ debugName: "refreshOverlay" }] : /* istanbul ignore next */ []);
776
+ validationErrors = signal([], ...ngDevMode ? [{ debugName: "validationErrors" }] : /* istanbul ignore next */ []);
777
+ isSaving = signal(false, ...ngDevMode ? [{ debugName: "isSaving" }] : /* istanbul ignore next */ []);
778
+ allEntityTypes = signal([], ...ngDevMode ? [{ debugName: "allEntityTypes" }] : /* istanbul ignore next */ []);
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);
793
+ constructor() {
794
+ this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => this.onParamsChange(params));
795
+ }
796
+ async onParamsChange(params) {
797
+ this.type = params.get("type") || "";
798
+ this.id = params.get("id") || "";
799
+ try {
800
+ const [types, item] = await Promise.all([this.sparkService.getEntityTypes(), this.sparkService.get(this.type, this.id)]);
801
+ const entityType = types.find((t) => t.id === this.type || t.alias === this.type) || null;
802
+ this.entityType.set(entityType);
803
+ this.allEntityTypes.set(types);
804
+ this.item.set(item);
805
+ this.initFormData();
806
+ } catch (e) {
807
+ const error = e;
808
+ this.validationErrors.set([{
809
+ attributeName: "",
810
+ errorMessage: { en: error.error?.error || error.message || "An unexpected error occurred" },
811
+ ruleType: "error"
812
+ }]);
813
+ }
814
+ }
815
+ initFormData() {
816
+ this.formData.set(this.formDataFrom(this.item()));
817
+ }
818
+ formDataFrom(currentItem) {
819
+ const data = {};
820
+ this.getEditableAttributes().forEach((attr) => {
821
+ const itemAttr = currentItem?.attributes.find((a) => a.name === attr.name);
822
+ if (attr.dataType === "Reference") data[attr.name] = itemAttr?.value ?? null;
823
+ else if (attr.dataType === "AsDetail") {
824
+ if (attr.isArray) data[attr.name] = (itemAttr?.objects ?? []).map((po) => nestedPoToDict(po));
825
+ else data[attr.name] = itemAttr?.object ? nestedPoToDict(itemAttr.object) : {};
826
+ } else if (attr.dataType === "boolean" && !attr.lookupReferenceType) data[attr.name] = itemAttr?.value ?? false;
827
+ else if (isDateDataType(attr.dataType)) data[attr.name] = toDateInputValue(attr.dataType, itemAttr?.value);
828
+ else data[attr.name] = itemAttr?.value ?? "";
829
+ });
830
+ return data;
831
+ }
832
+ resolveEntityType() {
833
+ const cache = this.allEntityTypes();
834
+ const detailTypes = this.entityType()?.detailTypes ?? [];
835
+ return (clrName) => detailTypes.find((t) => t.clrType === clrName) ?? cache.find((t) => t.clrType === clrName);
836
+ }
837
+ getEditableAttributes() {
838
+ const overlay = this.refreshOverlay();
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) || [];
840
+ }
841
+ async onSave() {
842
+ const currentItem = this.item();
843
+ if (!this.entityType() || !currentItem) return;
844
+ this.validationErrors.set([]);
845
+ this.conflictNoticeText.set(null);
846
+ this.isSaving.set(true);
847
+ const resolver = this.resolveEntityType();
848
+ const attributes = currentItem.attributes.map((attr) => {
849
+ const editableAttr = this.getEditableAttributes().find((a) => a.name === attr.name);
850
+ if (editableAttr?.dataType === "AsDetail" && editableAttr.asDetailType) {
851
+ const nestedType = resolver(editableAttr.asDetailType);
852
+ if (nestedType) {
853
+ const raw = this.formData()[attr.name];
854
+ if (editableAttr.isArray) {
855
+ const items = Array.isArray(raw) ? raw : [];
856
+ return {
857
+ ...attr,
858
+ value: null,
859
+ object: null,
860
+ objects: items.map((item) => dictToNestedPo(item ?? {}, nestedType, resolver)),
861
+ asDetailType: editableAttr.asDetailType,
862
+ isValueChanged: true
863
+ };
864
+ }
865
+ return {
866
+ ...attr,
867
+ value: null,
868
+ object: raw ? dictToNestedPo(raw, nestedType, resolver) : null,
869
+ objects: null,
870
+ asDetailType: editableAttr.asDetailType,
871
+ isValueChanged: true
872
+ };
873
+ }
874
+ }
875
+ const formData = this.formData();
876
+ const rawValue = editableAttr && attr.name in formData ? formData[attr.name] : attr.value;
877
+ if (editableAttr && isDateDataType(editableAttr.dataType)) {
878
+ const converted = fromDateInputValue(editableAttr.dataType, rawValue);
879
+ const changed = !wireDatesEqual(converted, attr.value);
880
+ return {
881
+ ...attr,
882
+ value: changed ? converted : attr.value,
883
+ isValueChanged: changed
884
+ };
885
+ }
886
+ const newValue = rawValue;
887
+ return {
888
+ ...attr,
889
+ value: newValue,
890
+ isValueChanged: editableAttr ? newValue !== attr.value : false
891
+ };
892
+ });
893
+ const po = {
894
+ id: currentItem.id,
895
+ etag: currentItem.etag,
896
+ name: this.formData()["Name"] || currentItem.name,
897
+ objectTypeId: this.entityType().id,
898
+ attributes
899
+ };
900
+ try {
901
+ const result = await this.sparkService.update(this.type, this.id, po);
902
+ this.isSaving.set(false);
903
+ this.saved.emit(result);
904
+ this.router.navigate([
905
+ "/po",
906
+ this.type,
907
+ this.id
908
+ ]);
909
+ } catch (e) {
910
+ this.isSaving.set(false);
911
+ const error = e;
912
+ const errors = error.error?.result?.errors ?? error.error?.errors;
913
+ if (error.status === 400 && errors) this.validationErrors.set(errors);
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([{
922
+ attributeName: "",
923
+ errorMessage: { en: error.error?.result?.error || error.error?.error || error.message || "An unexpected error occurred" },
924
+ ruleType: "error"
925
+ }]);
926
+ }
927
+ }
928
+ onCancel() {
929
+ this.cancelled.emit();
930
+ this.router.navigate([
931
+ "/po",
932
+ this.type,
933
+ this.id
934
+ ]);
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
+ }
1050
+ static ɵfac = i0.ɵɵngDeclareFactory({
1051
+ minVersion: "12.0.0",
1052
+ version: "22.2.0",
1053
+ ngImport: i0,
1054
+ type: SparkPoEditComponent,
1055
+ deps: [],
1056
+ target: i0.ɵɵFactoryTarget.Component
1057
+ });
1058
+ static ɵcmp = i0.ɵɵngDeclareComponent({
1059
+ minVersion: "17.0.0",
1060
+ version: "22.2.0",
1061
+ type: SparkPoEditComponent,
1062
+ isStandalone: true,
1063
+ selector: "spark-po-edit",
1064
+ outputs: {
1065
+ saved: "saved",
1066
+ cancelled: "cancelled"
1067
+ },
1068
+ viewQueries: [{
1069
+ propertyName: "form",
1070
+ first: true,
1071
+ predicate: SparkPoFormComponent,
1072
+ descendants: true,
1073
+ isSignal: true
1074
+ }],
1075
+ ngImport: i0,
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",
1077
+ dependencies: [
1078
+ {
1079
+ kind: "ngmodule",
1080
+ type: CommonModule
1081
+ },
1082
+ {
1083
+ kind: "component",
1084
+ type: BsAlertComponent,
1085
+ selector: "bs-alert",
1086
+ inputs: [
1087
+ "type",
1088
+ "announce",
1089
+ "isVisible"
1090
+ ],
1091
+ outputs: ["isVisibleChange", "afterOpenedOrClosed"]
1092
+ },
1093
+ {
1094
+ kind: "component",
1095
+ type: BsContainerComponent,
1096
+ selector: "bs-container"
1097
+ },
1098
+ {
1099
+ kind: "component",
1100
+ type: BsSpinnerComponent,
1101
+ selector: "bs-spinner",
1102
+ inputs: ["type", "color"]
1103
+ },
1104
+ {
1105
+ kind: "component",
1106
+ type: SparkPoFormComponent,
1107
+ selector: "spark-po-form",
1108
+ inputs: [
1109
+ "entityType",
1110
+ "formData",
1111
+ "validationErrors",
1112
+ "showButtons",
1113
+ "isSaving",
1114
+ "parentId",
1115
+ "parentType",
1116
+ "objectTypeId",
1117
+ "objectId",
1118
+ "triggerPathPrefix",
1119
+ "refreshOverlay"
1120
+ ],
1121
+ outputs: [
1122
+ "formDataChange",
1123
+ "save",
1124
+ "cancel",
1125
+ "nestedTriggerRequested",
1126
+ "nestedTriggerBlurred",
1127
+ "refreshOverlayChange"
1128
+ ]
1129
+ },
1130
+ {
1131
+ kind: "component",
1132
+ type: SparkPoConflictDialogComponent,
1133
+ selector: "spark-po-conflict-dialog",
1134
+ inputs: [
1135
+ "conflicts",
1136
+ "referenceLabels",
1137
+ "audit",
1138
+ "theyChanged"
1139
+ ],
1140
+ outputs: ["resolved", "cancelled"]
1141
+ },
1142
+ {
1143
+ kind: "pipe",
1144
+ type: ResolveTranslationPipe,
1145
+ name: "resolveTranslation"
1146
+ },
1147
+ {
1148
+ kind: "pipe",
1149
+ type: TranslateKeyPipe,
1150
+ name: "t"
1151
+ }
1152
+ ],
1153
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
1154
+ });
1155
+ };
1156
+ i0.ɵɵngDeclareClassMetadata({
1157
+ minVersion: "12.0.0",
1158
+ version: "22.2.0",
1159
+ ngImport: i0,
1160
+ type: SparkPoEditComponent,
1161
+ decorators: [{
1162
+ type: Component,
1163
+ args: [{
1164
+ selector: "spark-po-edit",
1165
+ imports: [
1166
+ CommonModule,
1167
+ BsAlertComponent,
1168
+ BsContainerComponent,
1169
+ BsSpinnerComponent,
1170
+ SparkPoFormComponent,
1171
+ SparkPoConflictDialogComponent,
1172
+ ResolveTranslationPipe,
1173
+ TranslateKeyPipe
1174
+ ],
1175
+ changeDetection: ChangeDetectionStrategy.OnPush,
1176
+ 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"
1177
+ }]
1178
+ }],
1179
+ ctorParameters: () => [],
1180
+ propDecorators: {
1181
+ saved: [{
1182
+ type: i0.Output,
1183
+ args: ["saved"]
1184
+ }],
1185
+ cancelled: [{
1186
+ type: i0.Output,
1187
+ args: ["cancelled"]
1188
+ }],
1189
+ form: [{
1190
+ type: i0.ViewChild,
1191
+ args: [i0.forwardRef(() => SparkPoFormComponent), { isSignal: true }]
1192
+ }]
1193
+ }
1194
+ });
1195
+ function modifiedBy(po) {
1196
+ const by = po.attributes.find((a) => a.name === "ModifiedBy")?.value;
1197
+ return typeof by === "string" && by !== "" ? by : null;
1198
+ }
1199
+ export { CONTRIBUTION_ROW_METADATA, SparkPoConflictDialogComponent, SparkPoEditComponent, addReferenceLabelsOf, addReferenceLabelsOfOptions, contributionRowIsOwn, mergeThreeWay };
283
1200
 
284
- export { SparkPoEditComponent };
285
- //# sourceMappingURL=mintplayer-ng-spark-po-edit.mjs.map
1201
+ //# sourceMappingURL=mintplayer-ng-spark-po-edit.mjs.map